using Blahblah.FlowerStory.Server.Data; using Blahblah.FlowerStory.Server.Data.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using System.ComponentModel.DataAnnotations; using System.Runtime.InteropServices; namespace Blahblah.FlowerStory.Server.Controller; /// /// 花草相关 API 服务 /// [ApiController] [Produces("application/json")] [Route("api/flower")] public class FlowerApiController : BaseController { /// public FlowerApiController(FlowerDatabase database, ILogger? logger = null) : base(database, logger) { } /// /// 获取用户名下所有符合条件的花草 /// /// /// 请求示例: /// /// GET /api/flower/query /// Authorization: authorization id /// /// 参数: /// /// cid: int? /// key: string? /// from: long? /// to: long? /// cfrom: decimal? /// cto: decimal? /// photo: bool? /// lon: double? /// lat: double? /// distance: int? /// p: int? /// size: int? /// /// /// 类别 id /// 查询关键字 /// 起始购买日期 /// 结束购买日期 /// 开销最小值 /// 开销最大值 /// 是否包含封面图片 /// 纬度 /// 经度 /// 距离(米) /// 页数 /// 分页大小 /// 会话有效则返回符合条件的花草集 /// 返回符合条件的花草集 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户 [Route("query", Name = "queryFlowers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public ActionResult GetFlowers( [FromQuery(Name = "cid")] int? categoryId, [FromQuery] string? key, [FromQuery(Name = "from")] long? buyFrom, [FromQuery(Name = "to")] long? buyTo, [FromQuery(Name = "cfrom")] decimal? costFrom, [FromQuery(Name = "cto")] decimal? costTo, [FromQuery(Name = "photo")] bool? includePhoto, [FromQuery(Name = "lat")] double? latitude, [FromQuery(Name = "lon")] double? longitude, [FromQuery] int? distance, [FromQuery(Name = "p")] int? page = 0, [FromQuery(Name = "size")] int? pageSize = 20) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } SaveDatabase(); IEnumerable flowers = database.Flowers.Where(f => f.OwnerId == user.Id); if (categoryId != null) { flowers = flowers.Where(f => f.CategoryId == categoryId); } if (key != null) { flowers = flowers.Where(f => f.Name.ToLower().Contains(key.ToLower()) || f.Purchase != null && f.Purchase.ToLower().Contains(key.ToLower())); } if (buyFrom != null) { flowers = flowers.Where(f => f.DateBuyUnixTime >= buyFrom); } if (buyTo != null) { flowers = flowers.Where(f => f.DateBuyUnixTime <= buyTo); } if (costFrom != null) { flowers = flowers.Where(f => f.Cost != null && f.Cost >= costFrom); } if (costTo != null) { flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo); } if (distance != null && latitude != null && longitude != null) { flowers = flowers.Where(f => f.Latitude != null && f.Longitude != null) .AsEnumerable() .Where(f => { var d = GetDistance(latitude.Value, longitude.Value, f.Latitude ?? 0, f.Longitude ?? 0); f.Distance = (int)d; return d <= distance; }); } int count = flowers.Count(); 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) { 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 = $"photo/flower/{f.Id}/{photo.Path}"; } } } return Ok(new FlowerResult { Flowers = flowers.ToArray(), Count = count }); } /// /// 查询用户的花草 /// /// /// 请求示例: /// /// GET /api/flower/get /// Authorization: authorization id /// /// 参数: /// /// id: int /// photo: bool? /// /// /// 花草唯一 id /// 是否包含封面图片 /// 会话有效则返回查询到的花草对象 /// 返回查询到的花草对象 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户或者未找到花草 [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 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 = $"photo/flower/{item.Id}/{photo.Path}"; } } return Ok(item); } /// /// 移除用户的花草 /// /// /// 请求示例: /// /// DELETE /api/flower/remove /// Authorization: authorization id /// /// 参数: /// /// id: int /// /// /// 花草唯一 id /// 会话有效则返回操作影响的数据库行数 /// 返回操作影响的数据库行数 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户 [Route("remove", Name = "removeFlower")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [HttpDelete] public ActionResult RemoveFlower([FromQuery][Required] int id) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } //database.Records.Where(r => r.OwnerId == user.Id && r.FlowerId == id).ExecuteDelete(); var count = database.Flowers.Where(f => f.OwnerId == user.Id && f.Id == id).ExecuteDelete(); SaveDatabase(); return Ok(count); } /// /// 批量移除用户的花草 /// /// /// 请求示例: /// /// POST /api/flower/remove /// Authorization: authorization id /// [ /// 2, 4, 5, 11 /// ] /// /// /// 要移除的花草唯一 id 的数组 /// 会话有效则返回操作影响的数据库行数 /// 返回操作影响的数据库行数 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户 [Route("removeany", Name = "removeFlowers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [HttpPost] [Consumes("application/json")] public ActionResult RemoveFlower([FromBody] int[] ids) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } //database.Records.Where(r => r.OwnerId == user.Id && ids.Contains(r.FlowerId)).ExecuteDelete(); var count = database.Flowers.Where(f => f.OwnerId == user.Id && ids.Contains(f.Id)).ExecuteDelete(); SaveDatabase(); return Ok(count); } /// /// 用户添加花草 /// /// /// 请求示例: /// /// POST /api/flower/add /// Authorization: authorization id /// /// 参数: /// /// categoryId: 0 /// name: "玛格丽特" /// dateBuy: 1684919954743 /// cost: 5.00 /// purchase: "花鸟市场" /// cover: <photo> /// /// /// 花草参数 /// 添加成功则返回已添加的花草对象 /// 返回已添加的花草对象 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户 /// 提交正文过大 [Route("add", Name = "addFlower")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] [HttpPost] [Consumes("multipart/form-data")] [RequestSizeLimit(5 * 1024 * 1024)] public async Task> AddFlower([FromForm] FlowerParameter flower) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } var item = new FlowerItem { OwnerId = user.Id, CategoryId = flower.CategoryId, Name = flower.Name, DateBuyUnixTime = flower.DateBuy, Cost = flower.Cost, Purchase = flower.Purchase }; 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(item.Id, file, token); }); } catch (Exception ex) { return Problem(ex.ToString(), "api/flower/add"); // TODO: Logger } } return Ok(item); } /// /// 修改花草 /// /// /// 请求示例: /// /// PUT /api/flower/update /// Authorization: authorization id /// /// 参数: /// /// id: 0 /// categoryId: 1 /// name: "姬小菊" /// dateBuy: 1684935276117 /// cost: 15.40 /// purchase: null /// cover: <photo> /// /// /// 修改参数 /// 修改成功则返回已修改的花草对象 /// 返回已修改的花草对象 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户或者未找到将修改的花草对象 /// 提交正文过大 [Route("update", Name = "updateFlower")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] [HttpPut] [Consumes("multipart/form-data")] [RequestSizeLimit(5 * 1024 * 1024)] public async Task> Update([FromForm] FlowerUpdateParameter update) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } var flower = database.Flowers.SingleOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id); if (flower == null) { 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; 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 photos = database.Photos.Where(p => p.RecordId == record.Id).ToList(); if (photos.Count > 0) { database.Photos.Where(p => p.RecordId == record.Id).ExecuteDelete(); foreach (var photo in photos) { DeleteFile(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(update.Id, file, token); }); } catch (Exception ex) { return Problem(ex.ToString(), "api/flower/update"); // TODO: Logger } } else { SaveDatabase(); } return Ok(flower); } /// /// 添加花草封面 /// /// /// 请求示例: /// /// POST /api/flower/add_cover /// Authorization: authorization id /// /// 参数: /// /// id: int /// photo: IFormFile /// /// /// 花草唯一 id /// 封面图片 /// 修改成功则返回 HTTP 204 /// 修改成功 /// 照片格式非法 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户或者关联的花草 /// 提交正文过大 [Route("add_cover", Name = "addFlowerCover")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] [HttpPost] [Consumes("multipart/form-data")] [RequestSizeLimit(5 * 1024 * 1024)] public async Task UploadCovers([Required][FromQuery] int id, [Required] IFormFile photo) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } var flower = database.Flowers.SingleOrDefault(f => f.Id == id && f.OwnerId == user.Id); if (flower == null) { return NotFound($"Flower id {id} not found"); } if (photo.Length > 0) { var file = WrapFormFile(photo); if (file == null) { return BadRequest(); } var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var record = database.Records.SingleOrDefault(r => r.FlowerId == id && r.EventId == EventCover); if (record == null) { record = new RecordItem { OwnerId = user.Id, FlowerId = 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 = id, RecordId = record.Id, FileType = file.FileType, FileName = file.Filename, Path = file.Path, DateUploadUnixTime = now }; AddPhotoItem(cover); await WriteToFile(id, file, token); }); } catch (Exception ex) { return Problem(ex.ToString(), "api/flower/add_cover"); // TODO: Logger } } return NoContent(); } /// /// 获取花草特定类型事件的照片列表 /// /// /// 请求示例: /// /// GET /api/flower/photos /// Authorization: authorization id /// /// 参数: /// /// id: int /// eid: int? /// /// /// 花草唯一 id /// 事件类型 id, 0 为封面 /// 验证通过则返回花草特定类型事件的照片列表 /// 返回花草特定类型事件的照片列表 /// 未找到登录会话或已过期 /// 用户已禁用 /// 未找到关联用户或者未找到花草对象 [Route("photos", Name = "getFlowerPhotos")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [HttpGet] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public ActionResult GetCovers([Required][FromQuery] int id, [FromQuery(Name = "eid")] int? eventId = 0) { var (result, user) = CheckPermission(); if (result != null) { return result; } if (user == null) { return NotFound(); } SaveDatabase(); var photos = database.Photos.Where(p => database.Records.Any(r => r.FlowerId == id && r.EventId == eventId && r.OwnerId == user.Id)); return Ok(photos); } }