complete controllers

This commit is contained in:
Tsanie Lily 2023-05-25 21:54:40 +08:00
parent 3b5bd291f9
commit 50e7297848
21 changed files with 2257 additions and 566 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
flower.db flower.db
TestCase/
# User-specific files # User-specific files
*.rsuser *.rsuser

View File

@ -3,6 +3,7 @@ using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@ -44,6 +45,10 @@ public abstract partial class BaseController : ControllerBase
/// 管理员用户 /// 管理员用户
/// </summary> /// </summary>
protected const int UserAdmin = 99; protected const int UserAdmin = 99;
/// <summary>
/// 封面事件
/// </summary>
protected const int EventCover = 0;
/// <summary> /// <summary>
/// 数据库对象 /// 数据库对象
@ -130,6 +135,17 @@ public abstract partial class BaseController : ControllerBase
return (Unauthorized(), null); return (Unauthorized(), null);
} }
var user = QueryUserItem(token.UserId); 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) if (user == null)
{ {
logger?.LogWarning("user not found with id {id}", token.UserId); logger?.LogWarning("user not found with id {id}", token.UserId);
@ -158,7 +174,7 @@ public abstract partial class BaseController : ControllerBase
/// <summary> /// <summary>
/// 保存数据库变动并输出日志 /// 保存数据库变动并输出日志
/// </summary> /// </summary>
/// <returns></returns> /// <returns>操作的数据库行数</returns>
protected int SaveDatabase() protected int SaveDatabase()
{ {
var count = database.SaveChanges(); var count = database.SaveChanges();
@ -177,7 +193,7 @@ public abstract partial class BaseController : ControllerBase
/// 读取文件到 byte 数组 /// 读取文件到 byte 数组
/// </summary> /// </summary>
/// <param name="file">来自请求的文件</param> /// <param name="file">来自请求的文件</param>
/// <returns></returns> /// <returns>文件结果对象</returns>
protected FileResult? WrapFormFile(IFormFile file) protected FileResult? WrapFormFile(IFormFile file)
{ {
if (file == null) if (file == null)
@ -203,16 +219,36 @@ public abstract partial class BaseController : ControllerBase
} }
var data = ms.ToArray(); var data = ms.ToArray();
var name = file.FileName; 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 return new FileResult
{ {
Filename = name, Filename = name,
FileType = ext,
Path = path,
Content = data Content = data
}; };
} }
return null; return null;
} }
/// <summary>
/// 写入文件到用户的花草目录中
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <param name="fid">花草唯一 id</param>
/// <param name="file">文件对象</param>
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);
}
} }
/// <summary> /// <summary>
@ -223,7 +259,17 @@ public record FileResult
/// <summary> /// <summary>
/// 文件名 /// 文件名
/// </summary> /// </summary>
public string? Filename { get; init; } public required string Filename { get; init; }
/// <summary>
/// 文件类型
/// </summary>
public required string FileType { get; init; }
/// <summary>
/// 储存路径
/// </summary>
public required string Path { get; set; }
/// <summary> /// <summary>
/// 文件内容 /// 文件内容
@ -231,3 +277,22 @@ public record FileResult
[Required] [Required]
public required byte[] Content { get; init; } public required byte[] Content { get; init; }
} }
/// <summary>
/// 照片参数
/// </summary>
public record PhotoParameter
{
/// <summary>
/// 花草 id
/// </summary>
[Required]
public int Id { get; set; }
/// <summary>
/// 封面照片
/// </summary>
[Required]
public required IFormFile Photo { get; set; }
}

View File

@ -1,5 +1,6 @@
using Blahblah.FlowerStory.Server.Data.Model; using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
namespace Blahblah.FlowerStory.Server.Controller; namespace Blahblah.FlowerStory.Server.Controller;
@ -14,7 +15,7 @@ partial class BaseController
protected UserItem? QueryUserItem(int uid) protected UserItem? QueryUserItem(int uid)
{ {
return database.Users 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(); .SingleOrDefault();
} }
@ -26,7 +27,7 @@ partial class BaseController
protected UserItem? QueryUserItemForAuthentication(string id) protected UserItem? QueryUserItemForAuthentication(string id)
{ {
return database.Users 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(); .SingleOrDefault();
} }
@ -49,27 +50,4 @@ partial class BaseController
{ {
return database.Database.ExecuteSql($"UPDATE \"users\" SET \"avatar\" = NULL WHERE \"uid\" = {uid}"); return database.Database.ExecuteSql($"UPDATE \"users\" SET \"avatar\" = NULL WHERE \"uid\" = {uid}");
} }
/// <summary>
/// 移除用户名下的花草
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <param name="fid">花草唯一 id</param>
/// <returns></returns>
protected int RemoveUserFlower(int uid, int fid)
{
return database.Database.ExecuteSql($"DELETE FROM \"flowers\" WHERE \"uid\" = {uid} AND \"fid\" = {fid}");
}
/// <summary>
/// 批量移除用户名下的花草
/// </summary>
/// <param name="uid">用户唯一 id</param>
/// <param name="fids">花草唯一 id 的数组</param>
/// <returns></returns>
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})");
}
} }

View File

@ -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;
/// <summary>
/// 事件相关 API 服务
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("api/event")]
public class EventApiController : BaseController
{
/// <inheritdoc/>
public EventApiController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
}
/// <summary>
/// 获取用户相关所有符合条件的事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/event/query
/// Authorization: authorization id
///
/// 参数:
///
/// eid: int?
/// key: string?
/// from: long?
/// to: long?
/// p: bool?
///
/// </remarks>
/// <param name="eventId">事件类型 id</param>
/// <param name="key">查询关键字</param>
/// <param name="from">起始日期</param>
/// <param name="to">结束日期</param>
/// <param name="includePhoto">是否包含图片</param>
/// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[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<RecordItem[]> 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());
}
/// <summary>
/// 移除用户的事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// DELETE /api/event/remove
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
///
/// </remarks>
/// <param name="id">事件唯一 id</param>
/// <returns>会话有效则返回操作影响的数据库行数</returns>
/// <response code="200">返回操作影响的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("remove", Name = "removeEvent")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpDelete]
public ActionResult<int> 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);
}
/// <summary>
/// 批量移除用户的事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/event/remove
/// Authorization: authorization id
/// [
/// 2, 4, 5, 11
/// ]
///
/// </remarks>
/// <param name="ids">要移除的事件唯一 id 的数组</param>
/// <returns>会话有效则返回操作影响的数据库行数</returns>
/// <response code="200">返回操作影响的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("removeany", Name = "removeEvents")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<int> 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);
}
/// <summary>
/// 用户添加事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/event/add
/// Authorization: authorization id
/// {
/// "flowerId": 1,
/// "eventId": 4, // 浇水
/// "byUser": "朋友",
/// "memo": "快干死了"
/// }
///
/// </remarks>
/// <param name="event">事件参数</param>
/// <returns>添加成功则返回已添加的事件对象</returns>
/// <response code="200">返回已添加的事件对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("add", Name = "addEvent")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<RecordItem> 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);
}
/// <summary>
/// 修改事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/event/update
/// Authorization: authorization id
/// {
/// "id": 1,
/// "flowerId": 1,
/// "eventId": 5, // 施肥
/// "byUser": null,
/// "memo": "花多多1号"
/// }
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的事件对象</returns>
/// <response code="200">返回已修改的事件对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到将修改的事件对象</response>
[Route("update", Name = "updateEvent")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpPut]
[Consumes("application/json")]
public ActionResult<RecordItem> 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);
}
/// <summary>
/// 添加事件关联照片
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/event/add_photo
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
/// photo: IFormFile
///
/// </remarks>
/// <param name="id">事件唯一 id</param>
/// <param name="photo">图片</param>
/// <returns>修改成功则返回 HTTP 204</returns>
/// <response code="204">修改成功</response>
/// <response code="400">照片格式非法</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者关联的事件</response>
/// <response code="413">提交正文过大</response>
[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<ActionResult> 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();
}
/// <summary>
/// 获取事件关联的照片列表
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/event/photos
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
///
/// </remarks>
/// <param name="id">事件唯一 id</param>
/// <returns>验证通过则返回事件关联的照片列表</returns>
/// <response code="200">返回事件关联的照片列表</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到事件对象</response>
[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<PhotoItem[]> 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);
}
}

View File

@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 事件参数
/// </summary>
public record EventParameter
{
/// <summary>
/// 花草唯一 id
/// </summary>
[Required]
public int FlowerId { get; init; }
/// <summary>
/// 事件 id
/// </summary>
[Required]
public int EventId { get; init; }
/// <summary>
/// 操作人姓名
/// </summary>
[Required]
public string? ByUser { get; init; }
/// <summary>
/// 事件备注
/// </summary>
public string? Memo { get; init; }
}
/// <summary>
/// 事件修改参数
/// </summary>
public record EventUpdateParameter : EventParameter
{
/// <summary>
/// 事件唯一 id
/// </summary>
[Required]
public int Id { get; set; }
}

View File

@ -1,95 +0,0 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
namespace Blahblah.FlowerStory.Server.Controller;
/// <summary>
/// 事件相关服务
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("api/event")]
public class EventController : BaseController
{
/// <inheritdoc/>
public EventController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{
}
/// <summary>
/// 获取用户相关所有符合条件的事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/event/query
/// Authorization: authorization id
///
/// 参数:
///
/// cid: int?
/// key: string?
/// from: long?
/// to: long?
/// cfrom: decimal?
/// cto: decimal?
///
/// </remarks>
/// <param name="eventId">事件类型 id</param>
/// <param name="key">查询关键字</param>
/// <param name="from">起始日期</param>
/// <param name="to">结束日期</param>
/// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("query", Name = "queryEvents")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public ActionResult<RecordItem[]> 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());
}
}

View File

@ -7,15 +7,15 @@ using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller; namespace Blahblah.FlowerStory.Server.Controller;
/// <summary> /// <summary>
/// 花草相关服务 /// 花草相关 API 服务
/// </summary> /// </summary>
[ApiController] [ApiController]
[Produces("application/json")] [Produces("application/json")]
[Route("api/flower")] [Route("api/flower")]
public class FlowerController : BaseController public class FlowerApiController : BaseController
{ {
/// <inheritdoc/> /// <inheritdoc/>
public FlowerController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger) public FlowerApiController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
{ {
} }
@ -36,6 +36,7 @@ public class FlowerController : BaseController
/// to: long? /// to: long?
/// cfrom: decimal? /// cfrom: decimal?
/// cto: decimal? /// cto: decimal?
/// p: bool?
/// ///
/// </remarks> /// </remarks>
/// <param name="categoryId">类别 id</param> /// <param name="categoryId">类别 id</param>
@ -44,6 +45,7 @@ public class FlowerController : BaseController
/// <param name="buyTo">结束购买日期</param> /// <param name="buyTo">结束购买日期</param>
/// <param name="costFrom">开销最小值</param> /// <param name="costFrom">开销最小值</param>
/// <param name="costTo">开销最大值</param> /// <param name="costTo">开销最大值</param>
/// <param name="includePhoto">是否包含封面图片</param>
/// <returns>会话有效则返回符合条件的花草集</returns> /// <returns>会话有效则返回符合条件的花草集</returns>
/// <response code="200">返回符合条件的花草集</response> /// <response code="200">返回符合条件的花草集</response>
/// <response code="401">未找到登录会话或已过期</response> /// <response code="401">未找到登录会话或已过期</response>
@ -55,13 +57,15 @@ public class FlowerController : BaseController
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet] [HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<FlowerItem[]> GetFlowers( public ActionResult<FlowerItem[]> GetFlowers(
[FromQuery(Name = "cid")] int? categoryId, [FromQuery(Name = "cid")] int? categoryId,
[FromQuery] string? key, [FromQuery] string? key,
[FromQuery(Name = "from")] long? buyFrom, [FromQuery(Name = "from")] long? buyFrom,
[FromQuery(Name = "to")] long? buyTo, [FromQuery(Name = "to")] long? buyTo,
[FromQuery(Name = "cfrom")] decimal? costFrom, [FromQuery(Name = "cfrom")] decimal? costFrom,
[FromQuery(Name = "cto")] decimal? costTo) [FromQuery(Name = "cto")] decimal? costTo,
[FromQuery(Name = "p")] bool? includePhoto)
{ {
var (result, user) = CheckPermission(); var (result, user) = CheckPermission();
if (result != null) if (result != null)
@ -104,6 +108,16 @@ public class FlowerController : BaseController
flowers = flowers.Where(f => f.Cost != null && f.Cost <= costTo); 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()); return Ok(flowers.ToArray());
} }
@ -145,7 +159,8 @@ public class FlowerController : BaseController
return NotFound(); 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(); SaveDatabase();
@ -190,7 +205,8 @@ public class FlowerController : BaseController
return NotFound(); 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(); SaveDatabase();
@ -300,6 +316,7 @@ public class FlowerController : BaseController
var flower = database.Flowers.SingleOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id); var flower = database.Flowers.SingleOrDefault(f => f.Id == update.Id && f.OwnerId == user.Id);
if (flower == null) if (flower == null)
{ {
SaveDatabase();
return NotFound(update.Id); return NotFound(update.Id);
} }
flower.CategoryId = update.CategoryId; flower.CategoryId = update.CategoryId;
@ -311,4 +328,156 @@ public class FlowerController : BaseController
return Ok(user); return Ok(user);
} }
/// <summary>
/// 添加花草封面
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/flower/add_cover
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
/// photo: IFormFile
///
/// </remarks>
/// <param name="id">花草唯一 id</param>
/// <param name="photo">封面图片</param>
/// <returns>修改成功则返回 HTTP 204</returns>
/// <response code="204">修改成功</response>
/// <response code="400">照片格式非法</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者关联的花草</response>
/// <response code="413">提交正文过大</response>
[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<ActionResult> 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();
}
/// <summary>
/// 获取花草特定类型事件的照片列表
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/flower/photos
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
/// eid: int?
///
/// </remarks>
/// <param name="id">花草唯一 id</param>
/// <param name="eventId">事件类型 id0 为封面</param>
/// <returns>验证通过则返回花草特定类型事件的照片列表</returns>
/// <response code="200">返回花草特定类型事件的照片列表</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者未找到花草对象</response>
[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<PhotoItem[]> 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);
}
} }

View File

@ -42,8 +42,8 @@ public record FlowerParameter
public record FlowerUpdateParameter : FlowerParameter public record FlowerUpdateParameter : FlowerParameter
{ {
/// <summary> /// <summary>
/// 花草 id /// 花草唯一 id
/// </summary> /// </summary>
[Required] [Required]
public int Id { get; set; } public int Id { get; set; }
} }

View File

@ -1,5 +1,6 @@
using Blahblah.FlowerStory.Server.Data; using Blahblah.FlowerStory.Server.Data;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller; namespace Blahblah.FlowerStory.Server.Controller;
@ -52,4 +53,45 @@ public class ImageController : BaseController
} }
return File(avatar, "image/png"); return File(avatar, "image/png");
} }
/// <summary>
/// 请求花草照片
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /photo/flower/{fid}/{name}
/// Authorization: authorization id
///
/// </remarks>
/// <returns>认证通过则显示花草照片</returns>
/// <response code="200">返回花草照片</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到花草照片</response>
[Route("flower/{fid}/{name}", Name = "getFlowerPhoto")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public async Task<ActionResult> GetFlowerPhoto([Required] int fid, [Required] string name)
{
var (result, token) = CheckToken();
if (result != null)
{
return result;
}
if (token == null)
{
return Unauthorized();
}
var 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();
}
} }

View File

@ -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;
/// <summary>
/// 用户会话相关 API 服务
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("api/user")]
public partial class UserApiController : BaseController
{
/// <inheritdoc/>
public UserApiController(FlowerDatabase db, ILogger<UserApiController> logger) : base(db, logger)
{
}
/// <summary>
/// 用户登录
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/auth
/// {
/// "id": "blahblah",
/// "password": "pwd123"
/// }
///
/// </remarks>
/// <param name="login">登录参数</param>
/// <returns>成功登录则返回自定义认证头</returns>
/// <response code="204">返回自定义认证头</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到用户</response>
[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();
}
/// <summary>
/// 注销当前登录会话
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/logout
/// Authorization: authorization id
///
/// </remarks>
/// <returns>注销失败则返回错误内容</returns>
/// <response code="204">注销成功</response>
/// <response code="401">认证失败</response>
[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();
}
/// <summary>
/// 注册用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/register
/// {
/// "id": "blahblah",
/// "password": "pwd123",
/// "userName": "Blah blah",
/// "email": "blah@example.com",
/// "mobile": "18012345678"
/// }
///
/// </remarks>
/// <param name="user">注册参数</param>
/// <returns>成功注册则返回已注册的用户对象</returns>
/// <response code="200">返回已注册的用户对象</response>
/// <response code="500">用户重复或其他服务器错误</response>
[Route("register", Name = "register")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<UserItem> 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);
}
/// <summary>
/// 查询当前会话关联的用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/user/profile
/// Authorization: authorization id
///
/// </remarks>
/// <returns>会话有效则返回关联的用户对象</returns>
/// <response code="200">返回关联的用户对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[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<UserItem> Profile()
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
// update last active time
SaveDatabase();
return Ok(user);
}
/// <summary>
/// 修改用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/user/update
/// Authorization: authorization id
/// {
/// "userName": "Blah blah",
/// "email": "blah@example.com",
/// "mobile": "18012345678"
/// }
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的用户对象</returns>
/// <response code="200">返回已修改的用户对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[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<UserItem> 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);
}
/// <summary>
/// 修改用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/user/update_avatar
/// Authorization: authorization id
///
/// 参数:
///
/// avatar: IFormFile
///
/// </remarks>
/// <param name="avatar">用户头像</param>
/// <returns>修改成功则返回 HTTP 204</returns>
/// <response code="204">修改成功</response>
/// <response code="400">头像内容格式非法</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[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<UserItem> 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();
}
/// <summary>
/// 移除用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// DELETE /api/user/remove_avatar
/// Authorization: authorization id
///
/// </remarks>
/// <returns>移除成功则返回修改的数据库行数</returns>
/// <response code="200">修改的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[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
/// <summary>
/// #DEBUG 获取所有用户
/// </summary>
/// <returns></returns>
[Route("debug_list", Name = "debug_list")]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<UserItem[]> GetUsers()
{
return Ok(database.Users.ToArray());
}
/// <summary>
/// #DEBUG 获取所有 token
/// </summary>
/// <returns></returns>
[Route("debug_tokens", Name = "debug_tokens")]
[HttpGet]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public ActionResult<TokenItem[]> GetTokens()
{
return Ok(database.Tokens.ToArray());
}
//#endif
}

View File

@ -2,7 +2,7 @@
namespace Blahblah.FlowerStory.Server.Controller; namespace Blahblah.FlowerStory.Server.Controller;
partial class UserController partial class UserApiController
{ {
} }

View File

@ -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
{
/// <summary>
/// 用户会话相关服务
/// </summary>
[ApiController]
[Produces("application/json")]
[Route("api/user")]
public partial class UserController : BaseController
{
/// <inheritdoc/>
public UserController(FlowerDatabase db, ILogger<UserController> logger) : base(db, logger)
{
}
/// <summary>
/// 用户登录
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/auth
/// {
/// "id": "blahblah",
/// "password": "pwd123"
/// }
///
/// </remarks>
/// <param name="login">登录参数</param>
/// <returns>成功登录则返回自定义认证头</returns>
/// <response code="204">返回自定义认证头</response>
/// <response code="401">认证失败</response>
/// <response code="404">未找到用户</response>
[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();
}
/// <summary>
/// 注销当前登录会话
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/logout
/// Authorization: authorization id
///
/// </remarks>
/// <returns>注销失败则返回错误内容</returns>
/// <response code="204">注销成功</response>
/// <response code="401">认证失败</response>
[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();
}
/// <summary>
/// 注册用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/user/register
/// {
/// "id": "blahblah",
/// "password": "pwd123",
/// "userName": "Blah blah",
/// "email": "blah@example.com",
/// "mobile": "18012345678"
/// }
///
/// </remarks>
/// <param name="user">注册参数</param>
/// <returns>成功注册则返回已注册的用户对象</returns>
/// <response code="200">返回已注册的用户对象</response>
/// <response code="500">用户重复或其他服务器错误</response>
[Route("register", Name = "register")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[HttpPost]
[Consumes("application/json")]
public ActionResult<UserItem> 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);
}
/// <summary>
/// 查询当前会话关联的用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// GET /api/user/profile
/// Authorization: authorization id
///
/// </remarks>
/// <returns>会话有效则返回关联的用户对象</returns>
/// <response code="200">返回关联的用户对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[Route("profile", Name = "getProfile")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
public ActionResult<UserItem> Profile()
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
// update last active time
SaveDatabase();
return Ok(user);
}
/// <summary>
/// 修改用户
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/user/update
/// Authorization: authorization id
/// {
/// "userName": "Blah blah",
/// "email": "blah@example.com",
/// "mobile": "18012345678"
/// }
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <returns>修改成功则返回已修改的用户对象</returns>
/// <response code="200">返回已修改的用户对象</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[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<UserItem> 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);
}
/// <summary>
/// 修改用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/user/update_avatar
/// Authorization: authorization id
///
/// 参数:
///
/// avatar: IFormFile?
///
/// </remarks>
/// <param name="avatar">用户头像</param>
/// <returns>修改成功则返回 HTTP 204</returns>
/// <response code="204">修改成功</response>
/// <response code="400">头像内容格式非法</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
/// <response code="413">提交正文过大</response>
[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<UserItem> 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);
}
/// <summary>
/// 移除用户头像
/// </summary>
/// <remarks>
/// 请求示例:
///
/// DELETE /api/user/remove_avatar
/// Authorization: authorization id
///
/// </remarks>
/// <returns>移除成功则返回修改的数据库行数</returns>
/// <response code="200">修改的数据库行数</response>
/// <response code="401">未找到登录会话或已过期</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户</response>
[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
/// <summary>
/// #DEBUG 获取所有用户
/// </summary>
/// <returns></returns>
[Route("debug_list", Name = "debug_list")]
[HttpGet]
public ActionResult<UserItem[]> GetUsers()
{
return Ok(database.Users.ToArray());
}
/// <summary>
/// #DEBUG 获取所有 token
/// </summary>
/// <returns></returns>
[Route("debug_tokens", Name = "debug_tokens")]
[HttpGet]
public ActionResult<TokenItem[]> GetTokens()
{
return Ok(database.Tokens.ToArray());
}
//#endif
}
}

View File

@ -36,4 +36,9 @@ public class FlowerDatabase : DbContext
/// 会话令牌集 /// 会话令牌集
/// </summary> /// </summary>
public DbSet<TokenItem> Tokens { get; set; } public DbSet<TokenItem> Tokens { get; set; }
/// <summary>
/// 照片集
/// </summary>
public DbSet<PhotoItem> Photos { get; set; }
} }

View File

@ -19,12 +19,19 @@ public class FlowerItem
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// 所有人 uid /// 所有人 id
/// </summary> /// </summary>
[Column("uid")] [Column("uid")]
[ForeignKey(nameof(Owner))]
[Required] [Required]
public int OwnerId { get; set; } public int OwnerId { get; set; }
/// <summary>
/// 所有人
/// </summary>
[JsonIgnore]
public UserItem? Owner { get; set; }
/// <summary> /// <summary>
/// 类别 id /// 类别 id
/// </summary> /// </summary>
@ -59,12 +66,6 @@ public class FlowerItem
[Column("purchase")] [Column("purchase")]
public string? Purchase { get; set; } public string? Purchase { get; set; }
/// <summary>
/// 购买照片
/// </summary>
[Column("photo")]
public byte[]? Photo { get; set; }
/// <summary> /// <summary>
/// 备注 /// 备注
/// </summary> /// </summary>
@ -77,4 +78,9 @@ public class FlowerItem
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
public DateTimeOffset DateBuy => DateTimeOffset.FromUnixTimeMilliseconds(DateBuyUnixTime); public DateTimeOffset DateBuy => DateTimeOffset.FromUnixTimeMilliseconds(DateBuyUnixTime);
/// <summary>
/// 封面相关照片
/// </summary>
public ICollection<PhotoItem>? Photos { get; set; }
} }

View File

@ -0,0 +1,84 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace Blahblah.FlowerStory.Server.Data.Model;
/// <summary>
/// 照片对象
/// </summary>
[Table("photos")]
public class PhotoItem
{
/// <summary>
/// 自增 id主键
/// </summary>
[Column("pid")]
[Key]
[Required]
public int Id { get; set; }
/// <summary>
/// 关联花草 id
/// </summary>
[Column("fid")]
[ForeignKey(nameof(Flower))]
[Required]
public int FlowerId { get; set; }
/// <summary>
/// 关联花草
/// </summary>
[JsonIgnore]
public FlowerItem? Flower { get; set; }
/// <summary>
/// 关联的事件 id
/// </summary>
[Column("rid")]
[ForeignKey(nameof(Record))]
[Required]
public int RecordId { get; set; }
/// <summary>
/// 关联的事件
/// </summary>
[JsonIgnore]
public RecordItem? Record { get; set; }
/// <summary>
/// 图片类型
/// </summary>
[Column("filetype")]
[Required]
public required string FileType { get; set; }
/// <summary>
/// 文件名
/// </summary>
[Column("filename")]
[Required]
public required string FileName { get; set; }
/// <summary>
/// 文件路径
/// </summary>
[Column("path")]
[Required]
public required string Path { get; set; }
/// <summary>
/// 上传时间
/// </summary>
[Column("dateupload")]
[Required]
[JsonPropertyName("dateUpload")]
public long DateUploadUnixTime { get; set; }
/// <summary>
/// 上传时间
/// </summary>
[NotMapped]
[JsonIgnore]
public DateTimeOffset DateUpload => DateTimeOffset.FromUnixTimeMilliseconds(DateUploadUnixTime);
}

View File

@ -19,12 +19,33 @@ public class RecordItem
public int Id { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// 关联人 uid /// 关联人 id
/// </summary> /// </summary>
[Column("uid")] [Column("uid")]
[ForeignKey(nameof(Owner))]
[Required] [Required]
public int OwnerId { get; set; } public int OwnerId { get; set; }
/// <summary>
/// 关联人
/// </summary>
[JsonIgnore]
public UserItem? Owner { get; set; }
/// <summary>
/// 关联花草 id
/// </summary>
[Column("fid")]
[ForeignKey(nameof(Flower))]
[Required]
public int FlowerId { get; set; }
/// <summary>
/// 关联花草
/// </summary>
[JsonIgnore]
public FlowerItem? Flower { get; set; }
/// <summary> /// <summary>
/// 事件类型 /// 事件类型
/// </summary> /// </summary>
@ -52,12 +73,6 @@ public class RecordItem
[Column("byname")] [Column("byname")]
public string? ByUserName { get; set; } public string? ByUserName { get; set; }
/// <summary>
/// 事件关联照片
/// </summary>
[Column("photo")]
public byte[]? Photo { get; set; }
/// <summary> /// <summary>
/// 备注 /// 备注
/// </summary> /// </summary>
@ -70,4 +85,9 @@ public class RecordItem
[NotMapped] [NotMapped]
[JsonIgnore] [JsonIgnore]
public DateTimeOffset Date => DateTimeOffset.FromUnixTimeMilliseconds(DateUnixTime); public DateTimeOffset Date => DateTimeOffset.FromUnixTimeMilliseconds(DateUnixTime);
/// <summary>
/// 事件关联照片
/// </summary>
public ICollection<PhotoItem>? Photos { get; set; }
} }

View File

@ -0,0 +1,300 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER")
.HasColumnName("categoryid");
b.Property<decimal?>("Cost")
.HasColumnType("real")
.HasColumnName("cost");
b.Property<long>("DateBuyUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("datebuy")
.HasAnnotation("Relational:JsonPropertyName", "dateBuy");
b.Property<string>("Memo")
.HasColumnType("TEXT")
.HasColumnName("memo");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("pid");
b.Property<long>("DateUploadUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("dateupload")
.HasAnnotation("Relational:JsonPropertyName", "dateUpload");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filename");
b.Property<string>("FileType")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filetype");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rid");
b.Property<int?>("ByUserId")
.HasColumnType("INTEGER")
.HasColumnName("byuid");
b.Property<string>("ByUserName")
.HasColumnType("TEXT")
.HasColumnName("byname");
b.Property<long>("DateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("date")
.HasAnnotation("Relational:JsonPropertyName", "date");
b.Property<int>("EventId")
.HasColumnType("INTEGER")
.HasColumnName("eid");
b.Property<int>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<string>("Memo")
.HasColumnType("TEXT")
.HasColumnName("memo");
b.Property<int>("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<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("tid");
b.Property<long>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate")
.HasAnnotation("Relational:JsonPropertyName", "activeDate");
b.Property<string>("ClientAgent")
.HasColumnType("TEXT")
.HasColumnName("clientagent");
b.Property<string>("ClientApp")
.HasColumnType("TEXT")
.HasColumnName("clientapp");
b.Property<string>("DeviceId")
.HasColumnType("TEXT")
.HasColumnName("deviceid");
b.Property<long>("ExpireDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("expiredate")
.HasAnnotation("Relational:JsonPropertyName", "expireDate");
b.Property<int>("ExpireSeconds")
.HasColumnType("INTEGER")
.HasColumnName("expiresecs");
b.Property<long>("LogonDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("logondate")
.HasAnnotation("Relational:JsonPropertyName", "logonDate");
b.Property<int>("UserId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("VerifyCode")
.HasColumnType("TEXT")
.HasColumnName("verifycode");
b.HasKey("Id");
b.ToTable("tokens");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<long?>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate");
b.Property<byte[]>("Avatar")
.HasColumnType("BLOB")
.HasColumnName("avatar");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<int>("Level")
.HasColumnType("INTEGER")
.HasColumnName("level");
b.Property<string>("Mobile")
.HasColumnType("TEXT")
.HasColumnName("mobile");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<long>("RegisterDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("regdate")
.HasAnnotation("Relational:JsonPropertyName", "registerDate");
b.Property<string>("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
}
}
}

View File

@ -0,0 +1,143 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Blahblah.FlowerStory.Server.Migrations
{
/// <inheritdoc />
public partial class AddPhotos : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "photo",
table: "records");
migrationBuilder.DropColumn(
name: "photo",
table: "flowers");
migrationBuilder.AddColumn<int>(
name: "fid",
table: "records",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateTable(
name: "photos",
columns: table => new
{
pid = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
rid = table.Column<int>(type: "INTEGER", nullable: false),
filetype = table.Column<string>(type: "TEXT", nullable: false),
filename = table.Column<string>(type: "TEXT", nullable: false),
path = table.Column<string>(type: "TEXT", nullable: false),
dateupload = table.Column<long>(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);
}
/// <inheritdoc />
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<byte[]>(
name: "photo",
table: "records",
type: "BLOB",
nullable: true);
migrationBuilder.AddColumn<byte[]>(
name: "photo",
table: "flowers",
type: "BLOB",
nullable: true);
}
}
}

View File

@ -0,0 +1,319 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER")
.HasColumnName("categoryid");
b.Property<decimal?>("Cost")
.HasColumnType("real")
.HasColumnName("cost");
b.Property<long>("DateBuyUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("datebuy")
.HasAnnotation("Relational:JsonPropertyName", "dateBuy");
b.Property<string>("Memo")
.HasColumnType("TEXT")
.HasColumnName("memo");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("pid");
b.Property<long>("DateUploadUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("dateupload")
.HasAnnotation("Relational:JsonPropertyName", "dateUpload");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filename");
b.Property<string>("FileType")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filetype");
b.Property<int>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("rid");
b.Property<int?>("ByUserId")
.HasColumnType("INTEGER")
.HasColumnName("byuid");
b.Property<string>("ByUserName")
.HasColumnType("TEXT")
.HasColumnName("byname");
b.Property<long>("DateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("date")
.HasAnnotation("Relational:JsonPropertyName", "date");
b.Property<int>("EventId")
.HasColumnType("INTEGER")
.HasColumnName("eid");
b.Property<int>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<string>("Memo")
.HasColumnType("TEXT")
.HasColumnName("memo");
b.Property<int>("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<string>("Id")
.HasColumnType("TEXT")
.HasColumnName("tid");
b.Property<long>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate")
.HasAnnotation("Relational:JsonPropertyName", "activeDate");
b.Property<string>("ClientAgent")
.HasColumnType("TEXT")
.HasColumnName("clientagent");
b.Property<string>("ClientApp")
.HasColumnType("TEXT")
.HasColumnName("clientapp");
b.Property<string>("DeviceId")
.HasColumnType("TEXT")
.HasColumnName("deviceid");
b.Property<long>("ExpireDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("expiredate")
.HasAnnotation("Relational:JsonPropertyName", "expireDate");
b.Property<int>("ExpireSeconds")
.HasColumnType("INTEGER")
.HasColumnName("expiresecs");
b.Property<long>("LogonDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("logondate")
.HasAnnotation("Relational:JsonPropertyName", "logonDate");
b.Property<int>("UserId")
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<string>("VerifyCode")
.HasColumnType("TEXT")
.HasColumnName("verifycode");
b.HasKey("Id");
b.ToTable("tokens");
});
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("uid");
b.Property<long?>("ActiveDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("activedate");
b.Property<byte[]>("Avatar")
.HasColumnType("BLOB")
.HasColumnName("avatar");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<int>("Level")
.HasColumnType("INTEGER")
.HasColumnName("level");
b.Property<string>("Mobile")
.HasColumnType("TEXT")
.HasColumnName("mobile");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("name");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<long>("RegisterDateUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("regdate")
.HasAnnotation("Relational:JsonPropertyName", "registerDate");
b.Property<string>("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
}
}
}

View File

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Blahblah.FlowerStory.Server.Migrations
{
/// <inheritdoc />
public partial class AddPhotosFlowerForeignKey : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
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);
}
/// <inheritdoc />
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");
}
}
}

View File

@ -50,17 +50,59 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("uid"); .HasColumnName("uid");
b.Property<byte[]>("Photo")
.HasColumnType("BLOB")
.HasColumnName("photo");
b.Property<string>("Purchase") b.Property<string>("Purchase")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("purchase"); .HasColumnName("purchase");
b.HasKey("Id"); 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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("pid");
b.Property<long>("DateUploadUnixTime")
.HasColumnType("INTEGER")
.HasColumnName("dateupload")
.HasAnnotation("Relational:JsonPropertyName", "dateUpload");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filename");
b.Property<string>("FileType")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("filetype");
b.Property<int>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("path");
b.Property<int>("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 => modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.RecordItem", b =>
@ -87,6 +129,10 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("eid"); .HasColumnName("eid");
b.Property<int>("FlowerId")
.HasColumnType("INTEGER")
.HasColumnName("fid");
b.Property<string>("Memo") b.Property<string>("Memo")
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("memo"); .HasColumnName("memo");
@ -95,13 +141,13 @@ namespace Blahblah.FlowerStory.Server.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("uid"); .HasColumnName("uid");
b.Property<byte[]>("Photo")
.HasColumnType("BLOB")
.HasColumnName("photo");
b.HasKey("Id"); 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 => modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.TokenItem", b =>
@ -151,7 +197,7 @@ namespace Blahblah.FlowerStory.Server.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("tokens"); b.ToTable("tokens", (string)null);
}); });
modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b => modelBuilder.Entity("Blahblah.FlowerStory.Server.Data.Model.UserItem", b =>
@ -203,7 +249,66 @@ namespace Blahblah.FlowerStory.Server.Migrations
b.HasKey("Id"); 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 #pragma warning restore 612, 618
} }