sync code

This commit is contained in:
Tsanie Lily 2023-06-26 22:33:44 +08:00
parent 2d87e50dd0
commit 4bf330f824
26 changed files with 2644 additions and 62 deletions

View File

@ -5,6 +5,8 @@ public sealed class Constants
public const string CategoryOther = "other";
public const string EventUnknown = "unknown";
public const string BaseUrl = "https://flower.tsanie.org";
public const SQLite.SQLiteOpenFlags Flags =
SQLite.SQLiteOpenFlags.ReadWrite |
SQLite.SQLiteOpenFlags.Create |

View File

@ -26,12 +26,12 @@ public class FlowerDatabase
#if DEBUG
var result =
#endif
await database.CreateTablesAsync<FlowerItem, RecordItem>();
await database.CreateTablesAsync<FlowerItem, RecordItem, PhotoItem>();
#if DEBUG
foreach (var item in result.Results)
{
logger.LogDebug("create table {table}, result: {result}", item.Key, item.Value);
logger.LogInformation("create table {table}, result: {result}", item.Key, item.Value);
}
#endif
}

View File

@ -18,11 +18,11 @@ public class FlowerItem
{
if (categoryName == null)
{
if (!Constants.Categories.TryGetValue(CategoryId, out categoryName))
if (!Constants.Categories.TryGetValue(CategoryId, out var name))
{
categoryName = Constants.CategoryOther;
name = Constants.CategoryOther;
}
// TODO: i18n
categoryName = LocalizationResource.GetText(name);
}
return categoryName;
}
@ -40,6 +40,6 @@ public class FlowerItem
[Column("purchase")]
public string Purchase { get; set; }
[Column("photo")]
public byte[] Photo { get; set; }
[Column("memo")]
public string Memo { get; set; }
}

View File

@ -0,0 +1,28 @@
using SQLite;
namespace Blahblah.FlowerStory.Data.Model;
[Table("photos")]
public class PhotoItem
{
[Column("pid"), PrimaryKey, AutoIncrement]
public int Id { get; set; }
[Column("fid")]
public int FlowerId { get; set; }
[Column("rid")]
public int RecordId { get; set; }
[Column("filetype")]
public string FileType { get; set; }
[Column("filename")]
public string FileName { get; set; }
[Column("path")]
public string Path { get; set; }
[Column("dateupload")]
public DateTimeOffset DateUpload { get; set; }
}

View File

@ -8,6 +8,9 @@ public class RecordItem
[Column("rid"), PrimaryKey, AutoIncrement]
public int Id { get; set; }
[Column("fid")]
public int FlowerId { get; set; }
[Column("eid")]
public int EventId { get; set; }
@ -18,15 +21,16 @@ public class RecordItem
{
if (eventName == null)
{
string evtkey;
if (Constants.Events.TryGetValue(EventId, out var @event))
{
eventName = @event.Name;
evtkey = @event.Name;
}
else
{
eventName = Constants.EventUnknown;
evtkey = Constants.EventUnknown;
}
// TODO: i18n
eventName = LocalizationResource.GetText(evtkey);
}
return eventName;
}
@ -41,6 +45,6 @@ public class RecordItem
[Column("byname")]
public string ByUserName { get; set; }
[Column("photo")]
public byte[] Photo { get; set; }
[Column("memo")]
public string Memo { get; set; }
}

View File

@ -2,10 +2,19 @@
public class UserItem
{
public string Id { get; set; }
public int Id { get; set; }
public string UserId { get; set; }
public int Level { get; set; }
public DateTimeOffset RegisterDate { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Mobile { get; set; }
public byte[] Avatar { get; set; }
}

36
App/Extensions.cs Normal file
View File

@ -0,0 +1,36 @@
using Microsoft.Extensions.Localization;
using System.Text;
namespace Blahblah.FlowerStory;
[ContentProperty(nameof(Key))]
public class LocalizeExtension : IMarkupExtension
{
//private IStringLocalizer<Localizations> Localizer { get; }
public string Key { get; set; }
//public LocalizeExtension()
//{
// Localizer = MauiApplication.Current.Services.GetService<IStringLocalizer<Localizations>>();
//}
public object ProvideValue(IServiceProvider _)
{
return LocalizationResource.Localizer.GetString(Key);
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider);
}
sealed class LocalizationResource
{
private static IStringLocalizer<Localizations> localizer;
public static IStringLocalizer<Localizations> Localizer => localizer ??= MauiApplication.Current.Services.GetService<IStringLocalizer<Localizations>>();
public static string GetText(string key)
{
return Localizer.GetString(key);
}
}

View File

@ -70,8 +70,26 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Localization" Version="7.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="7.0.86" />
<PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="2.88.3" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.4" />
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.5" />
</ItemGroup>
<ItemGroup>
<Compile Update="Localizations.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Localizations.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Localizations.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localizations.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

72
App/Localizations.Designer.cs generated Normal file
View File

@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Blahblah.FlowerStory {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Localizations {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Localizations() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Blahblah.FlowerStory.Localizations", typeof(Localizations).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Unknown.
/// </summary>
internal static string unknown {
get {
return ResourceManager.GetString("unknown", resourceCulture);
}
}
}
}

123
App/Localizations.resx Normal file
View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="unknown" xml:space="preserve">
<value>Unknown</value>
</data>
</root>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="unknown" xml:space="preserve">
<value>未知</value>
</data>
</root>

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:l="clr-namespace:Blahblah.FlowerStory"
xmlns:forms="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
x:Class="Blahblah.FlowerStory.MainPage">
<ScrollView>
@ -15,8 +17,10 @@
HeightRequest="200"
HorizontalOptions="Center" />
<forms:SKCanvasView x:Name="canvasView" PaintSurface="canvasView_PaintSurface"/>
<Label
Text="Hello, World!"
Text="{l:Localize unknown}"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />

View File

@ -46,5 +46,10 @@ namespace Blahblah.FlowerStory
}
});
}
private void canvasView_PaintSurface(object sender, SkiaSharp.Views.Maui.SKPaintSurfaceEventArgs e)
{
}
}
}

View File

@ -28,6 +28,8 @@ namespace Blahblah.FlowerStory
builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<FlowerDatabase>();
builder.Services.AddLocalization();
return builder.Build();
}
}

View File

@ -241,7 +241,7 @@ public abstract partial class BaseController : ControllerBase
/// <param name="token">取消令牌</param>
protected async Task WriteToFile(int uid, int fid, FileResult file, CancellationToken token = default)
{
var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString());
var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString());
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
@ -259,7 +259,7 @@ public abstract partial class BaseController : ControllerBase
/// <returns>返回是否已删除</returns>
protected bool DeleteFile(int uid, int fid, string path)
{
var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString());
var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString());
if (Directory.Exists(directory))
{
path = Path.Combine(directory, path);

View File

@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Blahblah.FlowerStory.Server.Controller;
partial class BaseController
{
private static ErrorResponse CreateErrorResponse(int status, string title, string? detail = null, string? instance = null)
{
return new ErrorResponse
{
Status = status,
Title = title,
Detail = detail,
Instance = instance
};
}
/// <summary>
/// 用户未找到
/// </summary>
protected new NotFoundObjectResult NotFound()
{
return NotFound("User not found");
}
/// <summary>
/// 发生了未找到的错误
/// </summary>
/// <param name="title"></param>
/// <param name="detail"></param>
/// <param name="instance"></param>
protected NotFoundObjectResult NotFound(string title, string? detail = null, string? instance = null)
{
return NotFound(CreateErrorResponse(StatusCodes.Status404NotFound, title, detail, instance));
}
/// <summary>
/// 授权异常
/// </summary>
protected new UnauthorizedObjectResult Unauthorized()
{
return Unauthorized("Unauthorized");
}
/// <summary>
/// 发生了未授权的错误
/// </summary>
/// <param name="title"></param>
/// <param name="detail"></param>
/// <param name="instance"></param>
protected UnauthorizedObjectResult Unauthorized(string title, string? detail = null, string? instance = null)
{
return Unauthorized(CreateErrorResponse(StatusCodes.Status401Unauthorized, title, detail, instance));
}
}
/// <summary>
/// 错误结果
/// </summary>
public record ErrorResponse
{
/// <summary>
/// 错误代码
/// </summary>
[Required]
public required int Status { get; init; }
/// <summary>
/// 错误标题
/// </summary>
[Required]
public required string Title { get; init; }
/// <summary>
/// 错误详细信息
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Detail { get; init; }
/// <summary>
/// 错误示例
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Instance { get; init; }
}

View File

@ -3,7 +3,6 @@ using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.IO;
namespace Blahblah.FlowerStory.Server.Controller;
@ -294,7 +293,7 @@ public class EventApiController : BaseController
var record = database.Records.SingleOrDefault(r => r.Id == update.Id && r.OwnerId == user.Id);
if (record == null)
{
return NotFound(update.Id);
return NotFound($"Event id {update.Id} not found");
}
record.FlowerId = update.FlowerId;
record.EventId = update.EventId;
@ -365,7 +364,7 @@ public class EventApiController : BaseController
var record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id);
if (record == null)
{
return NotFound(id);
return NotFound($"Record id {id} not found");
}
if (photo.Length > 0)
{
@ -458,7 +457,7 @@ public class EventApiController : BaseController
var record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id);
if (record == null)
{
return NotFound(id);
return NotFound($"Record id {id} not found");
}
SaveDatabase();

View File

@ -3,6 +3,7 @@ using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Runtime.InteropServices;
namespace Blahblah.FlowerStory.Server.Controller;
@ -36,7 +37,9 @@ public class FlowerApiController : BaseController
/// to: long?
/// cfrom: decimal?
/// cto: decimal?
/// p: bool?
/// photo: bool?
/// p: int?
/// size: int?
///
/// </remarks>
/// <param name="categoryId">类别 id</param>
@ -46,6 +49,8 @@ public class FlowerApiController : BaseController
/// <param name="costFrom">开销最小值</param>
/// <param name="costTo">开销最大值</param>
/// <param name="includePhoto">是否包含封面图片</param>
/// <param name="page">页数</param>
/// <param name="pageSize">分页大小</param>
/// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response>
@ -65,7 +70,9 @@ public class FlowerApiController : BaseController
[FromQuery(Name = "to")] long? buyTo,
[FromQuery(Name = "cfrom")] decimal? costFrom,
[FromQuery(Name = "cto")] decimal? costTo,
[FromQuery(Name = "p")] bool? includePhoto)
[FromQuery(Name = "photo")] bool? includePhoto,
[FromQuery(Name = "p")] int? page = 0,
[FromQuery(Name = "size")] int? pageSize = 20)
{
var (result, user) = CheckPermission();
if (result != null)
@ -108,6 +115,10 @@ public class FlowerApiController : BaseController
flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo);
}
var size = pageSize ?? 20;
var p = page ?? 0;
flowers = flowers.OrderByDescending(f => f.DateBuyUnixTime).Skip(p * size).Take(size);
if (includePhoto == true)
{
foreach (var f in flowers)
@ -115,12 +126,81 @@ public class FlowerApiController : BaseController
f.Photos = database.Photos.Where(p =>
database.Records.Any(r =>
r.FlowerId == f.Id && r.EventId == EventCover && r.Id == p.RecordId)).ToList();
foreach (var photo in f.Photos)
{
photo.Url = $"{ImageController.BaseUrl}/photo/flower/{f.Id}/{photo.Path}";
}
}
}
return Ok(flowers.ToArray());
}
/// <summary>
/// 查询用户的花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/flower/get
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
/// photo: bool?
///
/// </remarks>
/// <param name="id">花草唯一 id</param>
/// <param name="includePhoto">是否包含封面图片</param>
/// <returns>会话有效则返回查询到的花草对象</returns>
/// <response code="200">返回查询到的花草对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到花草</response>
[Route("get", Name = "getFlower")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<FlowerItem> GetFlower(
[FromQuery][Required] int id,
[FromQuery(Name = "photo")] bool? includePhoto)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
SaveDatabase();
var item = database.Flowers.Find(id);
if (item == null)
{
return NotFound($"Flower id {id} not found");
}
if (includePhoto == true)
{
item.Photos = database.Photos.Where(p =>
database.Records.Any(r =>
r.FlowerId == item.Id && r.EventId == EventCover && r.Id == p.RecordId)).ToList();
foreach (var photo in item.Photos)
{
photo.Url = $"{ImageController.BaseUrl}/photo/flower/{item.Id}/{photo.Path}";
}
}
return Ok(item);
}
/// <summary>
/// 移除用户的花草
/// </summary>
@ -221,13 +301,15 @@ public class FlowerApiController : BaseController
///
/// POST /api/flower/add
/// Authorization: authorization id
/// {
/// "categoryId": 0,
/// "name": "玛格丽特",
/// "dateBuy": 1684919954743,
/// "cost": 5.00,
/// "purchase": "花鸟市场"
/// }
///
/// 参数:
///
/// categoryId: 0
/// name: "玛格丽特"
/// dateBuy: 1684919954743
/// cost: 5.00
/// purchase: "花鸟市场"
/// cover: &lt;photo&gt;
///
/// </remarks>
/// <param name="flower">花草参数</param>
@ -236,14 +318,17 @@ public class FlowerApiController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[Route("add", Name = "addFlower")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<FlowerItem> AddFlower([FromBody] FlowerParameter flower)
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
public async Task<ActionResult<FlowerItem>> AddFlower([FromForm] FlowerParameter flower)
{
var (result, user) = CheckPermission();
if (result != null)
@ -267,6 +352,57 @@ public class FlowerApiController : BaseController
database.Flowers.Add(item);
SaveDatabase();
if (flower.Cover?.Length > 0)
{
var file = WrapFormFile(flower.Cover);
if (file == null)
{
return BadRequest();
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var record = database.Records.SingleOrDefault(r => r.FlowerId == item.Id && r.EventId == EventCover);
if (record == null)
{
record = new RecordItem
{
OwnerId = user.Id,
FlowerId = item.Id,
EventId = EventCover,
DateUnixTime = now,
ByUserId = user.Id,
ByUserName = user.Name
//Memo = ""
};
database.Records.Add(record);
}
SaveDatabase();
try
{
await ExecuteTransaction(async token =>
{
var cover = new PhotoItem
{
FlowerId = item.Id,
RecordId = record.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
AddPhotoItem(cover);
await WriteToFile(user.Id, item.Id, file, token);
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/flower/add");
// TODO: Logger
}
}
return Ok(item);
}
@ -278,14 +414,16 @@ public class FlowerApiController : BaseController
///
/// PUT /api/flower/update
/// Authorization: authorization id
/// {
/// "id": 0,
/// "categoryId": 1,
/// "name": "姬小菊",
/// "dateBuy": 1684935276117,
/// "cost": 15.00,
/// "purchase": null
/// }
///
/// 参数:
///
/// id: 0
/// categoryId: 1
/// name: "姬小菊"
/// dateBuy: 1684935276117
/// cost: 15.40
/// purchase: null
/// cover: &lt;photo&gt;
///
/// </remarks>
/// <param name="update">修改参数</param>
@ -294,14 +432,17 @@ public class FlowerApiController : BaseController
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到将修改的花草对象</response>
/// <response code="413">提交正文过大</response>
[Route("update", Name = "updateFlower")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[HttpPut]
[Consumes("application/json")]
public ActionResult<FlowerItem> Update([FromBody] FlowerUpdateParameter update)
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
public async Task<ActionResult<FlowerItem>> Update([FromForm] FlowerUpdateParameter update)
{
var (result, user) = CheckPermission();
if (result != null)
@ -316,14 +457,73 @@ public class FlowerApiController : BaseController
var flower = database.Flowers.SingleOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id);
if (flower == null)
{
return NotFound(update.Id);
return NotFound($"Flower id {update.Id} not found");
}
flower.CategoryId = update.CategoryId;
flower.Name = update.Name;
flower.DateBuyUnixTime = update.DateBuy;
flower.Cost = update.Cost;
flower.Purchase = update.Purchase;
SaveDatabase();
if (update.Cover?.Length > 0)
{
var file = WrapFormFile(update.Cover);
if (file == null)
{
return BadRequest();
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var record = database.Records.SingleOrDefault(r => r.FlowerId == update.Id && r.EventId == EventCover);
if (record == null)
{
record = new RecordItem
{
OwnerId = user.Id,
FlowerId = update.Id,
EventId = EventCover,
DateUnixTime = now,
ByUserId = user.Id,
ByUserName = user.Name
//Memo = ""
};
database.Records.Add(record);
}
else
{
var photo = database.Photos.Where(p => p.RecordId == record.Id).SingleOrDefault();
if (photo != null)
{
database.Photos.Where(p => p.RecordId == record.Id).ExecuteDelete();
DeleteFile(user.Id, update.Id, photo.Path);
}
}
SaveDatabase();
try
{
await ExecuteTransaction(async token =>
{
var cover = new PhotoItem
{
FlowerId = update.Id,
RecordId = record.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
AddPhotoItem(cover);
await WriteToFile(user.Id, update.Id, file, token);
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/flower/update");
// TODO: Logger
}
}
return Ok(user);
}
@ -377,7 +577,7 @@ public class FlowerApiController : BaseController
var flower = database.Flowers.SingleOrDefault(f => f.Id == id && f.OwnerId == user.Id);
if (flower == null)
{
return NotFound(id);
return NotFound($"Flower id {id} not found");
}
if (photo.Length > 0)
{

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
@ -11,29 +12,40 @@ public record FlowerParameter
/// 类别 id
/// </summary>
[Required]
[FromForm(Name = "categoryId")]
public int CategoryId { get; init; }
/// <summary>
/// 花草名称
/// </summary>
[Required]
[FromForm(Name = "name")]
public required string Name { get; init; }
/// <summary>
/// 购买时间
/// </summary>
[Required]
[FromForm(Name = "dateBuy")]
public long DateBuy { get; init; }
/// <summary>
/// 购买花费
/// </summary>
[FromForm(Name = "cost")]
public decimal? Cost { get; init; }
/// <summary>
/// 购买渠道
/// </summary>
[FromForm(Name = "purchase")]
public string? Purchase { get; init; }
/// <summary>
/// 花草封面
/// </summary>
[FromForm(Name = "cover")]
public IFormFile? Cover { get; init; }
}
/// <summary>
@ -45,5 +57,6 @@ public record FlowerUpdateParameter : FlowerParameter
/// 花草唯一 id
/// </summary>
[Required]
[FromForm(Name = "id")]
public int Id { get; set; }
}

View File

@ -11,6 +11,9 @@ namespace Blahblah.FlowerStory.Server.Controller;
[Route("photo")]
public class ImageController : BaseController
{
/// <inheritdoc/>
public const string BaseUrl = "https://flower.tsanie.org";
/// <inheritdoc/>
public ImageController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
@ -61,7 +64,6 @@ public class ImageController : BaseController
/// 请求示例:
///
/// GET /photo/flower/{fid}/{name}
/// Authorization: authorization id
///
/// </remarks>
/// <returns>认证通过则显示花草照片</returns>
@ -75,17 +77,28 @@ public class ImageController : BaseController
[HttpGet]
public async Task<ActionResult> GetFlowerPhoto([Required] int fid, [Required] string name)
{
var (result, token) = CheckToken();
if (result != null)
{
return result;
}
if (token == null)
{
return Unauthorized();
}
//var (result, token) = CheckToken();
//if (result != null)
//{
// return result;
//}
//if (token == null)
//{
// return Unauthorized();
//}
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", token.UserId.ToString(), fid.ToString(), name);
#if !DEBUG
var referrer = Request.Headers.Referer.ToString();
if (string.IsNullOrEmpty(referrer))
{
return BadRequest();
}
if (!referrer.StartsWith(BaseUrl))
{
return Forbid();
}
#endif
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString(), name);
if (System.IO.File.Exists(path))
{
var data = await System.IO.File.ReadAllBytesAsync(path);

View File

@ -6,7 +6,7 @@ using System.Text;
namespace Blahblah.FlowerStory.Server.Controller;
/// <inheritdoc/>
[Route("apidoc")]
[Route("")]
public class SwaggerController : ControllerBase
{
private readonly SwaggerGenerator generator;
@ -18,7 +18,38 @@ public class SwaggerController : ControllerBase
}
/// <inheritdoc/>
[Route("get/{version}")]
[Route("")]
[Produces("text/html")]
[HttpGet]
public ActionResult Get()
{
return Content($@"<!DOCTYPE html>
<html>
<head>
<title>Redoc</title>
<!-- needed for adaptive design -->
<meta charset=""utf-8""/>
<meta name=""viewport"" content=""width=device-width, initial-scale=1"">
<!--
Redoc doesn't change outer page styles
-->
<style>
body {{
margin: 0;
padding: 0;
}}
</style>
</head>
<body>
<redoc spec-url=""/swagger/{Program.Version}/swagger.json""></redoc>
<script src=""/js/redoc.standalone.js""> </script>
</body>
</html>", "text/html");
}
/// <inheritdoc/>
[Route("apidoc/{version}")]
[Produces("text/html")]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]

View File

@ -37,10 +37,13 @@ public partial class UserApiController : BaseController
/// <response code="204">返回自定义认证头</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到用户</response>
/// <response code="500">服务器错误</response>
[Route("auth", Name = "authenticate")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("application/json")]
public ActionResult Authenticate([FromBody] LoginParamter login)

View File

@ -81,4 +81,7 @@ public class PhotoItem
[NotMapped]
[JsonIgnore]
public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime);
[NotMapped]
public string Url { get; set; }
}

View File

@ -11,7 +11,7 @@ public class Program
/// <inheritdoc/>
public const string ProjectName = "Flower Story";
/// <inheritdoc/>
public const string Version = "0.3.529";
public const string Version = "0.4.626";
/// <inheritdoc/>
public static void Main(string[] args)
@ -72,6 +72,7 @@ public class Program
}
app.UseAuthorization();
app.UseStaticFiles();
app.MapControllers();
app.Run();
@ -106,4 +107,4 @@ public class SwaggerHttpHeaderOperation : IOperationFilter
break;
}
}
}
}

View File

@ -13,7 +13,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"launchUrl": "#tag/UserApi",
"applicationUrl": "http://localhost:5247",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"

File diff suppressed because one or more lines are too long