From 50e729784861bcfedc0fae7d7bbf8389ad605d08 Mon Sep 17 00:00:00 2001 From: Tsanie Lily Date: Thu, 25 May 2023 21:54:40 +0800 Subject: [PATCH] complete controllers --- .gitignore | 1 + Server/Controller/BaseController.cs | 73 ++- Server/Controller/BaseController.sqlite.cs | 28 +- Server/Controller/EventApiController.cs | 452 ++++++++++++++++++ .../Controller/EventApiController.structs.cs | 44 ++ Server/Controller/EventController.cs | 95 ---- ...erController.cs => FlowerApiController.cs} | 181 ++++++- ...ucts.cs => FlowerApiController.structs.cs} | 4 +- Server/Controller/ImageController.cs | 42 ++ Server/Controller/UserApiController.cs | 410 ++++++++++++++++ ...tructs.cs => UserApiController.structs.cs} | 2 +- Server/Controller/UserController.cs | 407 ---------------- Server/Data/FlowerDatabase.cs | 5 + Server/Data/Model/FlowerItem.cs | 20 +- Server/Data/Model/PhotoItem.cs | 84 ++++ Server/Data/Model/RecordItem.cs | 34 +- .../20230525082941_AddPhotos.Designer.cs | 300 ++++++++++++ Server/Migrations/20230525082941_AddPhotos.cs | 143 ++++++ ...1254_AddPhotosFlowerForeignKey.Designer.cs | 319 ++++++++++++ ...0230525091254_AddPhotosFlowerForeignKey.cs | 50 ++ .../Migrations/FlowerDatabaseModelSnapshot.cs | 129 ++++- 21 files changed, 2257 insertions(+), 566 deletions(-) create mode 100644 Server/Controller/EventApiController.cs create mode 100644 Server/Controller/EventApiController.structs.cs delete mode 100644 Server/Controller/EventController.cs rename Server/Controller/{FlowerController.cs => FlowerApiController.cs} (60%) rename Server/Controller/{FlowerController.structs.cs => FlowerApiController.structs.cs} (97%) create mode 100644 Server/Controller/UserApiController.cs rename Server/Controller/{UserController.structs.cs => UserApiController.structs.cs} (97%) delete mode 100644 Server/Controller/UserController.cs create mode 100644 Server/Data/Model/PhotoItem.cs create mode 100644 Server/Migrations/20230525082941_AddPhotos.Designer.cs create mode 100644 Server/Migrations/20230525082941_AddPhotos.cs create mode 100644 Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.Designer.cs create mode 100644 Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.cs diff --git a/.gitignore b/.gitignore index de0b159..62b24f7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore flower.db +TestCase/ # User-specific files *.rsuser diff --git a/Server/Controller/BaseController.cs b/Server/Controller/BaseController.cs index a9a4f61..bcf696d 100644 --- a/Server/Controller/BaseController.cs +++ b/Server/Controller/BaseController.cs @@ -3,6 +3,7 @@ using Blahblah.FlowerStory.Server.Data.Model; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; +using System.Net; using System.Security.Cryptography; using System.Text; @@ -44,6 +45,10 @@ public abstract partial class BaseController : ControllerBase /// 管理员用户 /// protected const int UserAdmin = 99; + /// + /// 封面事件 + /// + protected const int EventCover = 0; /// /// 数据库对象 @@ -130,6 +135,17 @@ public abstract partial class BaseController : ControllerBase return (Unauthorized(), null); } var user = QueryUserItem(token.UserId); + //var user = database.Users.Where(u => u.Id == token.UserId).Select(u => new UserItem + //{ + // Id = u.Id, + // UserId = u.UserId, + // Level = u.Level, + // RegisterDateUnixTime = u.RegisterDateUnixTime, + // ActiveDateUnixTime = u.ActiveDateUnixTime, + // Name = u.Name, + // Email = u.Email, + // Mobile = u.Mobile + //}).SingleOrDefault(); if (user == null) { logger?.LogWarning("user not found with id {id}", token.UserId); @@ -158,7 +174,7 @@ public abstract partial class BaseController : ControllerBase /// /// 保存数据库变动并输出日志 /// - /// + /// 操作的数据库行数 protected int SaveDatabase() { var count = database.SaveChanges(); @@ -177,7 +193,7 @@ public abstract partial class BaseController : ControllerBase /// 读取文件到 byte 数组 /// /// 来自请求的文件 - /// + /// 文件结果对象 protected FileResult? WrapFormFile(IFormFile file) { if (file == null) @@ -203,16 +219,36 @@ public abstract partial class BaseController : ControllerBase } var data = ms.ToArray(); var name = file.FileName; - name = $"{Path.GetFileNameWithoutExtension(name)}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.{Path.GetExtension(name)}"; + 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; } + + /// + /// 写入文件到用户的花草目录中 + /// + /// 用户唯一 id + /// 花草唯一 id + /// 文件对象 + protected async Task WriteToFile(int uid, int fid, FileResult file) + { + var directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", uid.ToString(), fid.ToString()); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + var path = Path.Combine(directory, file.Path); + await System.IO.File.WriteAllBytesAsync(path, file.Content); + } } /// @@ -223,7 +259,17 @@ public record FileResult /// /// 文件名 /// - public string? Filename { get; init; } + public required string Filename { get; init; } + + /// + /// 文件类型 + /// + public required string FileType { get; init; } + + /// + /// 储存路径 + /// + public required string Path { get; set; } /// /// 文件内容 @@ -231,3 +277,22 @@ public record FileResult [Required] public required byte[] Content { get; init; } } + + +/// +/// 照片参数 +/// +public record PhotoParameter +{ + /// + /// 花草 id + /// + [Required] + public int Id { get; set; } + + /// + /// 封面照片 + /// + [Required] + public required IFormFile Photo { get; set; } +} \ No newline at end of file diff --git a/Server/Controller/BaseController.sqlite.cs b/Server/Controller/BaseController.sqlite.cs index 3ea3b3c..1c6e257 100644 --- a/Server/Controller/BaseController.sqlite.cs +++ b/Server/Controller/BaseController.sqlite.cs @@ -1,5 +1,6 @@ using Blahblah.FlowerStory.Server.Data.Model; using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; namespace Blahblah.FlowerStory.Server.Controller; @@ -14,7 +15,7 @@ partial class BaseController protected UserItem? QueryUserItem(int uid) { return database.Users - .FromSql($"SELECT \"uid\",\"id\",\"\" AS \"password\",\"level\",\"regdate\",\"activedate\",\"name\",\"email\",\"mobile\",NULL AS \"avatar\" FROM [users] WHERE \"uid\" = {uid} LIMIT 1") + .FromSql($"SELECT \"uid\",\"id\",\"\" AS \"password\",\"level\",\"regdate\",\"activedate\",\"name\",\"email\",\"mobile\",NULL AS \"avatar\" FROM \"users\" WHERE \"uid\" = {uid} LIMIT 1") .SingleOrDefault(); } @@ -26,7 +27,7 @@ partial class BaseController protected UserItem? QueryUserItemForAuthentication(string id) { return database.Users - .FromSql($"SELECT \"uid\",\"id\",\"password\",0 AS \"level\",0 AS \"regdate\",\"activedate\",\"\" AS \"name\",NULL AS \"email\",NULL AS \"mobile\",NULL as \"avatar\" FROM [users] WHERE \"id\" = {id} LIMIT 1") + .FromSql($"SELECT \"uid\",\"id\",\"password\",0 AS \"level\",0 AS \"regdate\",\"activedate\",\"\" AS \"name\",NULL AS \"email\",NULL AS \"mobile\",NULL as \"avatar\" FROM \"users\" WHERE \"id\" = {id} LIMIT 1") .SingleOrDefault(); } @@ -49,27 +50,4 @@ partial class BaseController { return database.Database.ExecuteSql($"UPDATE \"users\" SET \"avatar\" = NULL WHERE \"uid\" = {uid}"); } - - /// - /// 移除用户名下的花草 - /// - /// 用户唯一 id - /// 花草唯一 id - /// - protected int RemoveUserFlower(int uid, int fid) - { - return database.Database.ExecuteSql($"DELETE FROM \"flowers\" WHERE \"uid\" = {uid} AND \"fid\" = {fid}"); - } - - /// - /// 批量移除用户名下的花草 - /// - /// 用户唯一 id - /// 花草唯一 id 的数组 - /// - protected int RemoveUserFlowers(int uid, int[] fids) - { - var idfilter = string.Join(", ", fids); - return database.Database.ExecuteSql($"DELETE FROM \"flowers\" WHERE \"uid\" = {uid} AND \"fid\" IN ({idfilter})"); - } } diff --git a/Server/Controller/EventApiController.cs b/Server/Controller/EventApiController.cs new file mode 100644 index 0000000..d4e546c --- /dev/null +++ b/Server/Controller/EventApiController.cs @@ -0,0 +1,452 @@ +using Blahblah.FlowerStory.Server.Data; +using Blahblah.FlowerStory.Server.Data.Model; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace Blahblah.FlowerStory.Server.Controller; + +/// +/// 事件相关 API 服务 +/// +[ApiController] +[Produces("application/json")] +[Route("api/event")] +public class EventApiController : BaseController +{ + /// + public EventApiController(FlowerDatabase database, ILogger? logger = null) : base(database, logger) + { + } + + /// + /// 获取用户相关所有符合条件的事件 + /// + /// + /// 请求示例: + /// + /// GET /api/event/query + /// Authorization: authorization id + /// + /// 参数: + /// + /// eid: int? + /// key: string? + /// from: long? + /// to: long? + /// p: bool? + /// + /// + /// 事件类型 id + /// 查询关键字 + /// 起始日期 + /// 结束日期 + /// 是否包含图片 + /// 会话有效则返回符合条件的花草集 + /// 返回符合条件的花草集 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + [Route("query", Name = "queryEvents")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpGet] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public ActionResult GetRecords( + [FromQuery(Name = "eid")] int? eventId, + [FromQuery] string? key, + [FromQuery] long? from, + [FromQuery] long? to, + [FromQuery(Name = "p")] bool? includePhoto) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + SaveDatabase(); + + var records = database.Records.Where(r => r.OwnerId == user.Id); + if (eventId != null) + { + records = records.Where(r => r.EventId == eventId); + } + if (key != null) + { + records = records.Where(r => + r.ByUserName != null && r.ByUserName.ToLower().Contains(key.ToLower()) || + r.Memo != null && r.Memo.ToLower().Contains(key.ToLower())); + } + if (from != null) + { + records = records.Where(r => r.DateUnixTime >= from); + } + if (to != null) + { + records = records.Where(r => r.DateUnixTime <= to); + } + + if (includePhoto == true) + { + foreach (var r in records) + { + r.Photos = database.Photos.Where(p => p.RecordId == r.Id).ToList(); + } + } + + return Ok(records.ToArray()); + } + + /// + /// 移除用户的事件 + /// + /// + /// 请求示例: + /// + /// DELETE /api/event/remove + /// Authorization: authorization id + /// + /// 参数: + /// + /// id: int + /// + /// + /// 事件唯一 id + /// 会话有效则返回操作影响的数据库行数 + /// 返回操作影响的数据库行数 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + [Route("remove", Name = "removeEvent")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpDelete] + public ActionResult RemoveEvent([FromQuery][Required] int id) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + var count = database.Records.Where(r => r.OwnerId == user.Id && r.Id == id).ExecuteDelete(); + + SaveDatabase(); + + return Ok(count); + } + + /// + /// 批量移除用户的事件 + /// + /// + /// 请求示例: + /// + /// POST /api/event/remove + /// Authorization: authorization id + /// [ + /// 2, 4, 5, 11 + /// ] + /// + /// + /// 要移除的事件唯一 id 的数组 + /// 会话有效则返回操作影响的数据库行数 + /// 返回操作影响的数据库行数 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + [Route("removeany", Name = "removeEvents")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpPost] + [Consumes("application/json")] + public ActionResult RemoveEvents([FromBody] int[] ids) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + var count = database.Records.Where(r => r.OwnerId == user.Id && ids.Contains(r.Id)).ExecuteDelete(); + + SaveDatabase(); + + return Ok(count); + } + + /// + /// 用户添加事件 + /// + /// + /// 请求示例: + /// + /// POST /api/event/add + /// Authorization: authorization id + /// { + /// "flowerId": 1, + /// "eventId": 4, // 浇水 + /// "byUser": "朋友", + /// "memo": "快干死了" + /// } + /// + /// + /// 事件参数 + /// 添加成功则返回已添加的事件对象 + /// 返回已添加的事件对象 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + [Route("add", Name = "addEvent")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpPost] + [Consumes("application/json")] + public ActionResult AddEvent([FromBody] EventParameter @event) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + var item = new RecordItem + { + OwnerId = user.Id, + FlowerId = @event.FlowerId, + EventId = @event.EventId, + DateUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + ByUserId = @event.ByUser == null ? user.Id : null, + ByUserName = @event.ByUser, + Memo = @event.Memo + }; + database.Records.Add(item); + SaveDatabase(); + + return Ok(item); + } + + /// + /// 修改事件 + /// + /// + /// 请求示例: + /// + /// PUT /api/event/update + /// Authorization: authorization id + /// { + /// "id": 1, + /// "flowerId": 1, + /// "eventId": 5, // 施肥 + /// "byUser": null, + /// "memo": "花多多1号" + /// } + /// + /// + /// 修改参数 + /// 修改成功则返回已修改的事件对象 + /// 返回已修改的事件对象 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户或者未找到将修改的事件对象 + [Route("update", Name = "updateEvent")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpPut] + [Consumes("application/json")] + public ActionResult Update([FromBody] EventUpdateParameter update) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + var record = database.Records.SingleOrDefault(r => r.Id == update.Id && r.OwnerId == user.Id); + if (record == null) + { + SaveDatabase(); + return NotFound(update.Id); + } + record.FlowerId = update.FlowerId; + record.EventId = update.EventId; + if (update.ByUser == null) + { + record.ByUserId = user.Id; + record.ByUserName = null; + } + else + { + record.ByUserId = null; + record.ByUserName = update.ByUser; + } + record.Memo = update.Memo; + SaveDatabase(); + + return Ok(user); + } + + /// + /// 添加事件关联照片 + /// + /// + /// 请求示例: + /// + /// POST /api/event/add_photo + /// Authorization: authorization id + /// + /// 参数: + /// + /// id: int + /// photo: IFormFile + /// + /// + /// 事件唯一 id + /// 图片 + /// 修改成功则返回 HTTP 204 + /// 修改成功 + /// 照片格式非法 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户或者关联的事件 + /// 提交正文过大 + [Route("add_photo", Name = "addEventPhoto")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [HttpPost] + [Consumes("multipart/form-data")] + [RequestSizeLimit(15 * 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 record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id); + if (record == null) + { + SaveDatabase(); + return NotFound(id); + } + if (photo.Length > 0) + { + var file = WrapFormFile(photo); + if (file == null) + { + SaveDatabase(); + return BadRequest(); + } + + var p = new PhotoItem + { + FlowerId = record.FlowerId, + RecordId = id, + FileType = file.FileType, + FileName = file.Filename, + Path = file.Path, + DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + database.Photos.Add(p); + + try + { + await WriteToFile(user.Id, id, file); + } + catch (Exception ex) + { + SaveDatabase(); + return Problem(ex.ToString(), "api/event/add_photo"); + // TODO: Logger + } + } + SaveDatabase(); + + return NoContent(); + } + + /// + /// 获取事件关联的照片列表 + /// + /// + /// 请求示例: + /// + /// GET /api/event/photos + /// Authorization: authorization id + /// + /// 参数: + /// + /// id: int + /// + /// + /// 事件唯一 id + /// 验证通过则返回事件关联的照片列表 + /// 返回事件关联的照片列表 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户或者未找到事件对象 + [Route("photos", Name = "getEventPhotos")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpGet] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public ActionResult GetPhotos([Required][FromQuery] int id) + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + SaveDatabase(); + + var photos = database.Photos.Where(p => p.RecordId == id && database.Records.Any(r => r.Id == id && r.OwnerId == user.Id)); + + return Ok(photos); + } +} diff --git a/Server/Controller/EventApiController.structs.cs b/Server/Controller/EventApiController.structs.cs new file mode 100644 index 0000000..4244f69 --- /dev/null +++ b/Server/Controller/EventApiController.structs.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; + +namespace Blahblah.FlowerStory.Server.Controller; + +/// +/// 事件参数 +/// +public record EventParameter +{ + /// + /// 花草唯一 id + /// + [Required] + public int FlowerId { get; init; } + + /// + /// 事件 id + /// + [Required] + public int EventId { get; init; } + + /// + /// 操作人姓名 + /// + [Required] + public string? ByUser { get; init; } + + /// + /// 事件备注 + /// + public string? Memo { get; init; } +} + +/// +/// 事件修改参数 +/// +public record EventUpdateParameter : EventParameter +{ + /// + /// 事件唯一 id + /// + [Required] + public int Id { get; set; } +} diff --git a/Server/Controller/EventController.cs b/Server/Controller/EventController.cs deleted file mode 100644 index a8ebc52..0000000 --- a/Server/Controller/EventController.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Blahblah.FlowerStory.Server.Data; -using Blahblah.FlowerStory.Server.Data.Model; -using Microsoft.AspNetCore.Mvc; - -namespace Blahblah.FlowerStory.Server.Controller; - -/// -/// 事件相关服务 -/// -[ApiController] -[Produces("application/json")] -[Route("api/event")] -public class EventController : BaseController -{ - /// - public EventController(FlowerDatabase database, ILogger? logger = null) : base(database, logger) - { - } - - /// - /// 获取用户相关所有符合条件的事件 - /// - /// - /// 请求示例: - /// - /// GET /api/event/query - /// Authorization: authorization id - /// - /// 参数: - /// - /// cid: int? - /// key: string? - /// from: long? - /// to: long? - /// cfrom: decimal? - /// cto: decimal? - /// - /// - /// 事件类型 id - /// 查询关键字 - /// 起始日期 - /// 结束日期 - /// 会话有效则返回符合条件的花草集 - /// 返回符合条件的花草集 - /// 未找到登录会话或已过期 - /// 用户已禁用 - /// 未找到关联用户 - [Route("query", Name = "queryEvents")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [HttpGet] - public ActionResult GetRecords( - [FromQuery(Name = "eid")] int? eventId, - [FromQuery] string? key, - [FromQuery] long? from, - [FromQuery] long? to) - { - var (result, user) = CheckPermission(); - if (result != null) - { - return result; - } - if (user == null) - { - return NotFound(); - } - - SaveDatabase(); - - var records = database.Records.Where(r => r.OwnerId == user.Id); - if (eventId != null) - { - records = records.Where(r => r.EventId == eventId); - } - if (key != null) - { - records = records.Where(r => - r.ByUserName != null && r.ByUserName.ToLower().Contains(key.ToLower()) - // TODO: notes - ); - } - if (from != null) - { - records = records.Where(r => r.DateUnixTime >= from); - } - if (to != null) - { - records = records.Where(r => r.DateUnixTime <= to); - } - - return Ok(records.ToArray()); - } -} diff --git a/Server/Controller/FlowerController.cs b/Server/Controller/FlowerApiController.cs similarity index 60% rename from Server/Controller/FlowerController.cs rename to Server/Controller/FlowerApiController.cs index 507c632..4f93517 100644 --- a/Server/Controller/FlowerController.cs +++ b/Server/Controller/FlowerApiController.cs @@ -7,15 +7,15 @@ using System.ComponentModel.DataAnnotations; namespace Blahblah.FlowerStory.Server.Controller; /// -/// 花草相关服务 +/// 花草相关 API 服务 /// [ApiController] [Produces("application/json")] [Route("api/flower")] -public class FlowerController : BaseController +public class FlowerApiController : BaseController { /// - public FlowerController(FlowerDatabase database, ILogger? logger = null) : base(database, logger) + public FlowerApiController(FlowerDatabase database, ILogger? logger = null) : base(database, logger) { } @@ -36,6 +36,7 @@ public class FlowerController : BaseController /// to: long? /// cfrom: decimal? /// cto: decimal? + /// p: bool? /// /// /// 类别 id @@ -44,6 +45,7 @@ public class FlowerController : BaseController /// 结束购买日期 /// 开销最小值 /// 开销最大值 + /// 是否包含封面图片 /// 会话有效则返回符合条件的花草集 /// 返回符合条件的花草集 /// 未找到登录会话或已过期 @@ -55,13 +57,15 @@ public class FlowerController : BaseController [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 = "cto")] decimal? costTo, + [FromQuery(Name = "p")] bool? includePhoto) { var (result, user) = CheckPermission(); if (result != null) @@ -104,6 +108,16 @@ public class FlowerController : BaseController flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo); } + 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(); + } + } + return Ok(flowers.ToArray()); } @@ -145,7 +159,8 @@ public class FlowerController : BaseController return NotFound(); } - var count = RemoveUserFlower(user.Id, id); + //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(); @@ -190,7 +205,8 @@ public class FlowerController : BaseController return NotFound(); } - var count = RemoveUserFlowers(user.Id, ids); + //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(); @@ -300,6 +316,7 @@ public class FlowerController : BaseController var flower = database.Flowers.SingleOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id); if (flower == null) { + SaveDatabase(); return NotFound(update.Id); } flower.CategoryId = update.CategoryId; @@ -311,4 +328,156 @@ public class FlowerController : BaseController return Ok(user); } + + /// + /// 添加花草封面 + /// + /// + /// 请求示例: + /// + /// 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) + { + SaveDatabase(); + return NotFound(id); + } + if (photo.Length > 0) + { + var file = WrapFormFile(photo); + if (file == null) + { + SaveDatabase(); + 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); + } + + var cover = new PhotoItem + { + FlowerId = id, + Record = record, + FileType = file.FileType, + FileName = file.Filename, + Path = file.Path, + DateUploadUnixTime = now + }; + database.Photos.Add(cover); + + try + { + await WriteToFile(user.Id, id, file); + } + catch (Exception ex) + { + SaveDatabase(); + return Problem(ex.ToString(), "api/flower/add_cover"); + // TODO: Logger + } + } + SaveDatabase(); + + 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); + } } diff --git a/Server/Controller/FlowerController.structs.cs b/Server/Controller/FlowerApiController.structs.cs similarity index 97% rename from Server/Controller/FlowerController.structs.cs rename to Server/Controller/FlowerApiController.structs.cs index bf29a21..f296847 100644 --- a/Server/Controller/FlowerController.structs.cs +++ b/Server/Controller/FlowerApiController.structs.cs @@ -42,8 +42,8 @@ public record FlowerParameter public record FlowerUpdateParameter : FlowerParameter { /// - /// 花草 id + /// 花草唯一 id /// [Required] public int Id { get; set; } -} \ No newline at end of file +} diff --git a/Server/Controller/ImageController.cs b/Server/Controller/ImageController.cs index 3d1a815..9f23372 100644 --- a/Server/Controller/ImageController.cs +++ b/Server/Controller/ImageController.cs @@ -1,5 +1,6 @@ using Blahblah.FlowerStory.Server.Data; using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; namespace Blahblah.FlowerStory.Server.Controller; @@ -52,4 +53,45 @@ public class ImageController : BaseController } return File(avatar, "image/png"); } + + /// + /// 请求花草照片 + /// + /// + /// 请求示例: + /// + /// GET /photo/flower/{fid}/{name} + /// Authorization: authorization id + /// + /// + /// 认证通过则显示花草照片 + /// 返回花草照片 + /// 认证失败 + /// 未找到花草照片 + [Route("flower/{fid}/{name}", Name = "getFlowerPhoto")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpGet] + public async Task GetFlowerPhoto([Required] int fid, [Required] string name) + { + 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 (System.IO.File.Exists(path)) + { + var data = await System.IO.File.ReadAllBytesAsync(path); + var ext = Path.GetExtension(path).ToLower(); + return File(data, ext == ".png" ? "image/png" : "image/jpeg"); + } + return NotFound(); + } } diff --git a/Server/Controller/UserApiController.cs b/Server/Controller/UserApiController.cs new file mode 100644 index 0000000..0939b7b --- /dev/null +++ b/Server/Controller/UserApiController.cs @@ -0,0 +1,410 @@ +using Blahblah.FlowerStory.Server.Data; +using Blahblah.FlowerStory.Server.Data.Model; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using System.Security.Cryptography; + +namespace Blahblah.FlowerStory.Server.Controller; + +/// +/// 用户会话相关 API 服务 +/// +[ApiController] +[Produces("application/json")] +[Route("api/user")] +public partial class UserApiController : BaseController +{ + /// + public UserApiController(FlowerDatabase db, ILogger logger) : base(db, logger) + { + } + + /// + /// 用户登录 + /// + /// + /// 请求示例: + /// + /// POST /api/user/auth + /// { + /// "id": "blahblah", + /// "password": "pwd123" + /// } + /// + /// + /// 登录参数 + /// 成功登录则返回自定义认证头 + /// 返回自定义认证头 + /// 认证失败 + /// 未找到用户 + [Route("auth", Name = "authenticate")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpPost] + [Consumes("application/json")] + public ActionResult Authenticate([FromBody] LoginParamter login) + { +#if DEBUG + logger?.LogInformation("user \"{user}\" try to login with password \"{password}\"", login.Id, login.Password); +#endif + //var user = database.Users.SingleOrDefault(u => u.UserId == login.Id); + var user = QueryUserItemForAuthentication(login.Id); + if (user == null) + { + logger?.LogWarning("user \"{user}\" not found", login.Id); + return NotFound(); + } + + // comput password hash with salt + string hash = HashPassword(login.Password, login.Id); + if (hash != user.Password) + { + logger?.LogWarning("hash result {hash}, unauthorized with hash {password}", hash, user.Password); + return Unauthorized(); + } + + // record the session + // TODO: singleton token + string clientApp; + int expires; + if (Request.Headers.TryGetValue("X-ClientAgent", out var clientAgent)) + { + clientApp = "app"; + expires = 30 * 24 * 60 * 60; // 30 days + } + else + { + clientApp = "browser"; + expires = 20 * 60; // 20 mins + } + var now = DateTimeOffset.UtcNow; + var token = new TokenItem + { + Id = Convert.ToBase64String(SHA256.HashData(Guid.NewGuid().ToByteArray())), + UserId = user.Id, + LogonDateUnixTime = now.ToUnixTimeMilliseconds(), + ActiveDateUnixTime = now.ToUnixTimeMilliseconds(), + ExpireDateUnixTime = now.AddSeconds(expires).ToUnixTimeMilliseconds(), + ExpireSeconds = expires, + ClientApp = clientApp, + ClientAgent = clientAgent.Count > 0 ? clientAgent : Request.Headers.UserAgent, + DeviceId = Request.Headers.TryGetValue("X-DeviceId", out var deviceId) ? deviceId.ToString() : null + }; + database.Tokens.Add(token); + + user.ActiveDateUnixTime = token.ActiveDateUnixTime; + SaveDatabase(); + + Response.Headers.Add(AuthHeader, token.Id); + return NoContent(); + } + + /// + /// 注销当前登录会话 + /// + /// + /// 请求示例: + /// + /// POST /api/user/logout + /// Authorization: authorization id + /// + /// + /// 注销失败则返回错误内容 + /// 注销成功 + /// 认证失败 + [Route("logout", Name = "logout")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [HttpPost] + public ActionResult Logout() + { + var (result, token) = CheckToken(); + if (result != null) + { + return result; + } + if (token == null) + { + return Unauthorized(); + } + + database.Tokens.Remove(token); + SaveDatabase(); + + return NoContent(); + } + + /// + /// 注册用户 + /// + /// + /// 请求示例: + /// + /// POST /api/user/register + /// { + /// "id": "blahblah", + /// "password": "pwd123", + /// "userName": "Blah blah", + /// "email": "blah@example.com", + /// "mobile": "18012345678" + /// } + /// + /// + /// 注册参数 + /// 成功注册则返回已注册的用户对象 + /// 返回已注册的用户对象 + /// 用户重复或其他服务器错误 + [Route("register", Name = "register")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [HttpPost] + [Consumes("application/json")] + public ActionResult Register([FromBody] UserParameter user) + { +#if DEBUG + logger?.LogInformation("user register, {user}", user); +#endif + if (database.Users.Any(u => u.UserId == user.Id)) + { + logger?.LogWarning("duplicate user \"{id}\"", user.Id); + return Problem("duplicateUser", "api/user/register"); + } + + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var item = new UserItem + { + UserId = user.Id, + Password = HashPassword(user.Password, user.Id), + Level = UserCommon, + RegisterDateUnixTime = now, + ActiveDateUnixTime = now, + Name = user.UserName, + Email = user.Email, + Mobile = user.Mobile + }; + database.Users.Add(item); + SaveDatabase(); + + return Ok(item); + } + + /// + /// 查询当前会话关联的用户 + /// + /// + /// 请求示例: + /// + /// GET /api/user/profile + /// Authorization: authorization id + /// + /// + /// 会话有效则返回关联的用户对象 + /// 返回关联的用户对象 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + [Route("profile", Name = "getProfile")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpGet] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public ActionResult Profile() + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + // update last active time + SaveDatabase(); + + return Ok(user); + } + + /// + /// 修改用户 + /// + /// + /// 请求示例: + /// + /// PUT /api/user/update + /// Authorization: authorization id + /// { + /// "userName": "Blah blah", + /// "email": "blah@example.com", + /// "mobile": "18012345678" + /// } + /// + /// + /// 修改参数 + /// 修改成功则返回已修改的用户对象 + /// 返回已修改的用户对象 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + /// 提交正文过大 + [Route("update", Name = "updateProfile")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [HttpPut] + [Consumes("application/json")] + public ActionResult Update([FromBody] UpdateParameter update) + { +#if DEBUG + logger?.LogInformation("user update, {user}", update); +#endif + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + user.Name = update.UserName; + user.Email = update.Email; + user.Mobile = update.Mobile; + SaveDatabase(); + + return Ok(user); + } + + /// + /// 修改用户头像 + /// + /// + /// 请求示例: + /// + /// PUT /api/user/update_avatar + /// Authorization: authorization id + /// + /// 参数: + /// + /// avatar: IFormFile + /// + /// + /// 用户头像 + /// 修改成功则返回 HTTP 204 + /// 修改成功 + /// 头像内容格式非法 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + /// 提交正文过大 + [Route("update_avatar", Name = "updateAvatar")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [HttpPut] + [Consumes("multipart/form-data")] + [RequestSizeLimit(5 * 1024 * 1024)] + public ActionResult UploadAvatar([Required] IFormFile avatar) + { +#if DEBUG + logger?.LogInformation("user update avatar, {avatar}", avatar); +#endif + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + if (avatar.Length > 0) + { + var file = WrapFormFile(avatar); + if (file == null) + { + return BadRequest(); + } + user.Avatar = file.Content; + } + SaveDatabase(); + + return NoContent(); + } + + /// + /// 移除用户头像 + /// + /// + /// 请求示例: + /// + /// DELETE /api/user/remove_avatar + /// Authorization: authorization id + /// + /// + /// 移除成功则返回修改的数据库行数 + /// 修改的数据库行数 + /// 未找到登录会话或已过期 + /// 用户已禁用 + /// 未找到关联用户 + [Route("remove_avatar", Name = "removeAvatar")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [HttpDelete] + public ActionResult RemoveAvatar() + { + var (result, user) = CheckPermission(); + if (result != null) + { + return result; + } + if (user == null) + { + return NotFound(); + } + + var count = RemoveUserAvatar(user.Id); + SaveDatabase(); + + return Ok(count); + } + + //#if DEBUG + /// + /// #DEBUG 获取所有用户 + /// + /// + [Route("debug_list", Name = "debug_list")] + [HttpGet] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public ActionResult GetUsers() + { + return Ok(database.Users.ToArray()); + } + + /// + /// #DEBUG 获取所有 token + /// + /// + [Route("debug_tokens", Name = "debug_tokens")] + [HttpGet] + [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] + public ActionResult GetTokens() + { + return Ok(database.Tokens.ToArray()); + } + //#endif +} diff --git a/Server/Controller/UserController.structs.cs b/Server/Controller/UserApiController.structs.cs similarity index 97% rename from Server/Controller/UserController.structs.cs rename to Server/Controller/UserApiController.structs.cs index 3d30fb0..f5cee42 100644 --- a/Server/Controller/UserController.structs.cs +++ b/Server/Controller/UserApiController.structs.cs @@ -2,7 +2,7 @@ namespace Blahblah.FlowerStory.Server.Controller; -partial class UserController +partial class UserApiController { } diff --git a/Server/Controller/UserController.cs b/Server/Controller/UserController.cs deleted file mode 100644 index 0d490a3..0000000 --- a/Server/Controller/UserController.cs +++ /dev/null @@ -1,407 +0,0 @@ -using Blahblah.FlowerStory.Server.Data; -using Blahblah.FlowerStory.Server.Data.Model; -using Microsoft.AspNetCore.Mvc; -using System.Security.Cryptography; - -namespace Blahblah.FlowerStory.Server.Controller -{ - /// - /// 用户会话相关服务 - /// - [ApiController] - [Produces("application/json")] - [Route("api/user")] - public partial class UserController : BaseController - { - /// - public UserController(FlowerDatabase db, ILogger logger) : base(db, logger) - { - } - - /// - /// 用户登录 - /// - /// - /// 请求示例: - /// - /// POST /api/user/auth - /// { - /// "id": "blahblah", - /// "password": "pwd123" - /// } - /// - /// - /// 登录参数 - /// 成功登录则返回自定义认证头 - /// 返回自定义认证头 - /// 认证失败 - /// 未找到用户 - [Route("auth", Name = "authenticate")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [HttpPost] - [Consumes("application/json")] - public ActionResult Authenticate([FromBody] LoginParamter login) - { -#if DEBUG - logger?.LogInformation("user \"{user}\" try to login with password \"{password}\"", login.Id, login.Password); -#endif - //var user = database.Users.SingleOrDefault(u => u.UserId == login.Id); - var user = QueryUserItemForAuthentication(login.Id); - if (user == null) - { - logger?.LogWarning("user \"{user}\" not found", login.Id); - return NotFound(); - } - - // comput password hash with salt - string hash = HashPassword(login.Password, login.Id); - if (hash != user.Password) - { - logger?.LogWarning("hash result {hash}, unauthorized with hash {password}", hash, user.Password); - return Unauthorized(); - } - - // record the session - // TODO: singleton token - string clientApp; - int expires; - if (Request.Headers.TryGetValue("X-ClientAgent", out var clientAgent)) - { - clientApp = "app"; - expires = 30 * 24 * 60 * 60; // 30 days - } - else - { - clientApp = "browser"; - expires = 20 * 60; // 20 mins - } - var now = DateTimeOffset.UtcNow; - var token = new TokenItem - { - Id = Convert.ToBase64String(SHA256.HashData(Guid.NewGuid().ToByteArray())), - UserId = user.Id, - LogonDateUnixTime = now.ToUnixTimeMilliseconds(), - ActiveDateUnixTime = now.ToUnixTimeMilliseconds(), - ExpireDateUnixTime = now.AddSeconds(expires).ToUnixTimeMilliseconds(), - ExpireSeconds = expires, - ClientApp = clientApp, - ClientAgent = clientAgent.Count > 0 ? clientAgent : Request.Headers.UserAgent, - DeviceId = Request.Headers.TryGetValue("X-DeviceId", out var deviceId) ? deviceId.ToString() : null - }; - database.Tokens.Add(token); - - user.ActiveDateUnixTime = token.ActiveDateUnixTime; - SaveDatabase(); - - Response.Headers.Add(AuthHeader, token.Id); - return NoContent(); - } - - /// - /// 注销当前登录会话 - /// - /// - /// 请求示例: - /// - /// POST /api/user/logout - /// Authorization: authorization id - /// - /// - /// 注销失败则返回错误内容 - /// 注销成功 - /// 认证失败 - [Route("logout", Name = "logout")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [HttpPost] - public ActionResult Logout() - { - var (result, token) = CheckToken(); - if (result != null) - { - return result; - } - if (token == null) - { - return Unauthorized(); - } - - database.Tokens.Remove(token); - SaveDatabase(); - - return NoContent(); - } - - /// - /// 注册用户 - /// - /// - /// 请求示例: - /// - /// POST /api/user/register - /// { - /// "id": "blahblah", - /// "password": "pwd123", - /// "userName": "Blah blah", - /// "email": "blah@example.com", - /// "mobile": "18012345678" - /// } - /// - /// - /// 注册参数 - /// 成功注册则返回已注册的用户对象 - /// 返回已注册的用户对象 - /// 用户重复或其他服务器错误 - [Route("register", Name = "register")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - [HttpPost] - [Consumes("application/json")] - public ActionResult Register([FromBody] UserParameter user) - { -#if DEBUG - logger?.LogInformation("user register, {user}", user); -#endif - if (database.Users.Any(u => u.UserId == user.Id)) - { - logger?.LogWarning("duplicate user \"{id}\"", user.Id); - return Problem("duplicateUser", "user/register", 500); - } - - var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - var item = new UserItem - { - UserId = user.Id, - Password = HashPassword(user.Password, user.Id), - Level = UserCommon, - RegisterDateUnixTime = now, - ActiveDateUnixTime = now, - Name = user.UserName, - Email = user.Email, - Mobile = user.Mobile - }; - database.Users.Add(item); - SaveDatabase(); - - return Ok(item); - } - - /// - /// 查询当前会话关联的用户 - /// - /// - /// 请求示例: - /// - /// GET /api/user/profile - /// Authorization: authorization id - /// - /// - /// 会话有效则返回关联的用户对象 - /// 返回关联的用户对象 - /// 未找到登录会话或已过期 - /// 用户已禁用 - /// 未找到关联用户 - [Route("profile", Name = "getProfile")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [HttpGet] - public ActionResult Profile() - { - var (result, user) = CheckPermission(); - if (result != null) - { - return result; - } - if (user == null) - { - return NotFound(); - } - - // update last active time - SaveDatabase(); - - return Ok(user); - } - - /// - /// 修改用户 - /// - /// - /// 请求示例: - /// - /// PUT /api/user/update - /// Authorization: authorization id - /// { - /// "userName": "Blah blah", - /// "email": "blah@example.com", - /// "mobile": "18012345678" - /// } - /// - /// - /// 修改参数 - /// 修改成功则返回已修改的用户对象 - /// 返回已修改的用户对象 - /// 未找到登录会话或已过期 - /// 用户已禁用 - /// 未找到关联用户 - /// 提交正文过大 - [Route("update", Name = "updateProfile")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] - [HttpPut] - [Consumes("application/json")] - public ActionResult Update([FromBody] UpdateParameter update) - { -#if DEBUG - logger?.LogInformation("user update, {user}", update); -#endif - var (result, user) = CheckPermission(); - if (result != null) - { - return result; - } - if (user == null) - { - return NotFound(); - } - - user.Name = update.UserName; - user.Email = update.Email; - user.Mobile = update.Mobile; - SaveDatabase(); - - return Ok(user); - } - - /// - /// 修改用户头像 - /// - /// - /// 请求示例: - /// - /// PUT /api/user/update_avatar - /// Authorization: authorization id - /// - /// 参数: - /// - /// avatar: IFormFile? - /// - /// - /// 用户头像 - /// 修改成功则返回 HTTP 204 - /// 修改成功 - /// 头像内容格式非法 - /// 未找到登录会话或已过期 - /// 用户已禁用 - /// 未找到关联用户 - /// 提交正文过大 - [Route("update_avatar", Name = "updateAvatar")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] - [HttpPut] - [Consumes("multipart/form-data")] - [RequestSizeLimit(5 * 1024 * 1024)] - public ActionResult UploadAvatar(IFormFile? avatar) - { -#if DEBUG - logger?.LogInformation("user update avatar, {avatar}", avatar); -#endif - var (result, user) = CheckPermission(); - if (result != null) - { - return result; - } - if (user == null) - { - return NotFound(); - } - - if (avatar?.Length > 0) - { - var file = WrapFormFile(avatar); - if (file == null) - { - return BadRequest(); - } - user.Avatar = file.Content; - } - SaveDatabase(); - - return Ok(user); - } - - /// - /// 移除用户头像 - /// - /// - /// 请求示例: - /// - /// DELETE /api/user/remove_avatar - /// Authorization: authorization id - /// - /// - /// 移除成功则返回修改的数据库行数 - /// 修改的数据库行数 - /// 未找到登录会话或已过期 - /// 用户已禁用 - /// 未找到关联用户 - [Route("remove_avatar", Name = "removeAvatar")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [HttpDelete] - public ActionResult RemoveAvatar() - { - var (result, user) = CheckPermission(); - if (result != null) - { - return result; - } - if (user == null) - { - return NotFound(); - } - - var count = RemoveUserAvatar(user.Id); - SaveDatabase(); - - return Ok(count); - } - - //#if DEBUG - /// - /// #DEBUG 获取所有用户 - /// - /// - [Route("debug_list", Name = "debug_list")] - [HttpGet] - public ActionResult GetUsers() - { - return Ok(database.Users.ToArray()); - } - - /// - /// #DEBUG 获取所有 token - /// - /// - [Route("debug_tokens", Name = "debug_tokens")] - [HttpGet] - public ActionResult GetTokens() - { - return Ok(database.Tokens.ToArray()); - } - //#endif - } -} diff --git a/Server/Data/FlowerDatabase.cs b/Server/Data/FlowerDatabase.cs index d707642..624da0b 100644 --- a/Server/Data/FlowerDatabase.cs +++ b/Server/Data/FlowerDatabase.cs @@ -36,4 +36,9 @@ public class FlowerDatabase : DbContext /// 会话令牌集 /// public DbSet Tokens { get; set; } + + /// + /// 照片集 + /// + public DbSet Photos { get; set; } } diff --git a/Server/Data/Model/FlowerItem.cs b/Server/Data/Model/FlowerItem.cs index 80d9e65..aa0a424 100644 --- a/Server/Data/Model/FlowerItem.cs +++ b/Server/Data/Model/FlowerItem.cs @@ -19,12 +19,19 @@ public class FlowerItem public int Id { get; set; } /// - /// 所有人 uid + /// 所有人 id /// [Column("uid")] + [ForeignKey(nameof(Owner))] [Required] public int OwnerId { get; set; } + /// + /// 所有人 + /// + [JsonIgnore] + public UserItem? Owner { get; set; } + /// /// 类别 id /// @@ -59,12 +66,6 @@ public class FlowerItem [Column("purchase")] public string? Purchase { get; set; } - /// - /// 购买照片 - /// - [Column("photo")] - public byte[]? Photo { get; set; } - /// /// 备注 /// @@ -77,4 +78,9 @@ public class FlowerItem [NotMapped] [JsonIgnore] public DateTimeOffset DateBuy => DateTimeOffset.FromUnixTimeMilliseconds(DateBuyUnixTime); + + /// + /// 封面相关照片 + /// + public ICollection? Photos { get; set; } } diff --git a/Server/Data/Model/PhotoItem.cs b/Server/Data/Model/PhotoItem.cs new file mode 100644 index 0000000..4203040 --- /dev/null +++ b/Server/Data/Model/PhotoItem.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace Blahblah.FlowerStory.Server.Data.Model; + +/// +/// 照片对象 +/// +[Table("photos")] +public class PhotoItem +{ + /// + /// 自增 id,主键 + /// + [Column("pid")] + [Key] + [Required] + public int Id { get; set; } + + /// + /// 关联花草 id + /// + [Column("fid")] + [ForeignKey(nameof(Flower))] + [Required] + public int FlowerId { get; set; } + + /// + /// 关联花草 + /// + [JsonIgnore] + public FlowerItem? Flower { get; set; } + + /// + /// 关联的事件 id + /// + [Column("rid")] + [ForeignKey(nameof(Record))] + [Required] + public int RecordId { get; set; } + + /// + /// 关联的事件 + /// + [JsonIgnore] + public RecordItem? Record { get; set; } + + /// + /// 图片类型 + /// + [Column("filetype")] + [Required] + public required string FileType { get; set; } + + /// + /// 文件名 + /// + [Column("filename")] + [Required] + public required string FileName { get; set; } + + /// + /// 文件路径 + /// + [Column("path")] + [Required] + public required string Path { get; set; } + + /// + /// 上传时间 + /// + [Column("dateupload")] + [Required] + [JsonPropertyName("dateUpload")] + public long DateUploadUnixTime { get; set; } + + /// + /// 上传时间 + /// + [NotMapped] + [JsonIgnore] + public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime); +} diff --git a/Server/Data/Model/RecordItem.cs b/Server/Data/Model/RecordItem.cs index 654f659..fe201b7 100644 --- a/Server/Data/Model/RecordItem.cs +++ b/Server/Data/Model/RecordItem.cs @@ -19,12 +19,33 @@ public class RecordItem public int Id { get; set; } /// - /// 关联人 uid + /// 关联人 id /// [Column("uid")] + [ForeignKey(nameof(Owner))] [Required] public int OwnerId { get; set; } + /// + /// 关联人 + /// + [JsonIgnore] + public UserItem? Owner { get; set; } + + /// + /// 关联花草 id + /// + [Column("fid")] + [ForeignKey(nameof(Flower))] + [Required] + public int FlowerId { get; set; } + + /// + /// 关联花草 + /// + [JsonIgnore] + public FlowerItem? Flower { get; set; } + /// /// 事件类型 /// @@ -52,12 +73,6 @@ public class RecordItem [Column("byname")] public string? ByUserName { get; set; } - /// - /// 事件关联照片 - /// - [Column("photo")] - public byte[]? Photo { get; set; } - /// /// 备注 /// @@ -70,4 +85,9 @@ public class RecordItem [NotMapped] [JsonIgnore] public DateTimeOffset Date => DateTimeOffset.FromUnixTimeMilliseconds(DateUnixTime); + + /// + /// 事件关联照片 + /// + public ICollection? Photos { get; set; } } diff --git a/Server/Migrations/20230525082941_AddPhotos.Designer.cs b/Server/Migrations/20230525082941_AddPhotos.Designer.cs new file mode 100644 index 0000000..a38277f --- /dev/null +++ b/Server/Migrations/20230525082941_AddPhotos.Designer.cs @@ -0,0 +1,300 @@ +// +using System; +using Blahblah.FlowerStory.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Blahblah.FlowerStory.Server.Migrations +{ + [DbContext(typeof(FlowerDatabase))] + [Migration("20230525082941_AddPhotos")] + partial class AddPhotos + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("fid"); + + b.Property("CategoryId") + .HasColumnType("INTEGER") + .HasColumnName("categoryid"); + + b.Property("Cost") + .HasColumnType("real") + .HasColumnName("cost"); + + b.Property("DateBuyUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("datebuy") + .HasAnnotation("Relational:JsonPropertyName", "dateBuy"); + + b.Property("Memo") + .HasColumnType("TEXT") + .HasColumnName("memo"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.Property("Purchase") + .HasColumnType("TEXT") + .HasColumnName("purchase"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("flowers"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("pid"); + + b.Property("DateUploadUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("dateupload") + .HasAnnotation("Relational:JsonPropertyName", "dateUpload"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("filename"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("filetype"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("RecordId") + .HasColumnType("INTEGER") + .HasColumnName("rid"); + + b.HasKey("Id"); + + b.HasIndex("RecordId"); + + b.ToTable("photos"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("rid"); + + b.Property("ByUserId") + .HasColumnType("INTEGER") + .HasColumnName("byuid"); + + b.Property("ByUserName") + .HasColumnType("TEXT") + .HasColumnName("byname"); + + b.Property("DateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("date") + .HasAnnotation("Relational:JsonPropertyName", "date"); + + b.Property("EventId") + .HasColumnType("INTEGER") + .HasColumnName("eid"); + + b.Property("FlowerId") + .HasColumnType("INTEGER") + .HasColumnName("fid"); + + b.Property("Memo") + .HasColumnType("TEXT") + .HasColumnName("memo"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.HasKey("Id"); + + b.HasIndex("FlowerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("records"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("tid"); + + b.Property("ActiveDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("activedate") + .HasAnnotation("Relational:JsonPropertyName", "activeDate"); + + b.Property("ClientAgent") + .HasColumnType("TEXT") + .HasColumnName("clientagent"); + + b.Property("ClientApp") + .HasColumnType("TEXT") + .HasColumnName("clientapp"); + + b.Property("DeviceId") + .HasColumnType("TEXT") + .HasColumnName("deviceid"); + + b.Property("ExpireDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("expiredate") + .HasAnnotation("Relational:JsonPropertyName", "expireDate"); + + b.Property("ExpireSeconds") + .HasColumnType("INTEGER") + .HasColumnName("expiresecs"); + + b.Property("LogonDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("logondate") + .HasAnnotation("Relational:JsonPropertyName", "logonDate"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.Property("VerifyCode") + .HasColumnType("TEXT") + .HasColumnName("verifycode"); + + b.HasKey("Id"); + + b.ToTable("tokens"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.Property("ActiveDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("activedate"); + + b.Property("Avatar") + .HasColumnType("BLOB") + .HasColumnName("avatar"); + + b.Property("Email") + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("Mobile") + .HasColumnType("TEXT") + .HasColumnName("mobile"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("RegisterDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("regdate") + .HasAnnotation("Relational:JsonPropertyName", "registerDate"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.RecordItem", "Record") + .WithMany("Photo") + .HasForeignKey("RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower") + .WithMany() + .HasForeignKey("FlowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flower"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.Navigation("Photo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20230525082941_AddPhotos.cs b/Server/Migrations/20230525082941_AddPhotos.cs new file mode 100644 index 0000000..2f22de6 --- /dev/null +++ b/Server/Migrations/20230525082941_AddPhotos.cs @@ -0,0 +1,143 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Blahblah.FlowerStory.Server.Migrations +{ + /// + public partial class AddPhotos : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "photo", + table: "records"); + + migrationBuilder.DropColumn( + name: "photo", + table: "flowers"); + + migrationBuilder.AddColumn( + name: "fid", + table: "records", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "photos", + columns: table => new + { + pid = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + rid = table.Column(type: "INTEGER", nullable: false), + filetype = table.Column(type: "TEXT", nullable: false), + filename = table.Column(type: "TEXT", nullable: false), + path = table.Column(type: "TEXT", nullable: false), + dateupload = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_photos", x => x.pid); + table.ForeignKey( + name: "FK_photos_records_rid", + column: x => x.rid, + principalTable: "records", + principalColumn: "rid", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_records_fid", + table: "records", + column: "fid"); + + migrationBuilder.CreateIndex( + name: "IX_records_uid", + table: "records", + column: "uid"); + + migrationBuilder.CreateIndex( + name: "IX_flowers_uid", + table: "flowers", + column: "uid"); + + migrationBuilder.CreateIndex( + name: "IX_photos_rid", + table: "photos", + column: "rid"); + + migrationBuilder.AddForeignKey( + name: "FK_flowers_users_uid", + table: "flowers", + column: "uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_records_flowers_fid", + table: "records", + column: "fid", + principalTable: "flowers", + principalColumn: "fid", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_records_users_uid", + table: "records", + column: "uid", + principalTable: "users", + principalColumn: "uid", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_flowers_users_uid", + table: "flowers"); + + migrationBuilder.DropForeignKey( + name: "FK_records_flowers_fid", + table: "records"); + + migrationBuilder.DropForeignKey( + name: "FK_records_users_uid", + table: "records"); + + migrationBuilder.DropTable( + name: "photos"); + + migrationBuilder.DropIndex( + name: "IX_records_fid", + table: "records"); + + migrationBuilder.DropIndex( + name: "IX_records_uid", + table: "records"); + + migrationBuilder.DropIndex( + name: "IX_flowers_uid", + table: "flowers"); + + migrationBuilder.DropColumn( + name: "fid", + table: "records"); + + migrationBuilder.AddColumn( + name: "photo", + table: "records", + type: "BLOB", + nullable: true); + + migrationBuilder.AddColumn( + name: "photo", + table: "flowers", + type: "BLOB", + nullable: true); + } + } +} diff --git a/Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.Designer.cs b/Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.Designer.cs new file mode 100644 index 0000000..06ac24e --- /dev/null +++ b/Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.Designer.cs @@ -0,0 +1,319 @@ +// +using System; +using Blahblah.FlowerStory.Server.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Blahblah.FlowerStory.Server.Migrations +{ + [DbContext(typeof(FlowerDatabase))] + [Migration("20230525091254_AddPhotosFlowerForeignKey")] + partial class AddPhotosFlowerForeignKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("fid"); + + b.Property("CategoryId") + .HasColumnType("INTEGER") + .HasColumnName("categoryid"); + + b.Property("Cost") + .HasColumnType("real") + .HasColumnName("cost"); + + b.Property("DateBuyUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("datebuy") + .HasAnnotation("Relational:JsonPropertyName", "dateBuy"); + + b.Property("Memo") + .HasColumnType("TEXT") + .HasColumnName("memo"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.Property("Purchase") + .HasColumnType("TEXT") + .HasColumnName("purchase"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("flowers"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("pid"); + + b.Property("DateUploadUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("dateupload") + .HasAnnotation("Relational:JsonPropertyName", "dateUpload"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("filename"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("filetype"); + + b.Property("FlowerId") + .HasColumnType("INTEGER") + .HasColumnName("fid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("RecordId") + .HasColumnType("INTEGER") + .HasColumnName("rid"); + + b.HasKey("Id"); + + b.HasIndex("FlowerId"); + + b.HasIndex("RecordId"); + + b.ToTable("photos"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("rid"); + + b.Property("ByUserId") + .HasColumnType("INTEGER") + .HasColumnName("byuid"); + + b.Property("ByUserName") + .HasColumnType("TEXT") + .HasColumnName("byname"); + + b.Property("DateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("date") + .HasAnnotation("Relational:JsonPropertyName", "date"); + + b.Property("EventId") + .HasColumnType("INTEGER") + .HasColumnName("eid"); + + b.Property("FlowerId") + .HasColumnType("INTEGER") + .HasColumnName("fid"); + + b.Property("Memo") + .HasColumnType("TEXT") + .HasColumnName("memo"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.HasKey("Id"); + + b.HasIndex("FlowerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("records"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b => + { + b.Property("Id") + .HasColumnType("TEXT") + .HasColumnName("tid"); + + b.Property("ActiveDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("activedate") + .HasAnnotation("Relational:JsonPropertyName", "activeDate"); + + b.Property("ClientAgent") + .HasColumnType("TEXT") + .HasColumnName("clientagent"); + + b.Property("ClientApp") + .HasColumnType("TEXT") + .HasColumnName("clientapp"); + + b.Property("DeviceId") + .HasColumnType("TEXT") + .HasColumnName("deviceid"); + + b.Property("ExpireDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("expiredate") + .HasAnnotation("Relational:JsonPropertyName", "expireDate"); + + b.Property("ExpireSeconds") + .HasColumnType("INTEGER") + .HasColumnName("expiresecs"); + + b.Property("LogonDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("logondate") + .HasAnnotation("Relational:JsonPropertyName", "logonDate"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.Property("VerifyCode") + .HasColumnType("TEXT") + .HasColumnName("verifycode"); + + b.HasKey("Id"); + + b.ToTable("tokens"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uid"); + + b.Property("ActiveDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("activedate"); + + b.Property("Avatar") + .HasColumnType("BLOB") + .HasColumnName("avatar"); + + b.Property("Email") + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("Level") + .HasColumnType("INTEGER") + .HasColumnName("level"); + + b.Property("Mobile") + .HasColumnType("TEXT") + .HasColumnName("mobile"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("RegisterDateUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("regdate") + .HasAnnotation("Relational:JsonPropertyName", "registerDate"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.HasKey("Id"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower") + .WithMany("Photos") + .HasForeignKey("FlowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.RecordItem", "Record") + .WithMany("Photos") + .HasForeignKey("RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flower"); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower") + .WithMany() + .HasForeignKey("FlowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flower"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.Navigation("Photos"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.cs b/Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.cs new file mode 100644 index 0000000..5a77cad --- /dev/null +++ b/Server/Migrations/20230525091254_AddPhotosFlowerForeignKey.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Blahblah.FlowerStory.Server.Migrations +{ + /// + public partial class AddPhotosFlowerForeignKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "fid", + table: "photos", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_photos_fid", + table: "photos", + column: "fid"); + + migrationBuilder.AddForeignKey( + name: "FK_photos_flowers_fid", + table: "photos", + column: "fid", + principalTable: "flowers", + principalColumn: "fid", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_photos_flowers_fid", + table: "photos"); + + migrationBuilder.DropIndex( + name: "IX_photos_fid", + table: "photos"); + + migrationBuilder.DropColumn( + name: "fid", + table: "photos"); + } + } +} diff --git a/Server/Migrations/FlowerDatabaseModelSnapshot.cs b/Server/Migrations/FlowerDatabaseModelSnapshot.cs index a736f1b..bfa546e 100644 --- a/Server/Migrations/FlowerDatabaseModelSnapshot.cs +++ b/Server/Migrations/FlowerDatabaseModelSnapshot.cs @@ -50,17 +50,59 @@ namespace Blahblah.FlowerStory.Server.Migrations .HasColumnType("INTEGER") .HasColumnName("uid"); - b.Property("Photo") - .HasColumnType("BLOB") - .HasColumnName("photo"); - b.Property("Purchase") .HasColumnType("TEXT") .HasColumnName("purchase"); b.HasKey("Id"); - b.ToTable("flowers"); + b.HasIndex("OwnerId"); + + b.ToTable("flowers", (string)null); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("pid"); + + b.Property("DateUploadUnixTime") + .HasColumnType("INTEGER") + .HasColumnName("dateupload") + .HasAnnotation("Relational:JsonPropertyName", "dateUpload"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("filename"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("filetype"); + + b.Property("FlowerId") + .HasColumnType("INTEGER") + .HasColumnName("fid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("RecordId") + .HasColumnType("INTEGER") + .HasColumnName("rid"); + + b.HasKey("Id"); + + b.HasIndex("FlowerId"); + + b.HasIndex("RecordId"); + + b.ToTable("photos", (string)null); }); modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => @@ -87,6 +129,10 @@ namespace Blahblah.FlowerStory.Server.Migrations .HasColumnType("INTEGER") .HasColumnName("eid"); + b.Property("FlowerId") + .HasColumnType("INTEGER") + .HasColumnName("fid"); + b.Property("Memo") .HasColumnType("TEXT") .HasColumnName("memo"); @@ -95,13 +141,13 @@ namespace Blahblah.FlowerStory.Server.Migrations .HasColumnType("INTEGER") .HasColumnName("uid"); - b.Property("Photo") - .HasColumnType("BLOB") - .HasColumnName("photo"); - b.HasKey("Id"); - b.ToTable("records"); + b.HasIndex("FlowerId"); + + b.HasIndex("OwnerId"); + + b.ToTable("records", (string)null); }); modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b => @@ -151,7 +197,7 @@ namespace Blahblah.FlowerStory.Server.Migrations b.HasKey("Id"); - b.ToTable("tokens"); + b.ToTable("tokens", (string)null); }); modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b => @@ -203,7 +249,66 @@ namespace Blahblah.FlowerStory.Server.Migrations b.HasKey("Id"); - b.ToTable("users"); + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.PhotoItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower") + .WithMany("Photos") + .HasForeignKey("FlowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.RecordItem", "Record") + .WithMany("Photos") + .HasForeignKey("RecordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flower"); + + b.Navigation("Record"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", "Flower") + .WithMany() + .HasForeignKey("FlowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blahblah.FlowerStory.Server.Data.Model.UserItem", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flower"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.FlowerItem", b => + { + b.Navigation("Photos"); + }); + + modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b => + { + b.Navigation("Photos"); }); #pragma warning restore 612, 618 }