sync code

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

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