flower-story/Server/Controller/EventApiController.cs
2023-07-13 12:10:30 +08:00

781 lines
25 KiB
C#

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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[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)
{
records = records.Include(r => r.Photos);
}
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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[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: "快干死了"
/// lon: 29.5462794
/// lat: 106.5380034
/// photos: &lt;photo&gt;[]
///
/// </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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
// 5 photos
[RequestSizeLimit(75 * 1024 * 1024)]
public async Task<ActionResult<RecordItem>> AddEvent([FromForm] EventParameter @event)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var item = new RecordItem
{
OwnerId = user.Id,
FlowerId = @event.FlowerId,
EventId = @event.CategoryId,
DateUnixTime = now,
ByUserId = @event.ByUser == null ? user.Id : null,
ByUserName = @event.ByUser,
Memo = @event.Memo,
Latitude = @event.Latitude,
Longitude = @event.Longitude
};
database.Records.Add(item);
SaveDatabase();
if (@event.Photos?.Length > 0)
{
try
{
await ExecuteTransaction(async token =>
{
foreach (var photo in @event.Photos)
{
var file = WrapFormFile(photo);
if (file == null)
{
continue;
}
var p = new PhotoItem
{
OwnerId = user.Id,
FlowerId = @event.FlowerId,
RecordId = item.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
AddPhotoItem(p);
await WriteToFile(@event.FlowerId, file, token);
}
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/event/add");
// TODO: Logger
}
}
return Ok(item);
}
/// <summary>
/// 修改事件
/// </summary>
/// <remarks>
/// 请求示例:
///
/// PUT /api/event/update
/// Authorization: authorization id
///
/// 参数:
///
/// id: 1
/// flowerId: 1
/// eventId": 5 // 施肥
/// byUser: null
/// memo: "花多多1号"
/// lon: 29.5462794
/// lat: 106.5380034
/// photos: &lt;photo&gt;[]
///
/// </remarks>
/// <param name="update">修改参数</param>
/// <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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPut]
[Consumes("multipart/form-data")]
// 5 photos
[RequestSizeLimit(75 * 1024 * 1024)]
public async Task<ActionResult<RecordItem>> Update([FromForm] EventUpdateParameter update)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var record = database.Records.SingleOrDefault(r => r.Id == update.Id && r.OwnerId == user.Id);
if (record == null)
{
return NotFound($"Event id {update.Id} not found");
}
var now = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
record.FlowerId = update.FlowerId;
record.EventId = update.CategoryId;
record.DateUnixTime = now;
if (update.ByUser == null)
{
record.ByUserId = user.Id;
record.ByUserName = null;
}
else
{
record.ByUserId = null;
record.ByUserName = update.ByUser;
}
record.Memo = update.Memo;
record.Latitude = update.Latitude;
record.Longitude = update.Longitude;
if (update.Photos?.Length > 0)
{
try
{
await ExecuteTransaction(async token =>
{
foreach (var photo in update.Photos)
{
var file = WrapFormFile(photo);
if (file == null)
{
continue;
}
var photos = database.Photos.Where(p => p.RecordId == record.Id).ToList();
if (photos.Count > 0)
{
database.Photos.Where(p => p.RecordId == record.Id).ExecuteDelete();
foreach (var p in photos)
{
DeleteFile(update.FlowerId, p.Path);
}
}
SaveDatabase();
var cover = new PhotoItem
{
OwnerId = user.Id,
FlowerId = update.FlowerId,
RecordId = record.Id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = now
};
AddPhotoItem(cover);
await WriteToFile(update.FlowerId, file, token);
}
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/event/update");
// TODO: Logger
}
}
else
{
SaveDatabase();
}
return Ok(user);
}
/// <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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
[RequestSizeLimit(15 * 1024 * 1024)]
public async Task<ActionResult> UploadPhoto([Required][FromQuery] int id, [Required] IFormFile photo)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
SaveDatabase();
var record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id);
if (record == null)
{
return NotFound($"Record id {id} not found");
}
if (photo.Length > 0)
{
var file = WrapFormFile(photo);
if (file == null)
{
return BadRequest();
}
try
{
await ExecuteTransaction(async token =>
{
var p = new PhotoItem
{
OwnerId = user.Id,
FlowerId = record.FlowerId,
RecordId = id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
AddPhotoItem(p);
await WriteToFile(record.FlowerId, file, token);
});
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/event/add_photo");
// TODO: Logger
}
}
return NoContent();
}
/// <summary>
/// 批量添加事件关联照片,总大小限制 75MB
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/event/add_photos
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
/// photos: IFormFile[]
///
/// </remarks>
/// <param name="id">事件唯一 id</param>
/// <param name="photos">图片集</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_photos", Name = "addEventPhotos")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("multipart/form-data")]
// 5 photos
[RequestSizeLimit(75 * 1024 * 1024)]
public async Task<ActionResult> UploadPhotos([Required][FromQuery] int id, [Required] IFormFile[] photos)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
if (photos == null || photos.Length == 0)
{
return BadRequest();
}
var record = database.Records.SingleOrDefault(r => r.Id == id && r.OwnerId == user.Id);
if (record == null)
{
return NotFound($"Record id {id} not found");
}
SaveDatabase();
try
{
await ExecuteTransaction(async token =>
{
foreach (var photo in photos)
{
if (photo.Length > 0)
{
var file = WrapFormFile(photo) ?? throw new BadHttpRequestException(photo?.FileName ?? string.Empty);
var p = new PhotoItem
{
OwnerId = user.Id,
FlowerId = record.FlowerId,
RecordId = id,
FileType = file.FileType,
FileName = file.Filename,
Path = file.Path,
DateUploadUnixTime = user.ActiveDateUnixTime ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
AddPhotoItem(p);
await WriteToFile(record.FlowerId, file, token);
}
}
});
}
catch (BadHttpRequestException bex)
{
return BadRequest(bex.Message);
}
catch (Exception ex)
{
return Problem(ex.ToString(), "api/event/add_photos");
// TODO: Logger
}
return NoContent();
}
/// <summary>
/// 移除事件关联照片
/// </summary>
/// <remarks>
/// 请求示例:
///
/// DELETE /api/event/remove_photo
/// Authorization: authorization id
///
/// 参数:
///
/// id: int
///
/// </remarks>
/// <param name="id">图片唯一 id</param>
/// <returns>移除成功则返回 HTTP 204</returns>
/// <response code="204">移除成功</response>
/// <response code="401">未找到登录会话或已过期或图片所有者不符</response>
/// <response code="403">用户已禁用</response>
/// <response code="404">未找到关联用户或者照片</response>
[Route("remove_photo", Name = "removeEventPhoto")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpDelete]
public ActionResult<int> RemoveEventPhoto([FromQuery][Required] int id)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
var photo = database.Photos.Find(id);
if (photo == null)
{
return NotFound();
}
var item = database.Flowers.Find(photo.FlowerId);
if (item == null || item.OwnerId != user.Id)
{
return Unauthorized();
}
database.Photos.Remove(photo);
SaveDatabase();
DeleteFile(photo.FlowerId ?? TempId, photo.Path);
return NoContent();
}
/// <summary>
/// 批量移除事件关联的照片
/// </summary>
/// <remarks>
/// 请求示例:
///
/// POST /api/event/remove_photos
/// 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("remove_photos", Name = "removeEventPhotos")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
[Consumes("application/json")]
public ActionResult<int> RemoveEventPhotos([FromBody] int[] ids)
{
var (result, user) = CheckPermission();
if (result != null)
{
return result;
}
if (user == null)
{
return NotFound();
}
if (database.Photos.Any(p => ids.Contains(p.Id) && database.Records.Any(r => r.Id == p.RecordId && r.OwnerId != user.Id)))
{
return Unauthorized();
}
var photos = database.Photos.Where(p => ids.Contains(p.Id)).ToList();
var count = database.Photos.Where(p => ids.Contains(p.Id)).ExecuteDelete();
SaveDatabase();
foreach (var photo in photos)
{
DeleteFile(photo.FlowerId ?? TempId, photo.Path);
}
return Ok(count);
}
/// <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)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[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);
}
}