.
This commit is contained in:
parent
697631c8d3
commit
ac250ea779
@ -1,7 +1,9 @@
|
|||||||
using Blahblah.FlowerStory.Server.Data;
|
using Blahblah.FlowerStory.Server.Data;
|
||||||
using Blahblah.FlowerStory.Server.Data.Model;
|
using Blahblah.FlowerStory.Server.Data.Model;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SkiaSharp;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@ -14,6 +16,8 @@ public abstract partial class BaseController : ControllerBase
|
|||||||
{
|
{
|
||||||
private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden.";
|
private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden.";
|
||||||
|
|
||||||
|
private const int ThumbWidth = 600;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 临时 id
|
/// 临时 id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -23,7 +27,7 @@ public abstract partial class BaseController : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件上传路径
|
/// 文件上传路径
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads");
|
protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(Program.DataPath, "uploads");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 支持的图片文件签名
|
/// 支持的图片文件签名
|
||||||
@ -31,6 +35,7 @@ public abstract partial class BaseController : ControllerBase
|
|||||||
protected static readonly List<byte[]> PhotoSignatures = new()
|
protected static readonly List<byte[]> PhotoSignatures = new()
|
||||||
{
|
{
|
||||||
// jpeg
|
// jpeg
|
||||||
|
new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
|
||||||
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
|
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
|
||||||
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
|
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
|
||||||
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
|
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
|
||||||
@ -195,6 +200,29 @@ public abstract partial class BaseController : ControllerBase
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取嵌入流的数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filename">文件名</param>
|
||||||
|
/// <param name="namespace">命名空间,默认为程序集.wwwroot</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] GetEmbeddedData(string filename, string? @namespace = null)
|
||||||
|
{
|
||||||
|
Assembly asm = Assembly.GetExecutingAssembly();
|
||||||
|
if (string.IsNullOrEmpty(@namespace))
|
||||||
|
{
|
||||||
|
@namespace = typeof(Program).Namespace + ".wwwroot";
|
||||||
|
}
|
||||||
|
using Stream? stream = asm.GetManifestResourceStream($"{@namespace}.{filename}");
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
return Array.Empty<byte>();
|
||||||
|
}
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
stream.CopyTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取文件到 byte 数组
|
/// 读取文件到 byte 数组
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -230,7 +258,7 @@ public abstract partial class BaseController : ControllerBase
|
|||||||
var ext = Path.GetExtension(name);
|
var ext = Path.GetExtension(name);
|
||||||
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
|
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
|
||||||
|
|
||||||
var image = SkiaSharp.SKImage.FromEncodedData(data);
|
var image = SKImage.FromEncodedData(data);
|
||||||
|
|
||||||
return new FileResult
|
return new FileResult
|
||||||
{
|
{
|
||||||
@ -256,17 +284,57 @@ public abstract partial class BaseController : ControllerBase
|
|||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建缩略图
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">图片数据</param>
|
||||||
|
/// <param name="maxWidth">最大宽度</param>
|
||||||
|
/// <param name="quality">缩放质量</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static byte[] CreateThumbnail(byte[] data, int maxWidth = ThumbWidth, SKFilterQuality quality = SKFilterQuality.Medium)
|
||||||
|
{
|
||||||
|
if (maxWidth <= 0)
|
||||||
|
{
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
using var bitmap = SKBitmap.Decode(data);
|
||||||
|
if (maxWidth >= bitmap.Width)
|
||||||
|
{
|
||||||
|
using var enc = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
|
||||||
|
return enc.ToArray();
|
||||||
|
}
|
||||||
|
var height = maxWidth * bitmap.Height / bitmap.Width;
|
||||||
|
using var image = bitmap.Resize(new SKImageInfo(maxWidth, height), quality);
|
||||||
|
using var encode = image.Encode(SKEncodedImageFormat.Jpeg, 80);
|
||||||
|
return encode.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 写入文件到用户的花草目录中
|
/// 写入文件到用户的花草目录中
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fid">花草唯一 id</param>
|
/// <param name="fid">花草唯一 id</param>
|
||||||
/// <param name="file">文件对象</param>
|
/// <param name="file">文件对象</param>
|
||||||
|
/// <param name="thumbnail">创建缩略图</param>
|
||||||
/// <param name="token">取消令牌</param>
|
/// <param name="token">取消令牌</param>
|
||||||
protected static async Task WriteToFile(int fid, FileResult file, CancellationToken token = default)
|
protected async Task WriteToFile(int fid, FileResult file, bool thumbnail = true, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var directory = GetFlowerDirectory(fid, true);
|
var directory = GetFlowerDirectory(fid, true);
|
||||||
var path = Path.Combine(directory, file.Path);
|
var path = Path.Combine(directory, file.Path);
|
||||||
await System.IO.File.WriteAllBytesAsync(path, file.Content, token);
|
await System.IO.File.WriteAllBytesAsync(path, file.Content, token);
|
||||||
|
|
||||||
|
if (thumbnail)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var thumb = CreateThumbnail(file.Content);
|
||||||
|
path = Path.Combine(directory, $"{file.Path}.thumb");
|
||||||
|
await System.IO.File.WriteAllBytesAsync(path, thumb, token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogError(ex, "failed to create thumbnail for flower: {fid}, file: {file}, error: {message}", fid, file.Filename, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -52,7 +52,7 @@ partial class BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取用户头像数据
|
/// 根据用户 uid 获取用户头像数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uid">用户唯一 id</param>
|
/// <param name="uid">用户唯一 id</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@ -61,6 +61,16 @@ partial class BaseController
|
|||||||
return database.Database.SqlQuery<byte[]>($"SELECT \"avatar\" AS \"Value\" FROM \"users\" WHERE \"uid\" = {uid} LIMIT 1").SingleOrDefault();
|
return database.Database.SqlQuery<byte[]>($"SELECT \"avatar\" AS \"Value\" FROM \"users\" WHERE \"uid\" = {uid} LIMIT 1").SingleOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据用户 id 获取用户头像数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">用户 id</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected byte[]? QueryUserAvatar(string id)
|
||||||
|
{
|
||||||
|
return database.Database.SqlQuery<byte[]>($"SELECT \"avatar\" AS \"Value\" FROM \"users\" WHERE \"id\" = {id} LIMIT 1").SingleOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 移除用户头像
|
/// 移除用户头像
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -316,7 +316,7 @@ public class EventApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(p);
|
AddPhotoItem(p);
|
||||||
|
|
||||||
await WriteToFile(@event.FlowerId, file, token);
|
await WriteToFile(@event.FlowerId, file, token: token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -443,7 +443,7 @@ public class EventApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(cover);
|
AddPhotoItem(cover);
|
||||||
|
|
||||||
await WriteToFile(update.FlowerId, file, token);
|
await WriteToFile(update.FlowerId, file, token: token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -541,7 +541,7 @@ public class EventApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(p);
|
AddPhotoItem(p);
|
||||||
|
|
||||||
await WriteToFile(record.FlowerId, file, token);
|
await WriteToFile(record.FlowerId, file, token: token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -639,7 +639,7 @@ public class EventApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(p);
|
AddPhotoItem(p);
|
||||||
|
|
||||||
await WriteToFile(record.FlowerId, file, token);
|
await WriteToFile(record.FlowerId, file, token: token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -235,7 +235,7 @@ public class FlowerApiController : BaseController
|
|||||||
f.Photos = database.Photos.Where(p => p.FlowerId == f.Id && p.RecordId == null).ToList();
|
f.Photos = database.Photos.Where(p => p.FlowerId == f.Id && p.RecordId == null).ToList();
|
||||||
foreach (var photo in f.Photos)
|
foreach (var photo in f.Photos)
|
||||||
{
|
{
|
||||||
photo.Url = $"photo/flower/{f.Id}/{photo.Path}";
|
photo.Url = $"photo/flower/{f.Id}/{photo.Path}?thumb=1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -532,7 +532,7 @@ public class FlowerApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(cover);
|
AddPhotoItem(cover);
|
||||||
|
|
||||||
await WriteToFile(item.Id, file, token);
|
await WriteToFile(item.Id, file, token: token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -753,7 +753,7 @@ public class FlowerApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(cover);
|
AddPhotoItem(cover);
|
||||||
|
|
||||||
await WriteToFile(update.Id, file, token);
|
await WriteToFile(update.Id, file, token: token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -854,7 +854,7 @@ public class FlowerApiController : BaseController
|
|||||||
};
|
};
|
||||||
AddPhotoItem(cover);
|
AddPhotoItem(cover);
|
||||||
|
|
||||||
await WriteToFile(param.Id, file, token);
|
await WriteToFile(param.Id, file, token: token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -8,35 +8,36 @@ namespace Blahblah.FlowerStory.Server.Controller;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 图片相关服务
|
/// 图片相关服务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Produces("image/png")]
|
|
||||||
[Route("photo")]
|
[Route("photo")]
|
||||||
public class ImageController : BaseController
|
public class ImageController : BaseController
|
||||||
{
|
{
|
||||||
|
static byte[]? emptyAvatar;
|
||||||
|
|
||||||
|
static byte[] EmptyAvatar => emptyAvatar ??= GetEmbeddedData("image.avatar.jpg");
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ImageController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
|
public ImageController(FlowerDatabase database, ILogger<BaseController>? logger = null) : base(database, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 请求用户头像
|
/// 请求自己的头像
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// 请求示例:
|
/// 请求示例:
|
||||||
///
|
///
|
||||||
/// GET /photo/avatar
|
/// GET /photo/my_avatar
|
||||||
/// Authorization: authorization id
|
/// Authorization: authorization id
|
||||||
///
|
///
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <returns>认证通过则显示用户头像</returns>
|
/// <returns>认证通过则显示自己的头像</returns>
|
||||||
/// <response code="200">返回头像</response>
|
/// <response code="200">返回头像</response>
|
||||||
/// <response code="401">认证失败</response>
|
/// <response code="401">认证失败</response>
|
||||||
/// <response code="404">未找到头像</response>
|
[Route("my_avatar", Name = "getMyAvatar")]
|
||||||
[Route("avatar", Name = "getAvatar")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public ActionResult GetUserAvatar()
|
public ActionResult GetMyAvatar()
|
||||||
{
|
{
|
||||||
var (result, token) = CheckToken();
|
var (result, token) = CheckToken();
|
||||||
if (result != null)
|
if (result != null)
|
||||||
@ -48,11 +49,47 @@ public class ImageController : BaseController
|
|||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
}
|
}
|
||||||
var avatar = QueryUserAvatar(token.UserId);
|
var avatar = QueryUserAvatar(token.UserId);
|
||||||
if (avatar == null)
|
if (avatar?.Length > 0)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return File(avatar, "image/jpeg");
|
||||||
}
|
}
|
||||||
return File(avatar, "image/png");
|
return File(EmptyAvatar, "image/jpeg");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求用户头像
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 请求示例:
|
||||||
|
///
|
||||||
|
/// GET /photo/avatar/2.jpg
|
||||||
|
///
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="uid">用户唯一 id</param>
|
||||||
|
/// <returns>认证通过则显示用户头像</returns>
|
||||||
|
/// <response code="200">返回头像</response>
|
||||||
|
/// <response code="401">认证失败</response>
|
||||||
|
[Route("avatar/{uid}.jpg", Name = "getAvatar")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[HttpGet]
|
||||||
|
public ActionResult GetAvatar([Required] int uid)
|
||||||
|
{
|
||||||
|
//var (result, token) = CheckToken();
|
||||||
|
//if (result != null)
|
||||||
|
//{
|
||||||
|
// return result;
|
||||||
|
//}
|
||||||
|
//if (token == null)
|
||||||
|
//{
|
||||||
|
// return Unauthorized();
|
||||||
|
//}
|
||||||
|
var avatar = QueryUserAvatar(uid);
|
||||||
|
if (avatar?.Length > 0)
|
||||||
|
{
|
||||||
|
return File(avatar, "image/jpeg");
|
||||||
|
}
|
||||||
|
return File(EmptyAvatar, "image/jpeg");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -61,11 +98,12 @@ public class ImageController : BaseController
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// 请求示例:
|
/// 请求示例:
|
||||||
///
|
///
|
||||||
/// GET /photo/flower/{fid}/{name}
|
/// GET /photo/flower/1/test.jpg
|
||||||
///
|
///
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="fid">花草唯一 id</param>
|
/// <param name="fid">花草唯一 id</param>
|
||||||
/// <param name="name">照片名称</param>
|
/// <param name="name">照片名称</param>
|
||||||
|
/// <param name="thumb">是否为缩略图</param>
|
||||||
/// <returns>认证通过则显示花草照片</returns>
|
/// <returns>认证通过则显示花草照片</returns>
|
||||||
/// <response code="200">返回花草照片</response>
|
/// <response code="200">返回花草照片</response>
|
||||||
/// <response code="401">认证失败</response>
|
/// <response code="401">认证失败</response>
|
||||||
@ -75,7 +113,7 @@ public class ImageController : BaseController
|
|||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult> GetFlowerPhoto([Required] int fid, [Required] string name)
|
public async Task<ActionResult> GetFlowerPhoto([Required] int fid, [Required] string name, [FromQuery] string? thumb = null)
|
||||||
{
|
{
|
||||||
//var (result, token) = CheckToken();
|
//var (result, token) = CheckToken();
|
||||||
//if (result != null)
|
//if (result != null)
|
||||||
@ -87,7 +125,7 @@ public class ImageController : BaseController
|
|||||||
// return Unauthorized();
|
// return Unauthorized();
|
||||||
//}
|
//}
|
||||||
|
|
||||||
#if !DEBUG
|
#if PRODUCTION
|
||||||
var referrer = Request.Headers.Referer.ToString();
|
var referrer = Request.Headers.Referer.ToString();
|
||||||
if (string.IsNullOrEmpty(referrer))
|
if (string.IsNullOrEmpty(referrer))
|
||||||
{
|
{
|
||||||
@ -98,12 +136,41 @@ public class ImageController : BaseController
|
|||||||
return Forbid();
|
return Forbid();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString(), WebUtility.UrlEncode(name));
|
var filename = WebUtility.UrlEncode(name);
|
||||||
if (System.IO.File.Exists(path))
|
var directory = Path.Combine(Program.DataPath, "uploads", fid.ToString());
|
||||||
|
var original = Path.Combine(directory, filename);
|
||||||
|
var thumbnail = Path.Combine(directory, $"{filename}.thumb");
|
||||||
|
if (!string.IsNullOrEmpty(thumb))
|
||||||
{
|
{
|
||||||
var data = await System.IO.File.ReadAllBytesAsync(path);
|
if (System.IO.File.Exists(thumbnail))
|
||||||
var ext = Path.GetExtension(path).ToLower();
|
{
|
||||||
return File(data, ext == ".png" ? "image/png" : "image/jpeg");
|
var thumbnailData = await System.IO.File.ReadAllBytesAsync(thumbnail);
|
||||||
|
return File(thumbnailData, "image/jpeg");
|
||||||
|
}
|
||||||
|
else if (System.IO.File.Exists(original))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var originalData = await System.IO.File.ReadAllBytesAsync(original);
|
||||||
|
var thumbnailData = CreateThumbnail(originalData);
|
||||||
|
await System.IO.File.WriteAllBytesAsync(thumbnail, thumbnailData);
|
||||||
|
return File(thumbnailData, "image/jpeg");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger?.LogWarning(ex, "failed to create thumbnail for flower: {fid}, name: {name}, error: {message}", fid, name, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
if (System.IO.File.Exists(original))
|
||||||
|
{
|
||||||
|
var data = await System.IO.File.ReadAllBytesAsync(original);
|
||||||
|
return Path.GetExtension(original).ToLower() switch
|
||||||
|
{
|
||||||
|
".png" => File(data, "image/png"),
|
||||||
|
_ => File(data, "image/jpeg"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Blahblah.FlowerStory.Server.Data;
|
using Blahblah.FlowerStory.Server.Data;
|
||||||
using Blahblah.FlowerStory.Server.Data.Model;
|
using Blahblah.FlowerStory.Server.Data.Model;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
@ -81,6 +82,9 @@ public partial class UserApiController : BaseController
|
|||||||
clientApp = "browser";
|
clientApp = "browser";
|
||||||
expires = 20 * 60; // 20 mins
|
expires = 20 * 60; // 20 mins
|
||||||
}
|
}
|
||||||
|
|
||||||
|
database.Tokens.Where(t => t.UserId == user.Id && t.ClientApp == clientApp).ExecuteDelete();
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var token = new TokenItem
|
var token = new TokenItem
|
||||||
{
|
{
|
||||||
@ -184,26 +188,31 @@ public partial class UserApiController : BaseController
|
|||||||
/// 请求示例:
|
/// 请求示例:
|
||||||
///
|
///
|
||||||
/// POST /api/user/register
|
/// POST /api/user/register
|
||||||
/// {
|
///
|
||||||
/// "id": "blahblah",
|
/// 参数:
|
||||||
/// "password": "pwd123",
|
///
|
||||||
/// "userName": "Blah blah",
|
/// id: "blahblah"
|
||||||
/// "email": "blah@example.com",
|
/// password: "pwd123"
|
||||||
/// "mobile": "18012345678"
|
/// name: "Blah blah"
|
||||||
/// }
|
/// email: "blah@example.com"
|
||||||
|
/// mobile: "18012345678"
|
||||||
|
/// avatar: <avatar>
|
||||||
///
|
///
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="user">注册参数</param>
|
/// <param name="user">注册参数</param>
|
||||||
/// <returns>成功注册则返回已注册的用户对象</returns>
|
/// <returns>成功注册则返回已注册的用户对象</returns>
|
||||||
/// <response code="200">返回已注册的用户对象</response>
|
/// <response code="200">返回已注册的用户对象</response>
|
||||||
|
/// <response code="400">用户头像格式非法</response>
|
||||||
/// <response code="500">用户重复或其他服务器错误</response>
|
/// <response code="500">用户重复或其他服务器错误</response>
|
||||||
[Route("register", Name = "register")]
|
[Route("register", Name = "register")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesErrorResponseType(typeof(ErrorResponse))]
|
[ProducesErrorResponseType(typeof(ErrorResponse))]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Consumes("application/json")]
|
[Consumes("multipart/form-data")]
|
||||||
public ActionResult<UserItem> Register([FromBody] UserParameter user)
|
[RequestSizeLimit(15 * 1024 * 1024)]
|
||||||
|
public ActionResult<UserItem> Register([FromForm] UserParameter user)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
logger?.LogInformation("user register, {user}", user);
|
logger?.LogInformation("user register, {user}", user);
|
||||||
@ -214,6 +223,21 @@ public partial class UserApiController : BaseController
|
|||||||
return Problem("duplicateUser", "api/user/register");
|
return Problem("duplicateUser", "api/user/register");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[]? data;
|
||||||
|
if (user.Avatar != null)
|
||||||
|
{
|
||||||
|
var avatar = WrapFormFile(user.Avatar);
|
||||||
|
if (avatar == null)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
data = CreateThumbnail(avatar.Content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
var item = new UserItem
|
var item = new UserItem
|
||||||
{
|
{
|
||||||
@ -224,7 +248,8 @@ public partial class UserApiController : BaseController
|
|||||||
ActiveDateUnixTime = now,
|
ActiveDateUnixTime = now,
|
||||||
Name = user.UserName,
|
Name = user.UserName,
|
||||||
Email = user.Email,
|
Email = user.Email,
|
||||||
Mobile = user.Mobile
|
Mobile = user.Mobile,
|
||||||
|
Avatar = data
|
||||||
};
|
};
|
||||||
database.Users.Add(item);
|
database.Users.Add(item);
|
||||||
SaveDatabase();
|
SaveDatabase();
|
||||||
@ -281,30 +306,35 @@ public partial class UserApiController : BaseController
|
|||||||
///
|
///
|
||||||
/// PUT /api/user/update
|
/// PUT /api/user/update
|
||||||
/// Authorization: authorization id
|
/// Authorization: authorization id
|
||||||
/// {
|
///
|
||||||
/// "userName": "Blah blah",
|
/// 参数:
|
||||||
/// "email": "blah@example.com",
|
///
|
||||||
/// "mobile": "18012345678"
|
/// name": "Blah blah"
|
||||||
/// }
|
/// email": "blah@example.com"
|
||||||
|
/// mobile": "18012345678",
|
||||||
|
/// avatar: <avatar>
|
||||||
///
|
///
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="update">修改参数</param>
|
/// <param name="update">修改参数</param>
|
||||||
/// <returns>修改成功则返回已修改的用户对象</returns>
|
/// <returns>修改成功则返回已修改的用户对象</returns>
|
||||||
/// <response code="200">返回已修改的用户对象</response>
|
/// <response code="200">返回已修改的用户对象</response>
|
||||||
|
/// <response code="400">用户头像格式非法</response>
|
||||||
/// <response code="401">未找到登录会话或已过期</response>
|
/// <response code="401">未找到登录会话或已过期</response>
|
||||||
/// <response code="403">用户已禁用</response>
|
/// <response code="403">用户已禁用</response>
|
||||||
/// <response code="404">未找到关联用户</response>
|
/// <response code="404">未找到关联用户</response>
|
||||||
/// <response code="413">提交正文过大</response>
|
/// <response code="413">提交正文过大</response>
|
||||||
[Route("update", Name = "updateProfile")]
|
[Route("update", Name = "updateProfile")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
|
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
|
||||||
[ProducesErrorResponseType(typeof(ErrorResponse))]
|
[ProducesErrorResponseType(typeof(ErrorResponse))]
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
[Consumes("application/json")]
|
[Consumes("multipart/form-data")]
|
||||||
public ActionResult<UserItem> Update([FromBody] UpdateParameter update)
|
[RequestSizeLimit(15 * 1024 * 1024)]
|
||||||
|
public ActionResult<UserItem> Update([FromForm] UpdateParameter update)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
logger?.LogInformation("user update, {user}", update);
|
logger?.LogInformation("user update, {user}", update);
|
||||||
@ -319,6 +349,17 @@ public partial class UserApiController : BaseController
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (update.Avatar != null)
|
||||||
|
{
|
||||||
|
var avatar = WrapFormFile(update.Avatar);
|
||||||
|
if (avatar == null)
|
||||||
|
{
|
||||||
|
return BadRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Avatar = CreateThumbnail(avatar.Content);
|
||||||
|
}
|
||||||
|
|
||||||
user.Name = update.UserName;
|
user.Name = update.UserName;
|
||||||
user.Email = update.Email;
|
user.Email = update.Email;
|
||||||
user.Mobile = update.Mobile;
|
user.Mobile = update.Mobile;
|
||||||
@ -382,7 +423,7 @@ public partial class UserApiController : BaseController
|
|||||||
{
|
{
|
||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
user.Avatar = file.Content;
|
user.Avatar = CreateThumbnail(file.Content);
|
||||||
}
|
}
|
||||||
SaveDatabase();
|
SaveDatabase();
|
||||||
|
|
||||||
@ -429,7 +470,7 @@ public partial class UserApiController : BaseController
|
|||||||
return Ok(count);
|
return Ok(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
//#if DEBUG
|
#if !PRODUCTION
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #DEBUG 获取所有用户
|
/// #DEBUG 获取所有用户
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -453,5 +494,5 @@ public partial class UserApiController : BaseController
|
|||||||
{
|
{
|
||||||
return Ok(database.Tokens.ToArray());
|
return Ok(database.Tokens.ToArray());
|
||||||
}
|
}
|
||||||
//#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Blahblah.FlowerStory.Server.Controller;
|
namespace Blahblah.FlowerStory.Server.Controller;
|
||||||
|
|
||||||
@ -33,12 +34,14 @@ public record UserParameter : UpdateParameter
|
|||||||
/// 用户 id
|
/// 用户 id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
|
[FromForm(Name = "id")]
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 密码
|
/// 密码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
|
[FromForm(Name = "password")]
|
||||||
public required string Password { get; init; }
|
public required string Password { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,15 +54,24 @@ public record UpdateParameter
|
|||||||
/// 用户名
|
/// 用户名
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
|
[FromForm(Name = "name")]
|
||||||
public required string UserName { get; init; }
|
public required string UserName { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 邮箱
|
/// 邮箱
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[FromForm(Name = "email")]
|
||||||
public string? Email { get; init; }
|
public string? Email { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 联系电话
|
/// 联系电话
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[FromForm(Name = "mobile")]
|
||||||
public string? Mobile { get; init; }
|
public string? Mobile { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户头像
|
||||||
|
/// </summary>
|
||||||
|
[FromForm(Name = "avatar")]
|
||||||
|
public IFormFile? Avatar { get; init; }
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0
|
FROM mcr.microsoft.com/dotnet/aspnet:7.0
|
||||||
|
RUN apt-get update && apt-get install -y libfontconfig1
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 80
|
EXPOSE 5180
|
||||||
|
|
||||||
|
ARG UID=1026
|
||||||
|
ARG GID=100
|
||||||
|
RUN useradd -m -u ${UID} tsanie
|
||||||
|
USER ${UID}:${GID}
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "Server.dll"]
|
ENTRYPOINT ["dotnet", "Server.dll"]
|
@ -11,7 +11,14 @@ public class Program
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public const string ProjectName = "Flower Story";
|
public const string ProjectName = "Flower Story";
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public const string Version = "1.0.807";
|
public const string Version = "1.1.808";
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public static string DataPath =>
|
||||||
|
#if DEBUG
|
||||||
|
AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
#else
|
||||||
|
"/data";
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
@ -56,7 +63,9 @@ public class Program
|
|||||||
options.IncludeXmlComments(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Server.xml"), true);
|
options.IncludeXmlComments(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Server.xml"), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddDbContext<FlowerDatabase>(options => options.UseSqlite("DataSource=flower.db;Cache=Shared"));
|
var dbPath = Path.Combine(DataPath, "flower.db");
|
||||||
|
builder.Services.AddDbContext<FlowerDatabase>(options => options.UseSqlite($"DataSource={dbPath};Cache=Shared"));
|
||||||
|
|
||||||
builder.Services.AddScoped<SwaggerGenerator>();
|
builder.Services.AddScoped<SwaggerGenerator>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
@ -10,6 +10,14 @@
|
|||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Remove="wwwroot\image\avatar.jpg" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="wwwroot\image\avatar.jpg" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.9" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
|
||||||
@ -18,6 +26,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
<PackageReference Include="SkiaSharp" Version="2.88.3" />
|
||||||
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@ -25,6 +34,9 @@
|
|||||||
<None Update="Dockerfile">
|
<None Update="Dockerfile">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="start">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="flower.db">
|
<None Update="flower.db">
|
||||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://+:5180"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
}
|
||||||
|
10
Server/start
Normal file
10
Server/start
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
docker stop flower-server
|
||||||
|
docker rm flower-server
|
||||||
|
docker rmi flower-server-image
|
||||||
|
|
||||||
|
docker build -t flower-server-image .
|
||||||
|
docker run -d -p 7080:5180 -v /volume2/docker/flower-data:/data --name flower-server flower-server-image:latest
|
||||||
|
|
||||||
|
docker logs -f flower-server
|
BIN
Server/wwwroot/image/avatar.jpg
Normal file
BIN
Server/wwwroot/image/avatar.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Loading…
x
Reference in New Issue
Block a user