add some APIs

This commit is contained in:
2023-07-13 12:10:30 +08:00
parent 32464cb494
commit 8b759c94ec
34 changed files with 1211 additions and 1889 deletions

View File

@ -0,0 +1,59 @@
using Blahblah.FlowerStory.Server.Data;
using Microsoft.AspNetCore.Mvc;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 基础 API 服务
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("api")]
public partial class ApiController : BaseController
{
/// <inheritdoc/>
public ApiController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
}
/// <summary>
/// 获取版本号
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/version
///
/// </remarks>
/// <returns>版本号</returns>
/// <response code="200">返回版本号</response>
[Route("version", Name = "getVersion")]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<string> GetApiVersion()
{
return Ok(Program.Version);
}
/// <summary>
/// 获取常量字典定义
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/consts?{ver}
///
/// </remarks>
/// <returns>字典集</returns>
/// <response code="200">返回常量字典集</response>
[Route("consts", Name = "getConsts")]
[HttpGet]
public ActionResult<DefinitionResult> GetDefinitions()
{
return Ok(new DefinitionResult
{
Categories = Constants.Categories,
Events = Constants.Events,
});
}
}

View File

@ -0,0 +1,17 @@
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 字典、定义
/// </summary>
public record DefinitionResult
{
/// <summary>
/// 花草分类
/// </summary>
public required Dictionary<int, NamedItem> Categories { get; init; }
/// <summary>
/// 事件
/// </summary>
public required Dictionary<int, Event> Events { get; init; }
}

View File

@ -15,6 +15,17 @@ public abstract partial class BaseController : ControllerBase
{
private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden.";
/// <summary>
/// 临时 id
/// </summary>
protected const int TempId = -1;
private static string? uploadsDirectory;
/// <summary>
/// 文件上传路径
/// </summary>
protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads");
/// <summary>
/// 支持的图片文件签名
/// </summary>
@ -195,43 +206,52 @@ public abstract partial class BaseController : ControllerBase
/// <returns>文件结果对象</returns>
protected static FileResult? WrapFormFile(IFormFile file)
{
if (file == null)
if (file?.Length > 0)
{
return null;
}
using var stream = file.OpenReadStream();
using var stream = file.OpenReadStream();
// check header
var headers = new byte[PhotoSignatures.Max(s => s.Length)];
int count = stream.Read(headers, 0, headers.Length);
if (PhotoSignatures.Any(s => headers.Take(s.Length).SequenceEqual(s)))
{
using var ms = new MemoryStream();
ms.Write(headers, 0, count);
// reading
const int size = 16384;
var buffer = new byte[size];
while ((count = stream.Read(buffer, 0, size)) > 0)
// check header
var headers = new byte[PhotoSignatures.Max(s => s.Length)];
int count = stream.Read(headers, 0, headers.Length);
if (PhotoSignatures.Any(s => headers.Take(s.Length).SequenceEqual(s)))
{
ms.Write(buffer, 0, count);
using var ms = new MemoryStream();
ms.Write(headers, 0, count);
// reading
const int size = 16384;
var buffer = new byte[size];
while ((count = stream.Read(buffer, 0, size)) > 0)
{
ms.Write(buffer, 0, count);
}
var data = ms.ToArray();
var name = file.FileName;
var ext = Path.GetExtension(name);
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
return new FileResult
{
Filename = name,
FileType = ext,
Path = path,
Content = data
};
}
var data = ms.ToArray();
var name = file.FileName;
var ext = Path.GetExtension(name);
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
return new FileResult
{
Filename = name,
FileType = ext,
Path = path,
Content = data
};
}
return null;
}
private static string GetFlowerDirectory(int fid, bool create = false)
{
var directory = Path.Combine(UploadsDirectory, fid.ToString());
if (create && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
return directory;
}
/// <summary>
/// 写入文件到用户的花草目录中
/// </summary>
@ -240,11 +260,7 @@ public abstract partial class BaseController : ControllerBase
/// <param name="token">取消令牌</param>
protected static async Task WriteToFile(int fid, FileResult file, CancellationToken token = default)
{
var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString());
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var directory = GetFlowerDirectory(fid, true);
var path = Path.Combine(directory, file.Path);
await System.IO.File.WriteAllBytesAsync(path, file.Content, token);
}
@ -257,7 +273,7 @@ public abstract partial class BaseController : ControllerBase
/// <returns>返回是否已删除</returns>
protected static bool DeleteFile(int fid, string path)
{
var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString());
var directory = GetFlowerDirectory(fid);
if (Directory.Exists(directory))
{
path = Path.Combine(directory, path);
@ -270,6 +286,30 @@ public abstract partial class BaseController : ControllerBase
return false;
}
/// <summary>
/// 移动临时照片到花草目录
/// </summary>
/// <param name="fid">花草唯一 id</param>
/// <param name="path">文件路径</param>
protected static void MoveTempFileToFlower(int fid, string path)
{
var directory = GetFlowerDirectory(TempId);
if (Directory.Exists(directory))
{
var file = Path.Combine(directory, path);
if (System.IO.File.Exists(file))
{
directory = GetFlowerDirectory(fid, true);
var to = Path.Combine(directory, path);
if (System.IO.File.Exists(to))
{
System.IO.File.Move(to, $"{to}.bak");
}
System.IO.File.Move(file, to);
}
}
}
private const double EarthRadius = 6378137;
private static double Radius(double degree)

View File

@ -52,6 +52,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<RecordItem[]> GetRecords(
@ -126,6 +127,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpDelete]
public ActionResult<int> RemoveEvent([FromQuery][Required] int id)
{
@ -170,6 +172,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("application/json")]
public ActionResult<int> RemoveEvents([FromBody] int[] ids)
@ -199,12 +202,16 @@ public class EventApiController : BaseController
///
/// POST /api/event/add
/// Authorization: authorization id
/// {
/// "flowerId": 1,
/// "eventId": 4, // 浇水
/// "byUser": "朋友",
/// "memo": "快干死了"
/// }
///
/// 参数:
///
/// flowerId: 1
/// eventId": 4 // 浇水
/// byUser: "朋友"
/// memo: "快干死了"
/// lon: 29.5462794
/// lat: 106.5380034
/// photos: &lt;photo&gt;[]
///
/// </remarks>
/// <param name="event">事件参数</param>
@ -218,9 +225,12 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("application/json")]
public ActionResult<RecordItem> AddEvent([FromBody] EventParameter @event)
[Consumes("multipart/form-data")]
// 5 photos
[RequestSizeLimit(75 * 1024 * 1024)]
public async Task<ActionResult<RecordItem>> AddEvent([FromForm] EventParameter @event)
{
var (result, user) = CheckPermission();
if (result != null)
@ -232,19 +242,58 @@ public class EventApiController : BaseController
return NotFound();
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var item = new RecordItem
{
OwnerId = user.Id,
FlowerId = @event.FlowerId,
EventId = @event.EventId,
DateUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
EventId = @event.CategoryId,
DateUnixTime = now,
ByUserId = @event.ByUser == null ? user.Id : null,
ByUserName = @event.ByUser,
Memo = @event.Memo
Memo = @event.Memo,
Latitude = @event.Latitude,
Longitude = @event.Longitude
};
database.Records.Add(item);
SaveDatabase();
if (@event.Photos?.Length > 0)
{
try
{
await ExecuteTransaction(async token =>
{
foreach (var photo in @event.Photos)
{
var file = WrapFormFile(photo);
if (file == null)
{
continue;
}
var p = new PhotoItem
{
OwnerId = user.Id,
FlowerId = @event.FlowerId,
RecordId = item.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
AddPhotoItem(p);
await WriteToFile(@event.FlowerId, file, token);
}
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/event/add");
// TODO: Logger
}
}
return Ok(item);
}
@ -256,13 +305,17 @@ public class EventApiController : BaseController
///
/// PUT /api/event/update
/// Authorization: authorization id
/// {
/// "id": 1,
/// "flowerId": 1,
/// "eventId": 5, // 施肥
/// "byUser": null,
/// "memo": "花多多1号"
/// }
///
/// 参数:
///
/// id: 1
/// flowerId: 1
/// eventId": 5 // 施肥
/// byUser: null
/// memo: "花多多1号"
/// lon: 29.5462794
/// lat: 106.5380034
/// photos: &lt;photo&gt;[]
///
/// </remarks>
/// <param name="update">修改参数</param>
@ -276,9 +329,12 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPut]
[Consumes("application/json")]
public ActionResult<RecordItem> Update([FromBody] EventUpdateParameter update)
[Consumes("multipart/form-data")]
// 5 photos
[RequestSizeLimit(75 * 1024 * 1024)]
public async Task<ActionResult<RecordItem>> Update([FromForm] EventUpdateParameter update)
{
var (result, user) = CheckPermission();
if (result != null)
@ -295,8 +351,10 @@ public class EventApiController : BaseController
{
return NotFound($"Event id {update.Id} not found");
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
record.FlowerId = update.FlowerId;
record.EventId = update.EventId;
record.EventId = update.CategoryId;
record.DateUnixTime = now;
if (update.ByUser == null)
{
record.ByUserId = user.Id;
@ -308,7 +366,60 @@ public class EventApiController : BaseController
record.ByUserName = update.ByUser;
}
record.Memo = update.Memo;
SaveDatabase();
record.Latitude = update.Latitude;
record.Longitude = update.Longitude;
if (update.Photos?.Length > 0)
{
try
{
await ExecuteTransaction(async token =>
{
foreach (var photo in update.Photos)
{
var file = WrapFormFile(photo);
if (file == null)
{
continue;
}
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 p in photos)
{
DeleteFile(update.FlowerId, p.Path);
}
}
SaveDatabase();
var cover = new PhotoItem
{
OwnerId = user.Id,
FlowerId = update.FlowerId,
RecordId = record.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
AddPhotoItem(cover);
await WriteToFile(update.FlowerId, file, token);
}
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/event/update");
// TODO: Logger
}
}
else
{
SaveDatabase();
}
return Ok(user);
}
@ -344,6 +455,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
[RequestSizeLimit(15 * 1024 * 1024)]
@ -380,6 +492,7 @@ public class EventApiController : BaseController
{
var p = new PhotoItem
{
OwnerId = user.Id,
FlowerId = record.FlowerId,
RecordId = id,
FileType = file.FileType,
@ -433,6 +546,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
// 5 photos
@ -474,6 +588,7 @@ public class EventApiController : BaseController
var p = new PhotoItem
{
OwnerId = user.Id,
FlowerId = record.FlowerId,
RecordId = id,
FileType = file.FileType,
@ -526,6 +641,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpDelete]
public ActionResult<int> RemoveEventPhoto([FromQuery][Required] int id)
{
@ -539,12 +655,13 @@ public class EventApiController : BaseController
return NotFound();
}
var photo = database.Photos.Where(p => p.Id == id).Include(p => p.Record).SingleOrDefault();
var photo = database.Photos.Find(id);
if (photo == null)
{
return NotFound();
}
if (photo.Record != null && photo.Record.OwnerId != user.Id)
var item = database.Flowers.Find(photo.FlowerId);
if (item == null || item.OwnerId != user.Id)
{
return Unauthorized();
}
@ -553,10 +670,7 @@ public class EventApiController : BaseController
SaveDatabase();
if (photo.Record != null)
{
DeleteFile(photo.Record.FlowerId, photo.Path);
}
DeleteFile(photo.FlowerId ?? TempId, photo.Path);
return NoContent();
}
@ -585,6 +699,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("application/json")]
public ActionResult<int> RemoveEventPhotos([FromBody] int[] ids)
@ -604,17 +719,14 @@ public class EventApiController : BaseController
return Unauthorized();
}
var photos = database.Photos.Where(p => ids.Contains(p.Id)).Include(p => p.Record).ToList();
var photos = database.Photos.Where(p => ids.Contains(p.Id)).ToList();
var count = database.Photos.Where(p => ids.Contains(p.Id)).ExecuteDelete();
SaveDatabase();
foreach (var photo in photos)
{
if (photo.Record != null)
{
DeleteFile(photo.Record.FlowerId, photo.Path);
}
DeleteFile(photo.FlowerId ?? TempId, photo.Path);
}
return Ok(count);
@ -645,6 +757,7 @@ public class EventApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<PhotoItem[]> GetPhotos([Required][FromQuery] int id)

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
@ -11,24 +12,46 @@ public record EventParameter
/// 花草唯一 id
/// </summary>
[Required]
public int FlowerId { get; init; }
[FromForm(Name = "flowerId")]
public required int FlowerId { get; init; }
/// <summary>
/// 事件 id
/// 事件分类 id
/// </summary>
[Required]
public int EventId { get; init; }
[FromForm(Name = "categoryId")]
public required int CategoryId { get; init; }
/// <summary>
/// 操作人姓名
/// </summary>
[Required]
public string? ByUser { get; init; }
[FromForm(Name = "byUser")]
public required string ByUser { get; init; }
/// <summary>
/// 事件备注
/// 备注
/// </summary>
public string? Memo { get; init; }
[FromForm(Name = "memo")]
public string? Memo { get; set; }
/// <summary>
/// 纬度
/// </summary>
[FromForm(Name = "lat")]
public double? Latitude { get; set; }
/// <summary>
/// 经度
/// </summary>
[FromForm(Name = "lon")]
public double? Longitude { get; set; }
/// <summary>
/// 关联的照片
/// </summary>
[FromForm(Name = "photos")]
public IFormFile[]? Photos { get; init; }
}
/// <summary>
@ -40,5 +63,6 @@ public record EventUpdateParameter : EventParameter
/// 事件唯一 id
/// </summary>
[Required]
public int Id { get; set; }
[FromForm(Name = "id")]
public required int Id { get; set; }
}

View File

@ -2,9 +2,7 @@
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;
@ -68,6 +66,7 @@ public class FlowerApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<FlowerResult> GetFlowers(
@ -96,7 +95,88 @@ public class FlowerApiController : BaseController
SaveDatabase();
IEnumerable<FlowerItem> flowers = database.Flowers.Where(f => f.OwnerId == user.Id);
var flowers = GetFlowerResult(user.Id, null, categoryId, key, buyFrom, buyTo, costFrom, costTo, includePhoto, latitude, longitude, distance, page, pageSize);
return Ok(flowers);
}
/// <summary>
/// 获取最新、最近的花草
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/flower/latest
///
/// 参数:
///
/// cid: int?
/// key: string?
/// from: long?
/// to: long?
/// cfrom: decimal?
/// cto: decimal?
/// photo: bool?
/// lon: double?
/// lat: double?
/// distance: int?
/// p: int?
/// size: int?
///
/// </remarks>
/// <param name="categoryId">类别 id</param>
/// <param name="key">查询关键字</param>
/// <param name="buyFrom">起始购买日期</param>
/// <param name="buyTo">结束购买日期</param>
/// <param name="costFrom">开销最小值</param>
/// <param name="costTo">开销最大值</param>
/// <param name="includePhoto">是否包含封面图片</param>
/// <param name="latitude">纬度</param>
/// <param name="longitude">经度</param>
/// <param name="distance">距离(米)</param>
/// <param name="page">页数</param>
/// <param name="pageSize">分页大小</param>
/// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("latest", Name = "queryLatestFlowers")]
[ProducesResponseType(StatusCodes.Status200OK)]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<FlowerResult> GetLatestFlowers(
[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 = GetFlowerResult(null, (int)EventTypes.Share, categoryId, key, buyFrom, buyTo, costFrom, costTo, includePhoto, latitude, longitude, distance, page, pageSize);
return Ok(result);
}
private FlowerResult GetFlowerResult(int? userId, int? eventId, int? categoryId, string? key,
long? buyFrom, long? buyTo, decimal? costFrom, decimal? costTo,
bool? includePhoto, double? latitude, double? longitude, int? distance,
int? page = 0, int? pageSize = 20)
{
IEnumerable<FlowerItem> flowers;
if (userId != null)
{
flowers = database.Flowers.Where(f => f.OwnerId == userId);
}
else
{
flowers = database.Flowers;
}
if (categoryId != null)
{
flowers = flowers.Where(f => f.CategoryId == categoryId);
@ -125,6 +205,11 @@ public class FlowerApiController : BaseController
flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo);
}
if (eventId != null)
{
flowers = flowers.Where(f => database.Records.Any(r => r.FlowerId == f.Id && r.EventId == eventId));
}
if (distance != null && latitude != null && longitude != null)
{
flowers = flowers.Where(f => f.Latitude != null && f.Longitude != null)
@ -157,11 +242,11 @@ public class FlowerApiController : BaseController
}
}
return Ok(new FlowerResult
return new()
{
Flowers = flowers.ToArray(),
Count = count
});
};
}
/// <summary>
@ -191,6 +276,7 @@ public class FlowerApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<FlowerItem> GetFlower(
@ -254,6 +340,7 @@ public class FlowerApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpDelete]
public ActionResult<int> RemoveFlower([FromQuery][Required] int id)
{
@ -299,9 +386,10 @@ public class FlowerApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("application/json")]
public ActionResult<int> RemoveFlower([FromBody] int[] ids)
public ActionResult<int> RemoveFlowers([FromBody] int[] ids)
{
var (result, user) = CheckPermission();
if (result != null)
@ -337,22 +425,29 @@ public class FlowerApiController : BaseController
/// dateBuy: 1684919954743
/// cost: 5.00
/// purchase: "花鸟市场"
/// memo: "备注信息"
/// lon: 29.5462794
/// lat: 106.5380034
/// cid: 1
/// cover: &lt;photo&gt;
///
/// </remarks>
/// <param name="flower">花草参数</param>
/// <returns>添加成功则返回已添加的花草对象</returns>
/// <response code="200">返回已添加的花草对象</response>
/// <response code="400">提交的内容不合法</response>
/// <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.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
@ -375,11 +470,15 @@ public class FlowerApiController : BaseController
Name = flower.Name,
DateBuyUnixTime = flower.DateBuy,
Cost = flower.Cost,
Purchase = flower.Purchase
Purchase = flower.Purchase,
Memo = flower.Memo,
Latitude = flower.Latitude,
Longitude = flower.Longitude
};
database.Flowers.Add(item);
SaveDatabase();
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (flower.Cover?.Length > 0)
{
var file = WrapFormFile(flower.Cover);
@ -388,7 +487,6 @@ public class FlowerApiController : BaseController
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)
{
@ -399,8 +497,10 @@ public class FlowerApiController : BaseController
EventId = EventCover,
DateUnixTime = now,
ByUserId = user.Id,
ByUserName = user.Name
//Memo = ""
ByUserName = user.Name,
//Memo = flower.Memo,
Latitude = flower.Latitude,
Longitude = flower.Longitude
};
database.Records.Add(record);
}
@ -412,6 +512,7 @@ public class FlowerApiController : BaseController
{
var cover = new PhotoItem
{
OwnerId = user.Id,
FlowerId = item.Id,
RecordId = record.Id,
FileType = file.FileType,
@ -426,14 +527,135 @@ public class FlowerApiController : BaseController
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/flower/add");
return Problem(ex.ToString(), "api/flower/add#WriteToFile");
// TODO: Logger
}
}
else if (flower.CoverId is int coverId)
{
var photo = database.Photos.SingleOrDefault(p => p.Id == coverId && p.OwnerId == user.Id);
if (photo != null)
{
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 = flower.Memo,
Latitude = flower.Latitude,
Longitude = flower.Longitude
};
database.Records.Add(record);
SaveDatabase();
}
photo.FlowerId = item.Id;
photo.RecordId = record.Id;
SaveDatabase();
try
{
MoveTempFileToFlower(item.Id, photo.Path);
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/flower/add#MoveTempFileToFlower");
// TODO: Logger
}
}
}
return Ok(item);
}
/// <summary>
/// 用户上传封面
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/flower/cover_upload
/// Authorization: authorization id
///
/// 参数:
///
/// cover: &lt;photo&gt;
///
/// </remarks>
/// <param name="p">封面参数</param>
/// <returns>添加成功则返回封面 id</returns>
/// <response code="200">返回已添加的封面 id</response>
/// <response code="400">提交的内容不合法</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[Route("cover_upload", Name = "uploadCover")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
public async Task<ActionResult<int>> UploadCover([FromForm] CoverParameter p)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
if (p.Cover?.Length > 0)
{
var file = WrapFormFile(p.Cover);
if (file == null)
{
return BadRequest();
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
try
{
await WriteToFile(TempId, file);
var item = new PhotoItem
{
OwnerId = user.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
database.Photos.Add(item);
SaveDatabase();
return Ok(item.Id);
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/flower/cover_upload");
// TODO: Logger
}
}
return BadRequest();
}
/// <summary>
/// 修改花草
/// </summary>
@ -451,22 +673,28 @@ public class FlowerApiController : BaseController
/// dateBuy: 1684935276117
/// cost: 15.40
/// purchase: null
/// memo: "备注"
/// lon: 29.5462794
/// lat: 106.5380034
/// cover: &lt;photo&gt;
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的花草对象</returns>
/// <response code="200">返回已修改的花草对象</response>
/// <response code="400">提交的内容不合法</response>
/// <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.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPut]
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
@ -492,6 +720,9 @@ public class FlowerApiController : BaseController
flower.DateBuyUnixTime = update.DateBuy;
flower.Cost = update.Cost;
flower.Purchase = update.Purchase;
flower.Memo = update.Memo;
flower.Latitude = update.Latitude;
flower.Longitude = update.Longitude;
if (update.Cover?.Length > 0)
{
@ -512,8 +743,10 @@ public class FlowerApiController : BaseController
EventId = EventCover,
DateUnixTime = now,
ByUserId = user.Id,
ByUserName = user.Name
//Memo = ""
ByUserName = user.Name,
//Memo = flower.Memo,
Latitude = flower.Latitude,
Longitude = flower.Longitude
};
database.Records.Add(record);
}
@ -537,6 +770,7 @@ public class FlowerApiController : BaseController
{
var cover = new PhotoItem
{
OwnerId = user.Id,
FlowerId = update.Id,
RecordId = record.Id,
FileType = file.FileType,
@ -575,11 +809,12 @@ public class FlowerApiController : BaseController
/// 参数:
///
/// id: int
/// lon: double?
/// lat: double?
/// photo: IFormFile
///
/// </remarks>
/// <param name="id">花草唯一 id</param>
/// <param name="photo">封面图片</param>
/// <param name="param">封面修改参数</param>
/// <returns>修改成功则返回 HTTP 204</returns>
/// <response code="204">修改成功</response>
/// <response code="400">照片格式非法</response>
@ -594,10 +829,11 @@ public class FlowerApiController : BaseController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
[RequestSizeLimit(5 * 1024 * 1024)]
public async Task<ActionResult> UploadCovers([Required][FromQuery] int id, [Required] IFormFile photo)
public async Task<ActionResult> UploadCovers([FromForm] FlowerCoverParameter param)
{
var (result, user) = CheckPermission();
if (result != null)
@ -609,35 +845,40 @@ public class FlowerApiController : BaseController
return NotFound();
}
var flower = database.Flowers.SingleOrDefault(f => f.Id == id && f.OwnerId == user.Id);
var flower = database.Flowers.SingleOrDefault(f => f.Id == param.Id && f.OwnerId == user.Id);
if (flower == null)
{
return NotFound($"Flower id {id} not found");
return NotFound($"Flower id {param.Id} not found");
}
if (photo.Length > 0)
if (param.Cover?.Length > 0)
{
var file = WrapFormFile(photo);
var file = WrapFormFile(param.Cover);
if (file == null)
{
return BadRequest();
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var record = database.Records.SingleOrDefault(r => r.FlowerId == id && r.EventId == EventCover);
var record = database.Records.SingleOrDefault(r => r.FlowerId == param.Id && r.EventId == EventCover);
if (record == null)
{
record = new RecordItem
{
OwnerId = user.Id,
FlowerId = id,
FlowerId = param.Id,
EventId = EventCover,
DateUnixTime = now,
ByUserId = user.Id,
ByUserName = user.Name
//Memo = ""
ByUserName = user.Name,
//Memo = "",
Latitude = param.Latitude,
Longitude = param.Longitude
};
database.Records.Add(record);
}
flower.Latitude = param.Latitude;
flower.Longitude = param.Longitude;
SaveDatabase();
try
@ -646,7 +887,8 @@ public class FlowerApiController : BaseController
{
var cover = new PhotoItem
{
FlowerId = id,
OwnerId = user.Id,
FlowerId = param.Id,
RecordId = record.Id,
FileType = file.FileType,
FileName = file.Filename,
@ -655,7 +897,7 @@ public class FlowerApiController : BaseController
};
AddPhotoItem(cover);
await WriteToFile(id, file, token);
await WriteToFile(param.Id, file, token);
});
}
catch (Exception ex)
@ -695,6 +937,7 @@ public class FlowerApiController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<PhotoItem[]> GetCovers([Required][FromQuery] int id, [FromQuery(Name = "eid")] int? eventId = 0)

View File

@ -4,17 +4,35 @@ using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 封面参数
/// </summary>
public record CoverParameter
{
/// <summary>
/// 封面
/// </summary>
[FromForm(Name = "cover")]
public IFormFile? Cover { get; init; }
/// <summary>
/// 封面 id
/// </summary>
[FromForm(Name = "cid")]
public int? CoverId { get; init; }
}
/// <summary>
/// 花草参数
/// </summary>
public record FlowerParameter
public record FlowerParameter : CoverParameter
{
/// <summary>
/// 类别 id
/// </summary>
[Required]
[FromForm(Name = "categoryId")]
public int CategoryId { get; init; }
public required int CategoryId { get; init; }
/// <summary>
/// 花草名称
@ -28,7 +46,7 @@ public record FlowerParameter
/// </summary>
[Required]
[FromForm(Name = "dateBuy")]
public long DateBuy { get; init; }
public required long DateBuy { get; init; }
/// <summary>
/// 购买花费
@ -43,10 +61,22 @@ public record FlowerParameter
public string? Purchase { get; init; }
/// <summary>
/// 花草封面
/// 备注
/// </summary>
[FromForm(Name = "cover")]
public IFormFile? Cover { get; init; }
[FromForm(Name = "memo")]
public string? Memo { get; set; }
/// <summary>
/// 纬度
/// </summary>
[FromForm(Name = "lat")]
public double? Latitude { get; set; }
/// <summary>
/// 经度
/// </summary>
[FromForm(Name = "lon")]
public double? Longitude { get; set; }
}
/// <summary>
@ -59,7 +89,32 @@ public record FlowerUpdateParameter : FlowerParameter
/// </summary>
[Required]
[FromForm(Name = "id")]
public int Id { get; init; }
public required int Id { get; init; }
}
/// <summary>
/// 花草封面修改参数
/// </summary>
public record FlowerCoverParameter : CoverParameter
{
/// <summary>
/// 花草唯一 id
/// </summary>
[Required]
[FromForm(Name = "id")]
public required int Id { get; init; }
/// <summary>
/// 纬度
/// </summary>
[FromForm(Name = "lat")]
public double? Latitude { get; set; }
/// <summary>
/// 经度
/// </summary>
[FromForm(Name = "lon")]
public double? Longitude { get; set; }
}
/// <summary>

View File

@ -103,6 +103,44 @@ public partial class UserApiController : BaseController
return NoContent();
}
/// <summary>
/// 判断当前会话是否有效
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/user/validation
/// Authorization: authorization id
///
/// </remarks>
/// <returns>会话有效则返回会话对象</returns>
/// <response code="200">返回会话对象</response>
/// <response code="401">认证失败</response>
[Route("validation", Name = "validation")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<TokenItem> Validation()
{
var (result, token) = CheckToken();
if (result != null)
{
return result;
}
if (token == null)
{
return Unauthorized();
}
// update last active time
SaveDatabase();
return Ok(token);
}
/// <summary>
/// 注销当前登录会话
/// </summary>