feat: 文传易匿名临时文件传输站初始版本

- 后端 .NET 8 + FreeSql + 阿里云 OSS,支持共享/私密/标签三种文件模式
- 前端 Vue3 + Vite + TDesign,上传/取件/管理三页
- 取件码/二维码/海报合成(含保存二维码为图片)
- deploy/ 部署文档与一键 FTP 发布脚本
- appsettings.json 含真实凭据,已 gitignore 不入库
This commit is contained in:
2026-08-24 00:03:28 +08:00
commit 1bfc72f9cb
40 changed files with 6849 additions and 0 deletions
@@ -0,0 +1,68 @@
using Aliyun.OSS;
namespace WenChuanyi.Api.Services;
/// <summary>OSS 存储服务:对象键构造、流式上传/下载/删除(后端中转,前端不接触凭据)</summary>
public class OssStorageService
{
private readonly OssClient _client;
private readonly string _bucket;
private readonly string _baseKey;
private readonly ILogger<OssStorageService> _logger;
public OssStorageService(IConfiguration cfg, ILogger<OssStorageService> logger)
{
var endpoint = cfg["Oss:Endpoint"] ?? throw new InvalidOperationException("缺少配置 Oss:Endpoint");
var ak = cfg["Oss:AccessKeyId"] ?? throw new InvalidOperationException("缺少配置 Oss:AccessKeyId");
var sk = cfg["Oss:AccessKeySecret"] ?? throw new InvalidOperationException("缺少配置 Oss:AccessKeySecret");
_bucket = cfg["Oss:Bucket"] ?? throw new InvalidOperationException("缺少配置 Oss:Bucket");
_baseKey = cfg["Oss:BaseKey"] ?? "文传易2026";
_client = new OssClient(endpoint, ak, sk);
_logger = logger;
}
/// <summary>构造对象键:文传易2026/{yyyyMM}/{Guid}{ext}(按上传月份建子目录)</summary>
public string BuildObjectKey(string originalName)
{
var month = DateTime.Now.ToString("yyyyMM");
var ext = Path.GetExtension(originalName).ToLowerInvariant();
return $"{_baseKey}/{month}/{Guid.NewGuid():N}{ext}";
}
/// <summary>流式上传(不落本地、不整体读内存)。ContentType 会去除参数部分(如 ";charset=utf-8"),
/// 因为 OSS SDK 对带参数的 Content-Type 签名会与服务端校验不一致,导致 403 签名不匹配。</summary>
public async Task UploadAsync(string objectKey, Stream stream, string contentType)
{
var safeContentType = (contentType ?? "application/octet-stream").Split(';')[0].Trim();
if (string.IsNullOrEmpty(safeContentType)) safeContentType = "application/octet-stream";
var req = new PutObjectRequest(_bucket, objectKey, stream);
req.Metadata = new ObjectMetadata { ContentType = safeContentType };
await Task.Run(() => _client.PutObject(req)).ConfigureAwait(false);
}
/// <summary>获取对象(支持 Rangeend 为 -1 表示到文件末尾)</summary>
public OssObject GetObject(string objectKey, long? start = null, long? end = null)
{
var req = new GetObjectRequest(_bucket, objectKey);
if (start.HasValue || end.HasValue)
{
req.SetRange(start ?? 0, end ?? -1);
}
return _client.GetObject(req);
}
/// <summary>删除对象(失败不抛出,由调用方记录日志)</summary>
public bool TryDeleteObject(string objectKey)
{
try
{
_client.DeleteObject(_bucket, objectKey);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "OSS 对象删除失败:{ObjectKey}", objectKey);
return false;
}
}
}