This commit is contained in:
owenchen committed 2026-10-20 15:44:56 +08:00
1 parent 9b01c4a57a
commit 6417562c4f
37 files changed
+2767 -202

No files matched your search

@@ -0,0 +1,62 @@
using CloudBuilder.Master.Entity;
using System;
namespace CloudBuilder.Master.Data
{
/// <summary>
/// 族谱全系/小系导出的明文数据(按 D1-D8 决策):
/// 后端按权限闸裁剪后下发,前端用「用户密码⊕系统因子」派生密钥加密成 .fm 信封,落盘/上传交换目录
/// </summary>
public class FamilyTreeExchangeData
{
/// <summary>ALL=全系 / SUB=小系</summary>
public string Scope { get; set; } = FamilyTreeExchangeScope.ALL;
public FamilyTreeEntity FamilyTree { get; set; }
public FamilyPersonEntity[] Persons { get; set; } = [];
public FamilyTreeSpouseMapEntity[] SpouseMaps { get; set; } = [];
public FamilyTreeChildrenMapEntity[] ChildrenMaps { get; set; } = [];
/// <summary>
/// 本谱权限行快照(仅备份 BK 写入,用于还原时权限随谱回滚形成绝对闭环;
/// .fm 交换文件恒为空——防成员账号名随导出文件外泄,也防外部文件携权限行劫持本谱)
/// </summary>
public FamilyTreePermissionEntity[] Permissions { get; set; } = [];
public int PersonCount => Persons?.Length ?? 0;
/// <summary>SUB 导出时的锚点人号(二期支系替换导入靠它验明同一人;ALL 为空)</summary>
public string AnchorPersonId { get; set; } = "";
/// <summary>导出发起人(service.UserId)</summary>
public string ExportedBy { get; set; } = "";
public DateTime ExportedAt { get; set; } = DateTime.Now;
}
/// <summary>交换目录下 .fm 密文文件的列表信息(文件名按 D1 规则反解析出 scope/treeId/锚点)</summary>
public class FamilyTreeExchangeFileInfo
{
public string FileName { get; set; } = "";
public long Size { get; set; }
public DateTime LastModified { get; set; }
public string Scope { get; set; } = "";
public string TreeId { get; set; } = "";
public string AnchorPersonId { get; set; } = "";
}
/// <summary>导出范围常量(后端无 CloudBuilder.Master.Service 引用,DTO 与 Policy 各留一份避免工程反向依赖)</summary>
public static class FamilyTreeExchangeScope
{
public const string ALL = "ALL";
public const string SUB = "SUB";
}
}
@@ -0,0 +1,33 @@
using CloudBuilder.Core.Authorization;
using CloudBuilder.Core.Service;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CloudBuilder.Master.Data
{
/// <summary>
/// dashboard 网盘用量卡片数据:当前登录用户的类型、头像、已用/总额度(KB)、百分比。
/// 用量取自 dms_file_usage_view(ower_id=本人 且非目录),额度按 user_type 从 appsettings:NetDiskQuota 解析。
/// </summary>
[TypeScript]
public class StorageUsageData
{
/// <summary>用户类型码:U=未认证 / N=普通 / V=VIP / S=SVIP(名称由前端 i18n 映射)</summary>
public string UserType { get; set; } = "";
/// <summary>头像地址(security_user.user_icon_img_path,当前无上传链路多为空,前端占位兜底)</summary>
public string AvatarUrl { get; set; } = "";
/// <summary>已用容量(KB)</summary>
public long UsedKB { get; set; }
/// <summary>额度总量(KB),0 表示未配置/不限</summary>
public long QuotaKB { get; set; }
/// <summary>已用百分比(额度为 0 时记 0;可大于 100 表示超额)</summary>
public int Percent { get; set; }
}
}
@@ -114,7 +114,10 @@ builder.Entity<FamilyTreePermissionUserViewEntity>(eb => { eb.HasNoKey(); });
//***END***
builder.Entity<DmsFileUsageViewEntity>(eb => { eb.HasNoKey(); });
//***END***
}
}
}
@@ -0,0 +1,153 @@
using CloudBuilder.Core.Authorization;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CloudBuilder.Master.Entity
{
[TypeScript]
[Table("dms_file_usage_view")]
public class DmsFileUsageViewEntity : ViewEntityBase
{
public DmsFileUsageViewEntity()
{
}
[Column("ower_id")]
public string OwerId { get; set; }
[Column("file_count")]
public long FileCount { get; set; }
[Column("used_kb")]
public long UsedKb { get; set; }
public const string OWER_ID = "OwerId";
public const string FILE_COUNT = "FileCount";
public const string USED_KB = "UsedKb";
public const string DB_NAME_DMS_FILE_USAGE_VIEW = "dms_file_usage_view";
public const string DB_NAME_FIELDS="ower_id,file_count,used_kb";
public const string DB_OWER_ID = "ower_id";
public const string DB_FILE_COUNT = "file_count";
public const string DB_USED_KB = "used_kb";
private string ProviderName { get; set; }
public static DmsFileUsageViewEntity GetInstance(string providerName)
{
return new DmsFileUsageViewEntity() { ProviderName = providerName };
}
public virtual void CopyFrom(DmsFileUsageViewEntity source)
{
CopyFrom(source, true);
}
public virtual void CopyFrom(DmsFileUsageViewEntity source, bool includeSystemFields)
{
if (source == null)
{
throw new NullReferenceException("Source entity is null.");
}
this.OwerId = source.OwerId;
this.FileCount = source.FileCount;
this.UsedKb = source.UsedKb;
if (includeSystemFields)
{
}
}
public override string GetSelectFrom()
{
if (ProviderName.Contains("MySql"))
{
return GetSelectFromByMySql();
}
else if (ProviderName.Contains("SqlServer"))
{
return GetSelectFromByBySqlServer();
}
else
{
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
}
}
public string GetSelectFromByMySql()
{
return $" select {DB_NAME_FIELDS} from { DB_NAME_DMS_FILE_USAGE_VIEW} with(nolock) ";
}
public string GetSelectFromByBySqlServer()
{
return $" select {DB_NAME_FIELDS} from { DB_NAME_DMS_FILE_USAGE_VIEW} ";
}
public override string GetSelectCountFrom(string sqlWhere)
{
if (ProviderName.Contains("MySql"))
{
return GetSelectCountFromByMySql(sqlWhere);
}
else if (ProviderName.Contains("SqlServer"))
{
return GetSelectCountFromBySqlServer(sqlWhere);
}
else
{
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
}
}
public string GetSelectCountFromByMySql(string sqlWhere)
{
return $" select count(*) from { DB_NAME_DMS_FILE_USAGE_VIEW} where 1=1 {sqlWhere}";
}
public string GetSelectCountFromBySqlServer(string sqlWhere)
{
return $" select count(*) from { DB_NAME_DMS_FILE_USAGE_VIEW} with(nolock) where 1=1 {sqlWhere}";
}
public override string GetSelectTopFrom(int limit, int start, string orderby, string orderbytag, string sqlWhere)
{
if (ProviderName.Contains("MySql"))
{
return GetSelectTopFromByMySql(limit, start, orderby, orderbytag, sqlWhere);
}
else if (ProviderName.Contains("SqlServer"))
{
return GetSelectTopFromBySqlServer(limit, start, orderby, orderbytag, sqlWhere);
}
else
{
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
}
}
public string GetSelectTopFromByMySql(int limit, int start,string orderby,string orderbytag, string sqlWhere)
{
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_OWER_ID ; int offset = limit * (start - 1);
return $" select {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from { DB_NAME_DMS_FILE_USAGE_VIEW} where 1=1 {sqlWhere} ) a where num> {offset} LIMIT {limit} ";
}
public string GetSelectTopFromBySqlServer(int limit, int start,string orderby,string orderbytag, string sqlWhere)
{
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_OWER_ID ; string top = limit > 0 ? $"top {limit}" : "";
return $" select {top} {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from { DB_NAME_DMS_FILE_USAGE_VIEW} with(nolock) where 1=1 {sqlWhere} ) a where num> {limit * (start - 1)}";
}
public DmsFileUsageViewEntity Clone()
{
DmsFileUsageViewEntity result = new DmsFileUsageViewEntity();
result.CopyFrom(this);
return result;
}
}
}
@@ -0,0 +1,209 @@
using CloudBuilder.Core.Authorization;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CloudBuilder.Master.Entity
{
[TypeScript]
[Table("family_tree_file_log")]
public class FamilyTreeFileLogEntity : ViewEntityBase
{
public FamilyTreeFileLogEntity()
{
}
[Column("created_datetime")]
public DateTime CreatedDatetime { get; set; }
[Column("created_by")]
public string CreatedBy { get; set; }
[Column("updated_datetime")]
public DateTime UpdatedDatetime { get; set; }
[Column("updated_by")]
public string UpdatedBy { get; set; }
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("log_id")]
public int LogId { get; set; }
[Column("tree_id")]
public string TreeId { get; set; }
[Column("action")]
public string Action { get; set; }
[Column("scope")]
public string Scope { get; set; }
[Column("anchor_person_id")]
public string? AnchorPersonId { get; set; }
[Column("file_name")]
public string? FileName { get; set; }
[Column("person_count")]
public int PersonCount { get; set; }
[Column("result")]
public string Result { get; set; }
[Column("message")]
public string? Message { get; set; }
public const string CREATED_DATETIME = "CreatedDatetime";
public const string CREATED_BY = "CreatedBy";
public const string UPDATED_DATETIME = "UpdatedDatetime";
public const string UPDATED_BY = "UpdatedBy";
public const string LOG_ID = "LogId";
public const string TREE_ID = "TreeId";
public const string ACTION = "Action";
public const string SCOPE = "Scope";
public const string ANCHOR_PERSON_ID = "AnchorPersonId";
public const string FILE_NAME = "FileName";
public const string PERSON_COUNT = "PersonCount";
public const string RESULT = "Result";
public const string MESSAGE = "Message";
public const string DB_NAME_FAMILY_TREE_FILE_LOG = "family_tree_file_log";
public const string DB_NAME_FIELDS="created_datetime,created_by,updated_datetime,updated_by,log_id,tree_id,action,scope,anchor_person_id,file_name,person_count,result,message";
public const string DB_CREATED_DATETIME = "created_datetime";
public const string DB_CREATED_BY = "created_by";
public const string DB_UPDATED_DATETIME = "updated_datetime";
public const string DB_UPDATED_BY = "updated_by";
public const string DB_LOG_ID = "log_id";
public const string DB_TREE_ID = "tree_id";
public const string DB_ACTION = "action";
public const string DB_SCOPE = "scope";
public const string DB_ANCHOR_PERSON_ID = "anchor_person_id";
public const string DB_FILE_NAME = "file_name";
public const string DB_PERSON_COUNT = "person_count";
public const string DB_RESULT = "result";
public const string DB_MESSAGE = "message";
private string ProviderName { get; set; }
public static FamilyTreeFileLogEntity GetInstance(string providerName)
{
return new FamilyTreeFileLogEntity() { ProviderName = providerName };
}
public virtual void CopyFrom(FamilyTreeFileLogEntity source)
{
CopyFrom(source, true);
}
public virtual void CopyFrom(FamilyTreeFileLogEntity source, bool includeSystemFields)
{
if (source == null)
{
throw new NullReferenceException("Source entity is null.");
}
this.CreatedDatetime = source.CreatedDatetime;
this.CreatedBy = source.CreatedBy;
this.UpdatedDatetime = source.UpdatedDatetime;
this.UpdatedBy = source.UpdatedBy;
this.LogId = source.LogId;
this.TreeId = source.TreeId;
this.Action = source.Action;
this.Scope = source.Scope;
this.AnchorPersonId = source.AnchorPersonId;
this.FileName = source.FileName;
this.PersonCount = source.PersonCount;
this.Result = source.Result;
this.Message = source.Message;
if (includeSystemFields)
{
this.CreatedDatetime = source.CreatedDatetime;
this.CreatedBy = source.CreatedBy;
this.UpdatedDatetime = source.UpdatedDatetime;
this.UpdatedBy = source.UpdatedBy;
}
}
public override string GetSelectFrom()
{
if (ProviderName.Contains("MySql"))
{
return GetSelectFromByMySql();
}
else if (ProviderName.Contains("SqlServer"))
{
return GetSelectFromByBySqlServer();
}
else
{
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
}
}
public string GetSelectFromByMySql()
{
return $" select {DB_NAME_FIELDS} from { DB_NAME_FAMILY_TREE_FILE_LOG} with(nolock) ";
}
public string GetSelectFromByBySqlServer()
{
return $" select {DB_NAME_FIELDS} from { DB_NAME_FAMILY_TREE_FILE_LOG} ";
}
public override string GetSelectCountFrom(string sqlWhere)
{
if (ProviderName.Contains("MySql"))
{
return GetSelectCountFromByMySql(sqlWhere);
}
else if (ProviderName.Contains("SqlServer"))
{
return GetSelectCountFromBySqlServer(sqlWhere);
}
else
{
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
}
}
public string GetSelectCountFromByMySql(string sqlWhere)
{
return $" select count(*) from { DB_NAME_FAMILY_TREE_FILE_LOG} where 1=1 {sqlWhere}";
}
public string GetSelectCountFromBySqlServer(string sqlWhere)
{
return $" select count(*) from { DB_NAME_FAMILY_TREE_FILE_LOG} with(nolock) where 1=1 {sqlWhere}";
}
public override string GetSelectTopFrom(int limit, int start, string orderby, string orderbytag, string sqlWhere)
{
if (ProviderName.Contains("MySql"))
{
return GetSelectTopFromByMySql(limit, start, orderby, orderbytag, sqlWhere);
}
else if (ProviderName.Contains("SqlServer"))
{
return GetSelectTopFromBySqlServer(limit, start, orderby, orderbytag, sqlWhere);
}
else
{
throw new NotSupportedException($"不支持的数据库提供程序:{ProviderName}");
}
}
public string GetSelectTopFromByMySql(int limit, int start,string orderby,string orderbytag, string sqlWhere)
{
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_LOG_ID ;
int offset = limit * (start - 1);
return $" select {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from { DB_NAME_FAMILY_TREE_FILE_LOG} where 1=1 {sqlWhere} ) a where num> {offset} LIMIT {limit} ";
}
public string GetSelectTopFromBySqlServer(int limit, int start,string orderby,string orderbytag, string sqlWhere)
{
if (string.IsNullOrEmpty(orderbytag) || orderbytag.Trim() != "desc") orderbytag = "asc";
if (string.IsNullOrEmpty(orderby) || !DB_NAME_FIELDS.Contains(orderby)) orderby = DB_LOG_ID ;
string top = limit > 0 ? $"top {limit}" : "";
return $" select {top} {DB_NAME_FIELDS} from (select {DB_NAME_FIELDS} ,row_number() over(order by {orderby} {orderbytag}) as num from { DB_NAME_FAMILY_TREE_FILE_LOG} with(nolock) where 1=1 {sqlWhere} ) a where num> {limit * (start - 1)}";
}
public FamilyTreeFileLogEntity Clone()
{
FamilyTreeFileLogEntity result = new FamilyTreeFileLogEntity();
result.CopyFrom(this);
return result;
}
}
}
@@ -0,0 +1,22 @@
using System;
namespace CloudBuilder.Master.Policy
{
/// <summary>
/// 分片上传落盘目录名统一管理。物理文件落在 {UploadFileDirectory}/{folderName}/{扩展名}/,
/// 末片合并后拷贝到 {BackupFileDirectory}/{folderName}/{扩展名}/;后端拼头像访问地址
/// (FileTypePolicy.PackAvatarUrl)用的目录名必须与前端 multipart 的 FilePath 头完全一致,
/// 故把这些散落在各服务的字符串收敛到此处,作为前后端共同对齐的单一真源。
/// </summary>
public static class UploadFolderPolicy
{
/// <summary>族谱成员头像目录(family_person.avatar_filename 对应)</summary>
public const string FAMILY_PERSON_AVATAR = "FamilyPersonAvatar";
/// <summary>登录用户(我的)头像目录(security_user.user_icon_img_path 对应)</summary>
public const string USER_AVATAR = "UserAvatar";
/// <summary>身份证照片目录(与前端身份证上传 FilePath 头一致,小写历史沿用)</summary>
public const string ID_CARD = "idcard";
}
}
@@ -33,6 +33,12 @@ namespace CloudBuilder.Master.Service
void UploadFile();
void UploadDmsFile();
FamilyPersonDisplayData UploadIdCardFile();
/// <summary>当前登录用户的网盘用量汇总(类型/头像/已用/额度/百分比),供 dashboard 卡片。</summary>
StorageUsageData GetMyStorageUsage();
/// <summary>上传并保存当前登录用户头像(multipart 单图),落盘 UserAvatar 目录并回写 security_user.user_icon_img_path,返回可访问 URL。</summary>
string UploadUserAvatar();
string FindM3u8Path(long fileId);
string FindSourceVideoPath(long fileId);
@@ -21,5 +21,18 @@ namespace CloudBuilder.Master.Service
FamilyTreeData BindSelfPerson(string treeId, string personId);
FamilyTreeData UnbindSelfPerson(string treeId);
// 族谱全系/小系 .fm 导入导出(D1-D8):Export 出明文数据由前端加密,Save/List/Read 走交换目录
string GetExchangeFactor();
FamilyTreeExchangeData ExportFamilyTree(string treeId, string scope, string anchorPersonId);
string SaveExchangeFile(string fileName, string contentBase64);
FamilyTreeExchangeFileInfo[] ListExchangeFiles(string treeId);
string ReadExchangeFile(string fileName);
int ImportFamilyTree(string targetTreeId, FamilyTreeExchangeData data);
int ImportSubTree(string targetTreeId, string anchorPersonId, FamilyTreeExchangeData data);
FamilyTreeExchangeFileInfo[] ListBackups(string treeId);
int RestoreBackup(string targetTreeId, string backupFileName);
}
}