Files
Docker/CloudBuilder/CloudBuilder.Master.Service/FamilyTreeService.auto.cs
T
2026-10-23 09:46:48 +08:00

1460 lines
84 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CloudBuilder.Core.DatabaseAccessor;
using CloudBuilder.Core.DatabaseAccessor.Entity;
using CloudBuilder.Core.Service;
using CloudBuilder.Master.Data;
using CloudBuilder.Master.Entity;
using CloudBuilder.Master.Policy;
using CloudBuilder.Master.SearchCriteria;
using CloudBuilder.Master.Service.FamilyTreeUtility;
using CloudBuilder.Security.Data;
using CloudBuilder.Security.Entity;
using CloudBuilder.Security.Policy;
using CloudBuilder.Security.Service;
using CloudBuilder.Security.Utility;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace CloudBuilder.Master.Service
{
public class FamilyTreeService : IFamilyTreeService
{
private readonly IApplicationService service;
private IRepository<FamilyTreeEntity> repository;
public FamilyTreeService(IApplicationService service)
{
this.service = service;
repository = service.GetRepository<IRepository<FamilyTreeEntity>>();
}
/// <summary>把 AvatarFilename 拼装成可访问的 AvatarUrl,供谱系图卡片显示头像</summary>
private void FillAvatarUrl(FamilyPersonDisplayData person)
{
if (person == null) return;
person.AvatarUrl = FileTypePolicy.PackAvatarUrl(service, UploadFolderPolicy.FAMILY_PERSON_AVATAR, person.AvatarFilename);
}
/// <summary>批量填充头像访问地址</summary>
private void FillAvatarUrl(FamilyPersonDisplayData[] persons)
{
if (persons == null || persons.Length == 0) return;
foreach (FamilyPersonDisplayData person in persons)
{
FillAvatarUrl(person);
}
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeData FindAllByTreeId(string treeId)
{
FamilyTreeData data = new FamilyTreeData();
if (string.IsNullOrEmpty(treeId)) return data;
FamilyTreeEntity d = repository.DetachedEntities.Where(x => x.TreeId == treeId).FirstOrDefault();
if (d == null) return data;
data.FamilyTree = new FamilyTreeDisplayData(d);
data.CurrentPerson = new FamilyPersonData();
IRepository<FamilyPersonEntity> repositoryFamilyPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
var currentPerson = d.AncestorPersonId;
data.CurrentPerson.FamilyPerson = new FamilyPersonDisplayData(repositoryFamilyPerson.DetachedEntities.Where(x => x.PersonId == currentPerson).FirstOrDefault()!);
FillAvatarUrl(data.CurrentPerson.FamilyPerson);
data.FamilyTree.AncestorPersonGenealogyName = data.CurrentPerson.FamilyPerson.GenealogyName;
IRepository<FamilyTreeChildrenMapViewEntity> repositoryChildrenMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>();
IRepository<FamilyTreeSpouseMapViewEntity> repositorySpouseMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>();
data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>().DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray();
data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray();
List<string> ps = new List<string>();
ps.Add(d.AncestorPersonId);
if (data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0)
{
ps.AddRange(data.CurrentPerson.FamilyTreeSpouseMaps.Select(x => x.SpouseId).ToArray());
}
if (data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0)
{
ps.AddRange(data.CurrentPerson.FamilyTreeChildrenMaps.Select(x => x.ChildrenId).ToArray());
}
ps = ps.Distinct().ToList();
FamilyPersonEntity[] families = repositoryFamilyPerson.DetachedEntities.Where(x => ps.AsQueryable().Contains(x.PersonId)).ToArray();
if (families != null && families.Length > 0)
{
data.CurrentPerson.FamilyTreePersons = families.Select(x => new FamilyPersonDisplayData(x)).ToArray();
FillAvatarUrl(data.CurrentPerson.FamilyTreePersons);
}
return data;
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeData FindByKey(string key)
{
return FindByCurrentPerson(key, null);
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeData FindByCurrentPerson(string key, string personId)
{
FamilyTreeData data = new FamilyTreeData();
if (string.IsNullOrEmpty(key)) return data;
FamilyTreeEntity d = repository.DetachedEntities.Where(x => x.TreeId == key).FirstOrDefault();
if (d == null) return data;
IRepository<FamilyTreePermissionUserViewEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionUserViewEntity>>();
data.FamilyTree = new FamilyTreeDisplayData(d);
data.CurrentPerson = new FamilyPersonData();
data.FamilyTreePermissions = repositoryPermission.DetachedEntities.Where(x => x.TreeId == key).ToArray();
if (data.FamilyTreePermissions.Any())
{
data.IsManager = data.FamilyTreePermissions.Where(x => x.UserName == service.UserId && x.ManagerIndc == YesNoPolicy.YES).Any();
if (data.IsManager) data.IsCanAmend = true;
// 成员口径:有绑定到自己且已生效的权限行即算(与锚点是否可编无关,供前端选项卡级控权);需在下方过滤名单前算,否则非管理员只剩待审行
var selfValidRows = data.FamilyTreePermissions.Where(x => x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).ToArray();
data.IsMember = selfValidRows.Any();
// 自己绑定的族员(供前端切换「绑定账号/解除绑定」按钮),管理员无绑定概念不下发
if (!data.IsManager) data.SelfBindPersonId = selfValidRows.Select(x => x.PersonId).FirstOrDefault() ?? "";
}
if (!data.IsManager && data.FamilyTreePermissions.Any())
{
data.FamilyTreePermissions = data.FamilyTreePermissions.Where(x => x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.NO).ToArray();
}
IRepository<FamilyPersonEntity> repositoryFamilyPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
var currentPerson = personId;
if (string.IsNullOrEmpty(personId))
{
currentPerson = d.AncestorPersonId;
FamilyPersonEntity ancestor0 = repositoryFamilyPerson.DetachedEntities.Where(x => x.PersonId == currentPerson).FirstOrDefault();
// 始祖行缺失是数据破损,给可读的业务错误而不是 NullReference 服务异常
if (ancestor0 == null) throw new ValidatedException("该族谱未设置始祖或始祖档案已丢失.");
data.CurrentPerson.FamilyPerson = new FamilyPersonDisplayData(ancestor0);
data.FamilyTree.AncestorPersonGenealogyName = data.CurrentPerson.FamilyPerson.GenealogyName;
}
else
{
FamilyPersonEntity ancestor = repositoryFamilyPerson.DetachedEntities.Where(x => x.PersonId == d.AncestorPersonId).FirstOrDefault();
if (ancestor != null)
{
data.CurrentPerson.FamilyPerson = new FamilyPersonDisplayData(ancestor);
data.FamilyTree.AncestorPersonGenealogyName = data.CurrentPerson.FamilyPerson.GenealogyName;
}
// 旧 personId 失效(如整谱替换/备份还原后全员重编号)回落始祖,不再抛「Source entity is null」服务异常
FamilyPersonEntity current = repositoryFamilyPerson.DetachedEntities.Where(x => x.PersonId == personId).FirstOrDefault() ?? ancestor;
if (current == null) throw new ValidatedException("该族谱中没有可用的族员.");
currentPerson = current.PersonId;
data.CurrentPerson.FamilyPerson = new FamilyPersonDisplayData(current);
}
FillAvatarUrl(data.CurrentPerson.FamilyPerson);
IRepository<FamilyTreeChildrenMapViewEntity> repositoryChildrenMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>();
IRepository<FamilyTreeSpouseMapViewEntity> repositorySpouseMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>();
data.CurrentPerson.FamilyTreeSpouseMaps = repositorySpouseMap.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>().DetachedEntities.Where(x => x.PersonId == currentPerson && x.TreeId == d.TreeId).OrderBy(x => x.Orderby).ToArray();
//data.CurrentPerson.FamilyTreeParentMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.ChildrenId == key).OrderBy(x => x.Orderby).ToArray();
data.CurrentPerson.FamilyTreeChildrenMaps = repositoryChildrenMap.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>().DetachedEntities.Where(x => x.PersonId == currentPerson && x.TreeId == d.TreeId).OrderBy(x => x.Orderby).ToArray();
// 补充当前人员及其配偶、子女的人员信息(含头像地址),供谱系图卡片显示头像
List<string> ps = new List<string>();
if (!string.IsNullOrEmpty(currentPerson)) ps.Add(currentPerson);
// 始祖恒定并入下发集合:切换当前族员后,前端「始祖信息」卡仍需回查始祖档案(Distinct 去重)
if (!string.IsNullOrEmpty(d.AncestorPersonId)) ps.Add(d.AncestorPersonId);
if (data.CurrentPerson.FamilyTreeSpouseMaps != null)
{
ps.AddRange(data.CurrentPerson.FamilyTreeSpouseMaps.Select(x => x.SpouseId).ToArray());
}
if (data.CurrentPerson.FamilyTreeChildrenMaps != null)
{
ps.AddRange(data.CurrentPerson.FamilyTreeChildrenMaps.Select(x => x.ChildrenId).ToArray());
}
ps = ps.Where(x => !string.IsNullOrEmpty(x)).Distinct().ToList();
FamilyPersonEntity[] families = repositoryFamilyPerson.DetachedEntities.Where(x => ps.AsQueryable().Contains(x.PersonId)).ToArray();
if (families != null && families.Length > 0)
{
data.CurrentPerson.FamilyTreePersons = families.Select(x => new FamilyPersonDisplayData(x)).ToArray();
FillAvatarUrl(data.CurrentPerson.FamilyTreePersons);
}
if (!data.IsCanAmend)
data.IsCanAmend = new FamilyTreePermissionHelper(service).HasPermission(data.FamilyTree.TreeId, currentPerson);
// 逐个目标人的可编集合(关系行里的旁支靠它隐藏编辑/删除钮);管理员全量可编不限量不下发
if (!data.IsManager && data.IsMember)
data.CanAmendPersonIds = new FamilyTreePermissionHelper(service).GetAmendablePersonIds(data.FamilyTree.TreeId).ToArray();
return data;
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeResultData FindBySearchCriteria(FamilyTreeSearchCriteria criteria)
{
FamilyTreeResultData data = new FamilyTreeResultData();
FamilyTreeViewEntity[] list = null;
Int64 count = 0;
string sqlWhere = null;
if (criteria.Filter != null && criteria.Filter.Length > 0)
{
sqlWhere = SearchFilter.ComposeWhereClause(criteria.Filter);
}
FamilyTreeViewEntity entity = FamilyTreeViewEntity.GetInstance(repository.GetDbContext().Database.ProviderName);
string sql = entity.GetSelectTopFrom(criteria.limit, criteria.page, criteria.orderby.StringToDbField(), criteria.orderbytag, sqlWhere);
IRepository<FamilyTreeViewEntity> repositoryView = repository.GetRepository<IRepository<FamilyTreeViewEntity>>();
string err = null;
try
{
list = repositoryView.FromSql(sql).ToArray();
}
catch (System.Exception ex)
{
err = ex.Message;
}
if (list != null && list.Length > 0)
{
sql = entity.GetSelectCountFrom(sqlWhere);
count = Convert.ToInt64(repository.ExecuteScalar(sql));
}
data.Limit = criteria.limit;
data.Page = criteria.page;
data.Datas = list;
data.Total = count;
//data.Sql = sql;
data.ErrorMessage = err;
return data;
}
//[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Print)]
public dynamic PrintBySearchCriteria(FamilyTreeSearchCriteria criteria)
{
throw new NotImplementedException();
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Insert)]
[UnitOfWork]
public FamilyTreeData Insert(FamilyTreeData data)
{
FamilyTreeEntity d = repository.DetachedEntities.Where(x => x.TreeName == data.FamilyTree.TreeName).FirstOrDefault();
if (d != null)
{
throw new ValidatedException("家族名称已存在.");
}
Validate(data.FamilyTree);
d = new FamilyTreeEntity();
ObjectCopyHelper.Copy(data.FamilyTree, d);
ISecurityGeneratorNumberService securityGeneratorNumberService = service.ServiceProvider.GetService<ISecurityGeneratorNumberService>()!;
d.TreeId = securityGeneratorNumberService.GetNo(MasterPrefixPolicy.FAMILY_TREE, MasterPrefixPolicy.FAMILY_TREE_TREE_ID, MasterPrefixPolicy.FAMILY_TREE_PREFIX, YesNoPolicy.NO, MasterPrefixPolicy.FAMILY_TREE_FORMAT);
repository.InsertNow(d);
FamilyTreePermissionEntity treePermissionEntity = new FamilyTreePermissionEntity();
treePermissionEntity.TreeId = d.TreeId;
treePermissionEntity.PersonId = d.AncestorPersonId;
treePermissionEntity.UserName = service.UserId;
treePermissionEntity.ManagerIndc = YesNoPolicy.YES;
treePermissionEntity.ValidIndc = YesNoPolicy.YES;
InsertFamilyTreePermission(treePermissionEntity);
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
FamilyPersonEntity familyPerson;
familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == d.AncestorPersonId).FirstOrDefault()!;
familyPerson.TreeId = d.TreeId;
repositoryPerson.UpdateNow(familyPerson);
var currentPerson = d.AncestorPersonId;
if (data.CurrentPerson != null && data.CurrentPerson.FamilyPerson != null && !string.IsNullOrEmpty(data.CurrentPerson.FamilyPerson.PersonId))
{
currentPerson = data.CurrentPerson.FamilyPerson.PersonId;
}
if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0)
data.CurrentPerson.FamilyTreeSpouseMaps.ToLookup(x => x.TreeId = d.TreeId);
if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0)
data.CurrentPerson.FamilyTreeChildrenMaps.ToLookup(x => x.TreeId = d.TreeId);
SaveSpouseMaps(d.TreeId, currentPerson, data.CurrentPerson.FamilyTreeSpouseMaps);
//SaveParentMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeParentMaps);
SaveSonMaps(d.TreeId, currentPerson, data.CurrentPerson.FamilyTreeChildrenMaps);
return FindByCurrentPerson(d.TreeId, currentPerson);
}
public void InsertFamilyTreePermission(FamilyTreePermissionEntity treePermissionEntity)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
repositoryPermission.InsertNow(treePermissionEntity);
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
[UnitOfWork]
public void RequestFamilyTreePermission(string treeId, string currentPersonId)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
// 一人一谱一行:PersonId 不再参与定位与绑定(申请锚点≠本人),查重按 TreeId+UserName
FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && service.UserId == x.UserName).FirstOrDefault()!;
if (treePermissionEntity != null) return;
treePermissionEntity = new FamilyTreePermissionEntity();
treePermissionEntity.TreeId = treeId;
// 申请时不绑定任何族员(旧口径把申请锚点——往往是始祖——绑了进去,导致成员误得大支系);
// 绑定时机:审批时按中文姓名同名唯一自动绑,或通过后由用户自行「绑定账号」
treePermissionEntity.PersonId = "";
treePermissionEntity.UserName = service.UserId;
treePermissionEntity.ManagerIndc = YesNoPolicy.NO;
treePermissionEntity.ValidIndc = YesNoPolicy.NO;
InsertFamilyTreePermission(treePermissionEntity);
}
/// <summary>
/// PersonId 属复合主键 (PersonId,TreeId,UserName),EF 分离实体直接改主键列更新不可靠,换绑一律删旧行插新行
/// </summary>
private void RebindFamilyTreePermission(FamilyTreePermissionEntity row, string personId)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity fresh = new FamilyTreePermissionEntity();
fresh.TreeId = row.TreeId;
fresh.UserName = row.UserName;
fresh.PersonId = personId;
fresh.ManagerIndc = row.ManagerIndc;
fresh.ValidIndc = row.ValidIndc;
repositoryPermission.DeleteNow(row);
repositoryPermission.InsertNow(fresh);
}
/// <summary>
/// 小系替换后的权限行迁移(用户拍板:按新旧编号关联保留绑定权限):
/// 被删后代/入配配偶若能经 idMap(文件旧号→重编新号)关联上则换绑到新号(同谱 round-trip 场景);
/// 关联不上(跨谱嫁接/文件已删掉的人)的悬挂行顺手删除。
/// 管理员行(ManagerIndc=YES)与 PersonId 空行(申请/未绑)一律不动;返回 moved/dropped 统计进审计。
/// </summary>
private string MigrateSubTreePermissions(string treeId, HashSet<string> deletedIds, Dictionary<string, string> idMap)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity[] rows = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId).ToArray();
int moved = 0;
int dropped = 0;
foreach (FamilyTreePermissionEntity row in rows)
{
if (row.ManagerIndc == YesNoPolicy.YES || string.IsNullOrEmpty(row.PersonId) || !deletedIds.Contains(row.PersonId)) continue;
string newId = idMap.ContainsKey(row.PersonId) ? idMap[row.PersonId] : null;
// 新号已被同名其它行占用(理论上不会)则只删旧行,防复合主键冲突
if (newId != null && rows.Any(x => x.PersonId == newId && x.UserName == row.UserName)) newId = null;
if (newId == null)
{
repositoryPermission.DeleteNow(row);
dropped++;
continue;
}
RebindFamilyTreePermission(row, newId);
moved++;
}
return $"moved={moved};dropped={dropped}";
}
/// <summary>
/// 跨谱整谱替换后的权限行清理:全员重编号后本谱引用已删旧号的成员权限行必然悬挂(垃圾行),顺手删除;
/// 管理员行(ManagerIndc=YES,建谱时 PersonId 指向始祖,IsManager 判定不看 PersonId)与 PersonId 空行保留。
/// 同谱替换编号不变权限行天然无损,不需要走这里。返回删除条数进审计。
/// </summary>
private int CleanDanglingPermissions(string treeId, HashSet<string> aliveIds)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
List<string> alive = aliveIds.ToList();
FamilyTreePermissionEntity[] dangling = repositoryPermission.DetachedEntities
.Where(x => x.TreeId == treeId && x.ManagerIndc != YesNoPolicy.YES && x.PersonId != "" && !alive.Contains(x.PersonId)).ToArray();
if (dangling.Length > 0) repositoryPermission.DeleteNow(dangling);
return dangling.Length;
}
/// <summary>
/// 备份还原的权限回放(绝对闭环):把本谱权限行整体替换为备份快照,PersonId 经 idMap 重映射
/// (还原恒同谱→编号不变→权限行原样落回);管理员行随快照一并恢复,还原人自身也在快照内故不会自锁。
/// 仅备份(含权限快照)触发;.fm 导入无权限快照不会走到这里。返回恢复条数进审计。
/// </summary>
private int RestorePermissions(string treeId, FamilyTreePermissionEntity[] snapshot, Dictionary<string, string> idMap)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity[] current = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId).ToArray();
if (current.Length > 0) repositoryPermission.DeleteNow(current);
int restored = 0;
foreach (FamilyTreePermissionEntity row in snapshot)
{
FamilyTreePermissionEntity fresh = new FamilyTreePermissionEntity
{
TreeId = treeId,
UserName = row.UserName,
PersonId = string.IsNullOrEmpty(row.PersonId) || !idMap.ContainsKey(row.PersonId) ? row.PersonId : idMap[row.PersonId],
ManagerIndc = row.ManagerIndc,
ValidIndc = row.ValidIndc,
};
repositoryPermission.InsertNow(fresh);
restored++;
}
return restored;
}
/// <summary>
/// 用户中文姓名(security_user.user_local_name);登录账号只是标识,不参与同名比较
/// </summary>
private string GetUserLocalName(string userName)
{
IRepository<SecurityUserEntity> repositoryUser = repository.GetRepository<IRepository<SecurityUserEntity>>();
return repositoryUser.DetachedEntities.Where(x => x.UserName == userName).FirstOrDefault()?.UserLocalName ?? "";
}
/// <summary>
/// 同名口径:族员「姓+谱名」组合或常用名与用户中文姓名相同
/// </summary>
private bool IsMatchedByLocalName(FamilyPersonEntity person, string localName)
{
if (string.IsNullOrEmpty(localName)) return false;
return (person.Surname ?? "") + (person.GenealogyName ?? "") == localName || person.CommonName == localName;
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
[UnitOfWork]
public void ApproveFamilyTreePermission(string treeId, string currentPersonId, string userName, string managerIndc)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
// 行定位不再用 PersonId(申请行已不绑族员,旧数据也可能已换绑),TreeId+UserName 即唯一行
FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && userName == x.UserName).FirstOrDefault()!;
if (treePermissionEntity == null) return;
treePermissionEntity.ManagerIndc = managerIndc;
treePermissionEntity.ValidIndc = YesNoPolicy.YES;
// 通过为成员:不绑始祖;用户中文姓名与「姓+谱名」或常用名相同且全谱唯一才自动绑定,否则留空由用户自行「绑定账号」
if (managerIndc != YesNoPolicy.YES)
{
string localName = GetUserLocalName(userName);
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
// 姓+谱名是运行时拼接,EF 翻不动就拉全谱内存筛(单谱人数量级小)
FamilyPersonEntity[] matches = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId).ToArray()
.Where(x => IsMatchedByLocalName(x, localName)).ToArray();
string boundPersonId = matches.Length == 1 ? matches[0].PersonId : "";
if (treePermissionEntity.PersonId != boundPersonId)
{
RebindFamilyTreePermission(treePermissionEntity, boundPersonId);
return;
}
}
repositoryPermission.UpdateNow(treePermissionEntity);
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
[UnitOfWork]
public void RejectFamilyTreePermission(string treeId, string currentPersonId, string userName)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity treePermissionEntity = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && userName == x.UserName).FirstOrDefault()!;
if (treePermissionEntity == null) return;
repositoryPermission.DeleteNow(treePermissionEntity);
}
/// <summary>
/// 绑定账号:把当前族员绑到自己已生效的权限行(仅限与登录中文姓名同名的族员);
/// 返回绑定后的详情数据(锚点切到刚绑的族员)
/// </summary>
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
[UnitOfWork]
public FamilyTreeData BindSelfPerson(string treeId, string personId)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity row = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).FirstOrDefault()!;
if (row == null) throw new ValidatedException("您还未通过审批,不能绑定账号.");
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
FamilyPersonEntity person = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId && x.PersonId == personId).FirstOrDefault()!;
if (person == null) throw new ValidatedException("族员不存在.");
// 同名口径:族员「姓+谱名」组合或常用名与登录中文姓名相同才可绑定(与管理员审批自动绑定同一把尺)
if (!IsMatchedByLocalName(person, GetUserLocalName(service.UserId)))
throw new ValidatedException("只能绑定与您的中文姓名同名的族员.");
if (row.PersonId != personId) RebindFamilyTreePermission(row, personId);
return FindByCurrentPerson(treeId, personId);
}
/// <summary>
/// 解除绑定:把自己已生效权限行的 PersonId 置空(锚点回到始祖),解绑后可重新「绑定账号」
/// </summary>
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
[UnitOfWork]
public FamilyTreeData UnbindSelfPerson(string treeId)
{
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity row = repositoryPermission.DetachedEntities.Where(x => x.TreeId == treeId && x.UserName == service.UserId && x.ValidIndc == YesNoPolicy.YES).FirstOrDefault()!;
if (row == null) throw new ValidatedException("您还未通过审批,不能解除绑定.");
if (!string.IsNullOrEmpty(row.PersonId)) RebindFamilyTreePermission(row, "");
return FindByCurrentPerson(treeId, "");
}
private void SaveSpouseMaps(string treeId, string personId, FamilyTreeSpouseMapViewEntity[] maps)
{
IRepository<FamilyTreeSpouseMapViewEntity> repositoryMapView = repository.GetRepository<IRepository<FamilyTreeSpouseMapViewEntity>>();
IRepository<FamilyTreeSpouseMapEntity> repositoryMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
FamilyPersonEntity familyPerson;
FamilyTreeSpouseMapViewEntity[] olds = repositoryMapView.Entities.Where(x => x.TreeId == treeId && x.PersonId == personId).ToArray();
if (olds != null && olds.Length > 0)
{
foreach (FamilyTreeSpouseMapViewEntity old in olds)
{
// 仅当新载荷不再保留该配偶时才连带删其人员档案;原写法误用 old.PersonId(父辈本人),
// 会把父辈 family_person 行删掉,导致子女 view 联查丢行、SaveSonMaps 查不到旧行不删,重插同主键报 Duplicate entry
if (maps == null || !maps.Any(m => m.SpouseId == old.SpouseId))
{
familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == old.SpouseId).FirstOrDefault()!;
if (familyPerson != null)
{
repositoryPerson.DeleteNow(familyPerson);
}
}
repositoryMap.DeleteNow(new FamilyTreeSpouseMapEntity()
{
TreeId = old.TreeId,
PersonId = personId,
SpouseId = old.SpouseId
});
}
}
if (maps == null || maps.Length == 0) return;
int i = 1;
foreach (FamilyTreeSpouseMapViewEntity map in maps.OrderBy(x => x.Orderby).ToArray())
{
familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == map.PersonId && string.IsNullOrEmpty(x.TreeId)).FirstOrDefault()!;
if (familyPerson != null)
{
familyPerson.TreeId = map.TreeId;
repositoryPerson.Update(familyPerson);
}
repositoryMap.InsertNow(new FamilyTreeSpouseMapEntity()
{
TreeId = map.TreeId,
PersonId = personId,
SpouseId = map.SpouseId,
MarryDateText = map.MarryDateText,
MarryDate = map.MarryDate,
DivorceDateText = map.DivorceDateText,
DivorceDate = map.DivorceDate,
MarryType = map.MarryType,
IsCurrent = map.IsCurrent,
Remark = map.Remark,
Orderby = i++
});
}
}
private void SaveSonMaps(string treeId, string personId, FamilyTreeChildrenMapViewEntity[] maps)
{
IRepository<FamilyTreeChildrenMapViewEntity> repositoryMapView = repository.GetRepository<IRepository<FamilyTreeChildrenMapViewEntity>>();
IRepository<FamilyTreeChildrenMapEntity> repositoryMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
FamilyPersonEntity familyPerson;
FamilyTreeChildrenMapViewEntity[] olds = repositoryMapView.Entities.Where(x => x.TreeId == treeId && x.PersonId == personId).ToArray();
if (olds != null && olds.Length > 0)
{
foreach (FamilyTreeChildrenMapViewEntity old in olds)
{
// 同 SaveSpouseMaps:只在该子女不再被新载荷保留时才连带删档案,且删的是子女而非父辈
if (maps == null || !maps.Any(m => m.ChildrenId == old.ChildrenId))
{
familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == old.ChildrenId).FirstOrDefault()!;
if (familyPerson != null)
{
repositoryPerson.DeleteNow(familyPerson);
}
}
repositoryMap.DeleteNow(new FamilyTreeChildrenMapEntity()
{
TreeId = old.TreeId,
PersonId = personId,
ChildrenId = old.ChildrenId
});
}
}
if (maps == null || maps.Length == 0) return;
int i = 1;
foreach (FamilyTreeChildrenMapViewEntity map in maps.OrderBy(x => x.Orderby).ToArray())
{
familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == map.PersonId && string.IsNullOrEmpty(x.TreeId)).FirstOrDefault()!;
if (familyPerson != null)
{
familyPerson.TreeId = map.TreeId;
repositoryPerson.Update(familyPerson);
}
repositoryMap.InsertNow(new FamilyTreeChildrenMapEntity()
{
TreeId = map.TreeId,
PersonId = personId,
SpouseId = map.SpouseId,
ChildrenId = map.ChildrenId,
RelationType = map.RelationType,
Remark = map.Remark,
Orderby = i++
});
}
}
private void Validate(FamilyTreeEntity d)
{
List<SysTableSchemaViewEntity> sysTableSchemas = service.ServiceProvider.GetRequiredService<ISysTableSchemaViewService>().FindByTableName(FamilyTreeEntity.DB_NAME_FAMILY_TREE);
ValidateHelper.ValidateString(d, sysTableSchemas.ToArray());
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Update)]
[UnitOfWork]
public FamilyTreeData Update(FamilyTreeData data)
{
FamilyTreeEntity d = null;
if (data.FamilyTree.TreeName != data.FamilyTree.OrgTreeName)
{
d = repository.DetachedEntities.Where(x => x.TreeName == data.FamilyTree.TreeName).FirstOrDefault();
if (d != null)
{
throw new ValidatedException("家族名称已存在.");
}
}
d = repository.DetachedEntities.Where(x => x.TreeId == data.FamilyTree.TreeId).FirstOrDefault();
Validate(data.FamilyTree);
var helper = new FamilyTreePermissionHelper(service);
var currentPerson = data.CurrentPerson != null ? data.CurrentPerson.FamilyPerson.PersonId : null;
if (string.IsNullOrEmpty(currentPerson)) currentPerson = d.AncestorPersonId;
// 谱级口径:管理员才能改谱字段(名/简介/公开开关/挂始祖);
// 成员只准维护以自己当前族员为锚点的家支关系(锚点上溯两代内可能含始祖,HasPermission(始祖) 做不了谱级闸)
var isManager = helper.IsManager(d.TreeId);
if (!isManager)
{
if (!helper.HasPermission(d.TreeId, currentPerson))
{
throw new ValidatedException("您没有修改权限.");
}
if (!string.IsNullOrEmpty(data.FamilyTree.AncestorPersonId) && data.FamilyTree.AncestorPersonId != d.AncestorPersonId)
{
throw new ValidatedException("您没有挂始祖权限.");
}
}
if (isManager)
{
ObjectCopyHelper.Copy(data.FamilyTree, d);
repository.UpdateNow(d);
}
if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeSpouseMaps != null && data.CurrentPerson.FamilyTreeSpouseMaps.Length > 0)
data.CurrentPerson.FamilyTreeSpouseMaps.ToLookup(x => x.TreeId = d.TreeId);
if (data.CurrentPerson != null && data.CurrentPerson.FamilyTreeChildrenMaps != null && data.CurrentPerson.FamilyTreeChildrenMaps.Length > 0)
data.CurrentPerson.FamilyTreeChildrenMaps.ToLookup(x => x.TreeId = d.TreeId);
SaveSpouseMaps(d.TreeId, data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeSpouseMaps);
//SaveParentMaps(data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeParentMaps);
SaveSonMaps(d.TreeId, data.CurrentPerson.FamilyPerson.PersonId, data.CurrentPerson.FamilyTreeChildrenMaps);
return FindByCurrentPerson(data.FamilyTree.TreeId, currentPerson);
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Delete)]
[UnitOfWork]
public void Delete(string key)
{
var ent = repository.DetachedEntities.Where(x => x.TreeId == key).FirstOrDefault()!;
if (ent == null) return;
// 删谱属谱级操作,仅管理员:成员锚点若贴近始祖,HasPermission 也会为真,不能做闸
if (!new FamilyTreePermissionHelper(service).IsManager(ent.TreeId))
{
throw new ValidatedException("您没有删除权限.");
}
repository.DeleteNow(ent);
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
FamilyPersonEntity familyPerson;
familyPerson = repositoryPerson.DetachedEntities.Where(x => x.PersonId == ent.AncestorPersonId).FirstOrDefault()!;
if (familyPerson != null)
{
repositoryPerson.DeleteNow(familyPerson);
}
DeleteSpouses(ent.AncestorPersonId);
DeleteChildrens(ent.AncestorPersonId);
}
private void DeleteSpouses(string personId)
{
IRepository<FamilyTreeSpouseMapEntity> repositoryMap = repository.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
FamilyTreeSpouseMapEntity[] olds = repositoryMap.Entities.Where(x => x.PersonId == personId).ToArray();
if (olds != null && olds.Length > 0)
{
repositoryMap.DeleteNow(olds);
}
}
private void DeleteChildrens(string personId)
{
IRepository<FamilyTreeChildrenMapEntity> repositoryMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
FamilyTreeChildrenMapEntity[] olds = repositoryMap.Entities.Where(x => x.PersonId == personId).ToArray();
if (olds != null && olds.Length > 0)
{
repositoryMap.DeleteNow(olds);
}
foreach (FamilyTreeChildrenMapEntity old in olds)
{
DeleteChildrens(old.ChildrenId);
DeleteSpouses(old.ChildrenId);
}
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Delete)]
[UnitOfWork]
public void Deletes(string[] keys)
{
foreach (string key in keys)
{
Delete(key);
}
}
//=================== 族谱全系/小系 .fm 导入导出(D1-D8 决策) ===================
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public string GetExchangeFactor()
{
// D2:前端用「用户密码⊕系统因子」PBKDF2 派生 AES 密钥,因子由后端下发,不进前端包体
return FamilyTreeExchangePolicy.EXCHANGE_FACTOR;
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeExchangeData ExportFamilyTree(string treeId, string scope, string anchorPersonId)
{
FamilyTreeEntity tree = repository.DetachedEntities.Where(x => x.TreeId == treeId).FirstOrDefault();
if (tree == null) throw new ValidatedException("族谱不存在.");
bool isSub = scope == FamilyTreeExchangeScope.SUB;
FamilyTreePermissionHelper helper = new FamilyTreePermissionHelper(service);
bool isManager = helper.IsManager(treeId);
// D8 闸门:全系导出仅管理员;小系导出管理员任意锚点、成员仅限自己可编辑家支的锚点
if (!isManager)
{
if (!helper.IsMember(treeId)) throw new ValidatedException("您没有导出权限.");
if (!isSub) throw new ValidatedException("全系导出需要族谱管理员权限.");
if (!helper.GetAmendablePersonIds(treeId).Contains(anchorPersonId)) throw new ValidatedException("只能导出以自己为锚点的家支.");
}
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
IRepository<FamilyTreeSpouseMapEntity> repositorySpouse = repository.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
IRepository<FamilyTreeChildrenMapEntity> repositoryChild = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
FamilyTreeSpouseMapEntity[] spouseMaps = repositorySpouse.DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray();
FamilyTreeChildrenMapEntity[] childrenMaps = repositoryChild.DetachedEntities.Where(x => x.TreeId == treeId).OrderBy(x => x.Orderby).ToArray();
FamilyPersonEntity[] persons = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId).ToArray();
if (isSub)
{
// 小系子集口径 = 权限 helper 同源家支展开(后代闭包 + 配偶 + 上溯两代及其配偶):
// 管理员拿传入锚点展开;成员闸门已验过锚点,直接用自己可编集合
List<string> ids;
if (isManager)
{
ids = new List<string>();
helper.PackPersonIdByChildrenMaps(childrenMaps, spouseMaps, anchorPersonId, ids);
}
else
{
ids = helper.GetAmendablePersonIds(treeId);
}
persons = persons.Where(x => ids.Contains(x.PersonId)).ToArray();
spouseMaps = spouseMaps.Where(x => ids.Contains(x.PersonId) && ids.Contains(x.SpouseId)).ToArray();
childrenMaps = childrenMaps.Where(x => ids.Contains(x.PersonId) && ids.Contains(x.ChildrenId)).ToArray();
}
string exportFileName = FamilyTreeExchangePolicy.BuildFileName(isSub ? FamilyTreeExchangeScope.SUB : FamilyTreeExchangeScope.ALL, treeId, anchorPersonId);
WriteExchangeLog("OUT", treeId, isSub ? FamilyTreeExchangeScope.SUB : FamilyTreeExchangeScope.ALL, anchorPersonId, exportFileName, persons.Length, "OK", "");
return new FamilyTreeExchangeData
{
Scope = isSub ? FamilyTreeExchangeScope.SUB : FamilyTreeExchangeScope.ALL,
FamilyTree = tree,
Persons = persons,
SpouseMaps = spouseMaps,
ChildrenMaps = childrenMaps,
// 二期:小系文件把锚点写进 payload,导入侧据此验明同一人(一期只体在文件名里,旧文件读进来为空串)
AnchorPersonId = isSub ? anchorPersonId : "",
ExportedBy = service.UserId,
ExportedAt = DateTime.Now,
};
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public string SaveExchangeFile(string fileName, string contentBase64)
{
if (!FamilyTreeExchangePolicy.IsValidFileName(fileName)) throw new ValidatedException("文件名不合法(须为 A谱号_时间.fm 或 P谱号-人名号_时间.fm).");
FamilyTreePermissionHelper helper = new FamilyTreePermissionHelper(service);
// 管理员也有生效权限行,IsMember 一并覆盖
if (!helper.IsMember(FamilyTreeExchangePolicy.ParseFileName(fileName).Item1)) throw new ValidatedException("您不是该族谱成员.");
byte[] bytes;
try { bytes = Convert.FromBase64String(contentBase64 ?? ""); }
catch { throw new ValidatedException("文件内容不是合法的 base64."); }
if (bytes.Length == 0 || bytes.Length > 50 * 1024 * 1024) throw new ValidatedException("文件大小异常(限 1B~50MB).");
string dir = FamilyTreeExchangePolicy.GetExchangeDirectory(service);
Directory.CreateDirectory(dir);
File.WriteAllBytes(Path.Combine(dir, fileName), bytes);
return fileName;
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeExchangeFileInfo[] ListExchangeFiles(string treeId)
{
if (string.IsNullOrEmpty(treeId)) throw new ValidatedException("请先选择族谱.");
if (!new FamilyTreePermissionHelper(service).IsMember(treeId)) throw new ValidatedException("您不是该族谱成员.");
string dir = FamilyTreeExchangePolicy.GetExchangeDirectory(service);
if (!Directory.Exists(dir)) return [];
List<FamilyTreeExchangeFileInfo> files = new List<FamilyTreeExchangeFileInfo>();
foreach (string path in Directory.GetFiles(dir, "*" + FamilyTreeExchangePolicy.FILE_EXTENSION))
{
string name = Path.GetFileName(path);
var parsed = FamilyTreeExchangePolicy.ParseFileName(name);
// 只列本谱的文件(非法文件名一律跳过)
if (parsed == null || parsed.Item1 != treeId) continue;
FileInfo info = new FileInfo(path);
files.Add(new FamilyTreeExchangeFileInfo
{
FileName = name,
Size = info.Length,
LastModified = info.LastWriteTime,
Scope = parsed.Item3,
TreeId = parsed.Item1,
AnchorPersonId = parsed.Item2,
});
}
// 导入以最新为准:时间降序,前端取第一条即可
return files.OrderByDescending(x => x.LastModified).ToArray();
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public string ReadExchangeFile(string fileName)
{
if (!FamilyTreeExchangePolicy.IsValidFileName(fileName)) throw new ValidatedException("文件名不合法.");
if (!new FamilyTreePermissionHelper(service).IsMember(FamilyTreeExchangePolicy.ParseFileName(fileName).Item1)) throw new ValidatedException("您不是该族谱成员.");
string path = Path.Combine(FamilyTreeExchangePolicy.GetExchangeDirectory(service), fileName);
if (!File.Exists(path)) throw new ValidatedException("文件不存在或已被清理.");
return Convert.ToBase64String(File.ReadAllBytes(path));
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Update)]
[UnitOfWork]
public int ImportFamilyTree(string targetTreeId, FamilyTreeExchangeData data)
{
FamilyTreeEntity target = repository.DetachedEntities.Where(x => x.TreeId == targetTreeId).FirstOrDefault();
if (target == null) throw new ValidatedException("目标族谱不存在.");
// D4:整谱替换是谱级破坏性操作,仅目标谱管理员;失败由 ValidatedException 抛回,事务整体回滚
if (!new FamilyTreePermissionHelper(service).IsManager(targetTreeId)) throw new ValidatedException("只有族谱管理员可以整谱替换导入.");
if (data == null || data.Persons == null || data.Persons.Length == 0) throw new ValidatedException("导入数据为空.");
// 一期只开放全系文件替换;小系 merge 未开(P 文件导进谱会把别人家支的人叠进来,关系锚点不明)
if (data.Scope != FamilyTreeExchangeScope.ALL) throw new ValidatedException("目前仅支持全系(ALL)文件导入,小系合并暂未开放.");
// D3:允许跨谱导入(payload 自带谱信息,主键以目标谱为准),无谱信息的是非法包
if (data.FamilyTree == null) throw new ValidatedException("导入数据缺少谱信息.");
return ReplaceTreeCore(target, data, "IN");
}
/// <summary>
/// 替换前整谱 JSON 备份(回滚点):明文存交换目录 Backup 子目录,文件名进审计 message;
/// ListExchangeFiles 只看 *.fm,备份不会被列成可导入文件。
/// </summary>
private string BackupTree(FamilyTreeEntity target, FamilyPersonEntity[] oldPersons, FamilyTreeSpouseMapEntity[] oldSpouses, FamilyTreeChildrenMapEntity[] oldChildren)
{
string backupName = FamilyTreeExchangePolicy.BuildBackupFileName(target.TreeId);
string backupDir = FamilyTreeExchangePolicy.GetBackupDirectory(service);
Directory.CreateDirectory(backupDir);
// 权限行一并入快照(还原时随谱回滚,绝对闭环);整份密文落盘(含账号名隐私)
IRepository<FamilyTreePermissionEntity> repositoryPermission = repository.GetRepository<IRepository<FamilyTreePermissionEntity>>();
FamilyTreePermissionEntity[] perms = repositoryPermission.DetachedEntities.Where(x => x.TreeId == target.TreeId).ToArray();
string plainJson = System.Text.Json.JsonSerializer.Serialize(new FamilyTreeExchangeData
{
Scope = FamilyTreeExchangeScope.ALL,
FamilyTree = target,
Persons = oldPersons,
SpouseMaps = oldSpouses,
ChildrenMaps = oldChildren,
Permissions = perms,
ExportedBy = service.UserId,
});
File.WriteAllText(Path.Combine(backupDir, backupName), FamilyTreeExchangePolicy.EncryptBackup(plainJson));
return backupName;
}
/// <summary>
/// 整谱替换本体(ImportFamilyTree 与 RestoreBackup 共用,调用方已开事务):
/// 先备份现谱→关系先行、人后删→插入→关系双端 remap→谱信息行更新→审计。
/// 同谱(文件谱号==目标谱,备份还原/服务器文件导入恒同谱):编号与绑定原样保留,库与文件 100% 一致,权限行不悬挂;
/// 跨谱嫁接:全员重编号+清绑定(防主键冲突与绑定劫持)。入库前先过完整性闸,坏文件直接拒绝。
/// 审计动码区分来源:IN=外部文件导入 / RESTORE=备份回滚回放。
/// </summary>
private int ReplaceTreeCore(FamilyTreeEntity target, FamilyTreeExchangeData data, string action)
{
string targetTreeId = target.TreeId;
// 同谱判定:文件谱号与目标谱一致(还原走 R6 闸恒同谱;跨谱全系只能来自本地上传)
bool sameTree = string.Equals(data.FamilyTree?.TreeId ?? "", targetTreeId, StringComparison.OrdinalIgnoreCase);
// 0) 完整性闸:整谱数据必须内部闭合(人员编号非空不重复、关系行两端都在文件内),
// 否则拒绝——宁可不导也不允许静默丢行,保证落库与文件 100% 相同
HashSet<string> fileIdSet = new HashSet<string>();
foreach (FamilyPersonEntity p in data.Persons)
if (string.IsNullOrEmpty(p.PersonId) || !fileIdSet.Add(p.PersonId))
throw new ValidatedException("导入/还原失败:文件内人员编号为空或重复,数据不完整.");
foreach (FamilyTreeSpouseMapEntity m in data.SpouseMaps ?? [])
if (!fileIdSet.Contains(m.PersonId) || !fileIdSet.Contains(m.SpouseId))
throw new ValidatedException("导入/还原失败:配偶关系引用了文件外的人员,数据不完整.");
foreach (FamilyTreeChildrenMapEntity m in data.ChildrenMaps ?? [])
if (!fileIdSet.Contains(m.PersonId) || !fileIdSet.Contains(m.ChildrenId))
throw new ValidatedException("导入/还原失败:父子关系引用了文件外的人员,数据不完整.");
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
IRepository<FamilyTreeSpouseMapEntity> repositorySpouse = repository.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
IRepository<FamilyTreeChildrenMapEntity> repositoryChild = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
// 1) 替换前自动备份(回滚点)
FamilyPersonEntity[] oldPersons = repositoryPerson.DetachedEntities.Where(x => x.TreeId == targetTreeId).ToArray();
FamilyTreeSpouseMapEntity[] oldSpouses = repositorySpouse.DetachedEntities.Where(x => x.TreeId == targetTreeId).ToArray();
FamilyTreeChildrenMapEntity[] oldChildren = repositoryChild.DetachedEntities.Where(x => x.TreeId == targetTreeId).ToArray();
string backupName = BackupTree(target, oldPersons, oldSpouses, oldChildren);
// 2) 清空当前谱:关系先行、人后删(事务内任一环节失败全部回滚,备份文件已落盘不受影响)
if (oldSpouses.Length > 0) repositorySpouse.DeleteNow(oldSpouses);
if (oldChildren.Length > 0) repositoryChild.DeleteNow(oldChildren);
foreach (FamilyPersonEntity old in oldPersons) repositoryPerson.DeleteNow(old);
// 同谱回放需保留文件里的 Created 印章,而框架 SaveChanges 会对 Added 强制盖 CreatedBy/CreatedDatetime,
// 故插入段临时关闭审计印章(仿 LocaleService 先例;跨谱不开——照旧由框架盖当次操作人/时间);
// 下方手工赋值不受影响(Updated* 均为代码显式设置),末尾 finally 复位防残留
MasterDbContext dbCtx = repository.GetDbContext() as MasterDbContext;
Dictionary<string, string> idMap = new Dictionary<string, string>();
try
{
if (sameTree && dbCtx != null) dbCtx.SkipAudit = true;
// 3) 编号策略:同谱保留原编号与绑定(权限行/锚点继续有效,数据 100% 回放);
// 跨谱一律走号段生成器重发 PersonId + 清绑定(防冲突防劫持,进谱后重走审批绑定)
ISecurityGeneratorNumberService generator = service.ServiceProvider.GetService<ISecurityGeneratorNumberService>()!;
DateTime now = DateTime.Now;
foreach (FamilyPersonEntity p in data.Persons)
{
idMap[p.PersonId] = sameTree ? p.PersonId : generator.GetNo(MasterPrefixPolicy.FAMILY_PERSON, MasterPrefixPolicy.FAMILY_PERSON_PERSON_ID, MasterPrefixPolicy.FAMILY_PERSON_PREFIX, YesNoPolicy.NO, MasterPrefixPolicy.FAMILY_PERSON_FORMAT);
}
foreach (FamilyPersonEntity p in data.Persons)
{
FamilyPersonEntity fresh = p.Clone();
fresh.PersonId = idMap[p.PersonId];
fresh.TreeId = targetTreeId;
fresh.FatherId = string.IsNullOrEmpty(p.FatherId) ? null : (idMap.ContainsKey(p.FatherId) ? idMap[p.FatherId] : null);
fresh.BindUserId = sameTree ? p.BindUserId : null;
fresh.IsBindUser = sameTree ? p.IsBindUser : null;
if (sameTree)
{
fresh.CreatedBy = p.CreatedBy;
fresh.CreatedDatetime = p.CreatedDatetime;
}
else
{
fresh.CreatedBy = service.UserId;
fresh.CreatedDatetime = now;
}
fresh.UpdatedBy = service.UserId;
fresh.UpdatedDatetime = now;
repositoryPerson.InsertNow(fresh);
}
// 4) 关系行双端 remap(完整性闸已保证两端都在文件内,此处必全量命中);
// 同谱时 Created 印章随 Clone 原样回放,Updated 补当次操作痕迹;跨谱由框架盖当次
foreach (FamilyTreeSpouseMapEntity m in data.SpouseMaps ?? [])
{
if (!idMap.ContainsKey(m.PersonId) || !idMap.ContainsKey(m.SpouseId)) continue;
FamilyTreeSpouseMapEntity fresh = m.Clone();
fresh.TreeId = targetTreeId;
fresh.PersonId = idMap[m.PersonId];
fresh.SpouseId = idMap[m.SpouseId];
if (sameTree)
{
fresh.UpdatedBy = service.UserId;
fresh.UpdatedDatetime = now;
}
repositorySpouse.InsertNow(fresh);
}
foreach (FamilyTreeChildrenMapEntity m in data.ChildrenMaps ?? [])
{
if (!idMap.ContainsKey(m.PersonId) || !idMap.ContainsKey(m.ChildrenId)) continue;
FamilyTreeChildrenMapEntity fresh = m.Clone();
fresh.TreeId = targetTreeId;
fresh.PersonId = idMap[m.PersonId];
fresh.ChildrenId = idMap[m.ChildrenId];
if (sameTree)
{
fresh.UpdatedBy = service.UserId;
fresh.UpdatedDatetime = now;
}
repositoryChild.InsertNow(fresh);
}
// 5) 谱信息字段以数据为准(主键 TreeId 保持目标谱;始祖指向重编后的始祖)
target.TreeName = data.FamilyTree.TreeName;
target.Brief = data.FamilyTree.Brief;
target.TreeCoverFilename = data.FamilyTree.TreeCoverFilename;
target.PublicIndc = data.FamilyTree.PublicIndc;
target.AncestorPersonId = idMap.ContainsKey(data.FamilyTree.AncestorPersonId ?? "") ? idMap[data.FamilyTree.AncestorPersonId] : target.AncestorPersonId;
target.UpdatedBy = service.UserId;
target.UpdatedDatetime = now;
repository.UpdateNow(target);
}
finally
{
if (dbCtx != null) dbCtx.SkipAudit = false;
}
// 权限闭环:备份还原(含权限快照)→ 权限整体回放到备份时刻;跨谱 .fm 导入(无快照)→ 清悬挂行;同谱 .fm 导入→不碰
string permStat;
if (data.Permissions != null && data.Permissions.Length > 0)
permStat = $"; permRestored={RestorePermissions(targetTreeId, data.Permissions, idMap)}";
else if (!sameTree)
permStat = $"; permDropped={CleanDanglingPermissions(targetTreeId, new HashSet<string>(idMap.Values))}";
else
permStat = "";
// 6) 审计(事务回滚则审计同滚——失败场景弹错给用户,不留半截日志)
WriteExchangeLog(action, targetTreeId, FamilyTreeExchangeScope.ALL, "", backupName, idMap.Count, "OK", $"backup={backupName}; sameTree={(sameTree ? 1 : 0)}{permStat}");
return idMap.Count;
}
/// <summary>
/// 支系闭包:锚点以下后代 + 后代的配偶(迭代到不动点,防回头婚配漏拉);锚点本人不进集合。
/// 目标侧算删除集、文件侧算插入集共用此口径,保证删/插范围对称。
/// </summary>
static HashSet<string> BranchClosure(string anchorId, FamilyTreeChildrenMapEntity[] childrenMaps, FamilyTreeSpouseMapEntity[] spouseMaps)
{
HashSet<string> set = new HashSet<string>();
bool changed = true;
while (changed)
{
changed = false;
foreach (FamilyTreeChildrenMapEntity m in childrenMaps ?? [])
{
if ((m.PersonId == anchorId || set.Contains(m.PersonId)) && m.ChildrenId != anchorId && set.Add(m.ChildrenId)) changed = true;
}
foreach (FamilyTreeSpouseMapEntity m in spouseMaps ?? [])
{
if (set.Contains(m.PersonId) && m.SpouseId != anchorId && set.Add(m.SpouseId)) changed = true;
if (set.Contains(m.SpouseId) && m.PersonId != anchorId && set.Add(m.PersonId)) changed = true;
}
}
return set;
}
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Update)]
[UnitOfWork]
public int ImportSubTree(string targetTreeId, string anchorPersonId, FamilyTreeExchangeData data)
{
FamilyTreeEntity target = repository.DetachedEntities.Where(x => x.TreeId == targetTreeId).FirstOrDefault();
if (target == null) throw new ValidatedException("目标族谱不存在.");
if (data == null || data.Persons == null || data.Persons.Length == 0) throw new ValidatedException("导入数据为空.");
if (data.Scope != FamilyTreeExchangeScope.SUB) throw new ValidatedException("支系导入仅收小系(SUB)文件,全系请走整谱替换.");
if (string.IsNullOrEmpty(data.AnchorPersonId)) throw new ValidatedException("导入文件未标记小系锚点(旧版导出文件),请重新导出.");
if (string.IsNullOrEmpty(anchorPersonId)) throw new ValidatedException("请先选择要替换的族内锚点族人.");
FamilyTreePermissionHelper helper = new FamilyTreePermissionHelper(service);
// P6 权限闸:管理员任意锚点;成员仅限自己可编家支的锚点(与小系导出同一把闸)
if (!helper.IsManager(targetTreeId))
{
if (!helper.IsMember(targetTreeId)) throw new ValidatedException("您没有导入权限.");
if (!helper.GetAmendablePersonIds(targetTreeId).Contains(anchorPersonId)) throw new ValidatedException("只能替换自己家支的锚点.");
}
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
IRepository<FamilyTreeSpouseMapEntity> repositorySpouse = repository.GetRepository<IRepository<FamilyTreeSpouseMapEntity>>();
IRepository<FamilyTreeChildrenMapEntity> repositoryChild = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
FamilyPersonEntity targetAnchor = repositoryPerson.DetachedEntities.Where(x => x.TreeId == targetTreeId && x.PersonId == anchorPersonId).FirstOrDefault();
if (targetAnchor == null) throw new ValidatedException("锚点族人不存在.");
// P2/P3 同一人验证:文件锚点与所选族人 姓+谱名 全量一致才同意,否则拒绝
FamilyPersonEntity fileAnchor = data.Persons.FirstOrDefault(x => x.PersonId == data.AnchorPersonId);
if (fileAnchor == null) throw new ValidatedException("文件里找不到锚点人.");
string Trim(string s) => (s ?? "").Trim();
if (Trim(fileAnchor.Surname) != Trim(targetAnchor.Surname) || Trim(fileAnchor.GenealogyName) != Trim(targetAnchor.GenealogyName))
throw new ValidatedException("文件锚点与所选族人不是同一人(姓/谱名不符),已拒绝导入.");
FamilyPersonEntity[] oldPersons = repositoryPerson.DetachedEntities.Where(x => x.TreeId == targetTreeId).ToArray();
FamilyTreeSpouseMapEntity[] oldSpouses = repositorySpouse.DetachedEntities.Where(x => x.TreeId == targetTreeId).ToArray();
FamilyTreeChildrenMapEntity[] oldChildren = repositoryChild.DetachedEntities.Where(x => x.TreeId == targetTreeId).ToArray();
// 1) 替换前整谱 JSON 备份(P7,回滚点,同整谱替换口径)
string backupName = BackupTree(target, oldPersons, oldSpouses, oldChildren);
// 1.5) 方案 C:锚点本人也纳入替换——资料整体覆盖为文件锚点(备份已先行,可回滚);
// PersonId/TreeId/FatherId/绑定字段/建档信息保留目标侧,防锚点脱谱、防绑定被文件旧值劫持
DateTime now = DateTime.Now;
FamilyPersonEntity mergedAnchor = targetAnchor.Clone();
mergedAnchor.CopyFrom(fileAnchor);
mergedAnchor.PersonId = targetAnchor.PersonId;
mergedAnchor.TreeId = targetTreeId;
mergedAnchor.FatherId = targetAnchor.FatherId;
mergedAnchor.BindUserId = targetAnchor.BindUserId;
mergedAnchor.IsBindUser = targetAnchor.IsBindUser;
mergedAnchor.CreatedBy = targetAnchor.CreatedBy;
mergedAnchor.CreatedDatetime = targetAnchor.CreatedDatetime;
mergedAnchor.UpdatedBy = service.UserId;
mergedAnchor.UpdatedDatetime = now;
repositoryPerson.UpdateNow(mergedAnchor);
// 2) 删除集:目标锚点以下后代闭包 + 锚点配偶(方案 C 扩展:锚点整家替换;锚点本人已在 1.5 步覆盖)
// 配偶仅「纯入配」(无其它婚配/血缘关系、无账号绑定、非始祖)才随删重插;否则只断与锚点的关系行、人行保留防悬挂
HashSet<string> delSet = BranchClosure(anchorPersonId, oldChildren, oldSpouses);
HashSet<string> delSpousePersons = new HashSet<string>();
foreach (string sid in oldSpouses.Where(x => x.PersonId == anchorPersonId || x.SpouseId == anchorPersonId)
.Select(x => x.PersonId == anchorPersonId ? x.SpouseId : x.PersonId).Distinct())
{
if (sid == anchorPersonId || sid == target.AncestorPersonId || delSet.Contains(sid)) continue;
FamilyPersonEntity sp = oldPersons.FirstOrDefault(x => x.PersonId == sid);
if (sp == null) continue;
bool pure = string.IsNullOrEmpty(sp.BindUserId)
&& !oldSpouses.Any(x => (x.PersonId == sid || x.SpouseId == sid) && x.PersonId != anchorPersonId && x.SpouseId != anchorPersonId)
&& !oldChildren.Any(x => x.PersonId == sid || x.ChildrenId == sid);
if (pure) delSpousePersons.Add(sid);
}
FamilyTreeSpouseMapEntity[] delSpouses = oldSpouses.Where(x => delSet.Contains(x.PersonId) || delSet.Contains(x.SpouseId) || x.PersonId == anchorPersonId || x.SpouseId == anchorPersonId).ToArray();
FamilyTreeChildrenMapEntity[] delChildren = oldChildren.Where(x => delSet.Contains(x.ChildrenId)).ToArray();
FamilyPersonEntity[] delPersons = oldPersons.Where(x => delSet.Contains(x.PersonId) || delSpousePersons.Contains(x.PersonId)).ToArray();
if (delSpouses.Length > 0) repositorySpouse.DeleteNow(delSpouses);
if (delChildren.Length > 0) repositoryChild.DeleteNow(delChildren);
foreach (FamilyPersonEntity old in delPersons) repositoryPerson.DeleteNow(old);
// 3) 插入集:文件侧同口径闭包(上溯段落不插,P4)+ 文件锚点的配偶(方案 C 扩展,与目标侧删除对称);文件锚点映射到保留的目标锚点,其余重编号
HashSet<string> insSet = BranchClosure(data.AnchorPersonId, data.ChildrenMaps, data.SpouseMaps);
foreach (string fsid in (data.SpouseMaps ?? []).Where(x => x.PersonId == data.AnchorPersonId || x.SpouseId == data.AnchorPersonId)
.Select(x => x.PersonId == data.AnchorPersonId ? x.SpouseId : x.PersonId).Distinct())
{
if (fsid != data.AnchorPersonId && !insSet.Contains(fsid) && data.Persons.Any(p => p.PersonId == fsid)) insSet.Add(fsid);
}
ISecurityGeneratorNumberService generator = service.ServiceProvider.GetService<ISecurityGeneratorNumberService>()!;
Dictionary<string, string> idMap = new Dictionary<string, string> { [data.AnchorPersonId] = anchorPersonId };
foreach (FamilyPersonEntity p in data.Persons)
{
if (!insSet.Contains(p.PersonId) || idMap.ContainsKey(p.PersonId)) continue;
idMap[p.PersonId] = generator.GetNo(MasterPrefixPolicy.FAMILY_PERSON, MasterPrefixPolicy.FAMILY_PERSON_PERSON_ID, MasterPrefixPolicy.FAMILY_PERSON_PREFIX, YesNoPolicy.NO, MasterPrefixPolicy.FAMILY_PERSON_FORMAT);
}
foreach (FamilyPersonEntity p in data.Persons)
{
if (!insSet.Contains(p.PersonId)) continue; // 锚点以外的文件人(上溯段)一律不插
FamilyPersonEntity fresh = p.Clone();
fresh.PersonId = idMap[p.PersonId];
fresh.TreeId = targetTreeId;
fresh.FatherId = string.IsNullOrEmpty(p.FatherId) ? null : (idMap.ContainsKey(p.FatherId) ? idMap[p.FatherId] : null);
fresh.BindUserId = null;
fresh.IsBindUser = null;
fresh.CreatedBy = service.UserId;
fresh.CreatedDatetime = now;
fresh.UpdatedBy = service.UserId;
fresh.UpdatedDatetime = now;
repositoryPerson.InsertNow(fresh);
}
// 4) 关系行双端 remap(文件锚点端直接对接保留的目标锚点);引用集合外人的行丢弃防悬挂
foreach (FamilyTreeSpouseMapEntity m in data.SpouseMaps ?? [])
{
if (!idMap.ContainsKey(m.PersonId) || !idMap.ContainsKey(m.SpouseId)) continue;
if (m.PersonId == data.AnchorPersonId && m.SpouseId == data.AnchorPersonId) continue;
FamilyTreeSpouseMapEntity fresh = m.Clone();
fresh.TreeId = targetTreeId;
fresh.PersonId = idMap[m.PersonId];
fresh.SpouseId = idMap[m.SpouseId];
repositorySpouse.InsertNow(fresh);
}
foreach (FamilyTreeChildrenMapEntity m in data.ChildrenMaps ?? [])
{
if (!idMap.ContainsKey(m.PersonId) || !idMap.ContainsKey(m.ChildrenId)) continue;
FamilyTreeChildrenMapEntity fresh = m.Clone();
fresh.TreeId = targetTreeId;
fresh.PersonId = idMap[m.PersonId];
fresh.ChildrenId = idMap[m.ChildrenId];
repositoryChild.InsertNow(fresh);
}
// 4.5) 权限行迁移(用户拍板:按新旧编号关联保留绑定权限):被删后代/入配配偶的绑定成员随人换绑到新号,
// 关联不上的悬挂行删除;锚点行(id 未变)与枝外/管理员行不动
string permStat = MigrateSubTreePermissions(targetTreeId, new HashSet<string>(delPersons.Select(x => x.PersonId)), idMap);
// 5) 审计 MERGE(P7 独立动作码,统计口径与整谱替换的 IN 分开):Message 记合并报告;人数含锚点本人(方案 C 同步覆盖)
WriteExchangeLog("MERGE", targetTreeId, FamilyTreeExchangeScope.SUB, anchorPersonId, backupName, insSet.Count + 1, "OK", $"backup={backupName}; deleted={delPersons.Length}; inserted={insSet.Count}; anchor=1; anchorSpouse={delSpousePersons.Count}; perm {permStat}");
return insSet.Count + 1;
}
/// <summary>
/// 备份列表(R1 收紧:备份是谁的操作谁的回滚——成员只见自己创建的,管理员不限):
/// 只列本谱 BK 文件,R5 保留窗口(一年)内,按备份时刻降序;时刻以文件名为准,归属看 JSON 里的 ExportedBy。
/// </summary>
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeExchangeFileInfo[] ListBackups(string treeId)
{
if (string.IsNullOrEmpty(treeId)) throw new ValidatedException("请先选择族谱.");
FamilyTreePermissionHelper helper = new FamilyTreePermissionHelper(service);
if (!helper.IsMember(treeId)) throw new ValidatedException("您不是该族谱成员.");
bool isManager = helper.IsManager(treeId);
string dir = FamilyTreeExchangePolicy.GetBackupDirectory(service);
if (!Directory.Exists(dir)) return [];
List<FamilyTreeExchangeFileInfo> files = new List<FamilyTreeExchangeFileInfo>();
DateTime earliest = DateTime.Now.AddYears(-FamilyTreeExchangePolicy.BACKUP_KEEP_YEARS);
foreach (string path in Directory.GetFiles(dir, $"BK{treeId}_*.json"))
{
string name = Path.GetFileName(path);
var parsed = FamilyTreeExchangePolicy.ParseBackupFileName(name);
// 谱号不符(大小写变体防御)、时刻超出保留窗口、文件名解析失败的一律不列
if (parsed == null || parsed.Item1 != treeId || parsed.Item2 < earliest) continue;
// 非管理员只列自己创建的备份(读 JSON 取 ExportedBy,大小写不敏感;解析不出归属的旧文件对成员隐藏、管理员可见)
if (!isManager && ReadBackupOwner(path) != service.UserId) continue;
files.Add(new FamilyTreeExchangeFileInfo
{
FileName = name,
Size = new FileInfo(path).Length,
LastModified = parsed.Item2,
Scope = "BK",
TreeId = treeId,
AnchorPersonId = "",
});
}
return files.OrderByDescending(x => x.LastModified).ToArray();
}
/// <summary>从备份 JSON 轻量提取创建人(ExportedBy);密文先解密再读,读不到/损坏返 null</summary>
private static string ReadBackupOwner(string path)
{
try
{
using System.Text.Json.JsonDocument doc = System.Text.Json.JsonDocument.Parse(FamilyTreeExchangePolicy.DecryptBackup(File.ReadAllText(path)));
foreach (System.Text.Json.JsonProperty p in doc.RootElement.EnumerateObject())
if (string.Equals(p.Name, "ExportedBy", StringComparison.OrdinalIgnoreCase)) return p.Value.GetString();
}
catch { }
return null;
}
/// <summary>
/// 备份还原(R2:导小系/导全系出事,用替换前自动备份整谱回放退回):
/// R1 成员也可还原;R3 还原前先把现谱再备份一份(还原错了还能退回去);
/// R6 文件名严格 BK 格式且谱号必须等于目标谱(防路径穿越与跨谱灌数据)。
/// 还原走整谱替换本体:全员重编号、绑定清除,与整谱导入同副作用。
/// </summary>
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Update)]
[UnitOfWork]
public int RestoreBackup(string targetTreeId, string backupFileName)
{
FamilyTreeEntity target = repository.DetachedEntities.Where(x => x.TreeId == targetTreeId).FirstOrDefault();
if (target == null) throw new ValidatedException("目标族谱不存在.");
if (!new FamilyTreePermissionHelper(service).IsMember(targetTreeId)) throw new ValidatedException("您没有还原权限.");
// R6:格式闸(含目录成分拒绝)+ 归属闸(只能还原本谱的备份)+ R5 保留窗口闸
var parsed = FamilyTreeExchangePolicy.ParseBackupFileName(backupFileName ?? "");
if (parsed == null) throw new ValidatedException("备份文件名不合法.");
if (parsed.Item1 != targetTreeId) throw new ValidatedException("备份文件不属于本族谱,不能跨谱还原.");
if (parsed.Item2 < DateTime.Now.AddYears(-FamilyTreeExchangePolicy.BACKUP_KEEP_YEARS)) throw new ValidatedException("备份已超出保留窗口(一年),不允许还原.");
string path = Path.Combine(FamilyTreeExchangePolicy.GetBackupDirectory(service), backupFileName);
if (!File.Exists(path)) throw new ValidatedException("备份文件不存在或已被清理.");
FamilyTreeExchangeData data;
try
{
// 备份密文先解密(旧明文备份无 ENC: 前缀直接透传);直读需大小写不敏感,实体属性对不上时会静默空值,故下面验内容完整性
data = System.Text.Json.JsonSerializer.Deserialize<FamilyTreeExchangeData>(FamilyTreeExchangePolicy.DecryptBackup(File.ReadAllText(path)),
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (Exception ex)
{
throw new ValidatedException($"备份文件已损坏,无法解析: {ex.Message}");
}
if (data == null || data.Persons == null || data.Persons.Length == 0 || data.FamilyTree == null)
throw new ValidatedException("备份内容不完整,禁止还原.");
// R1 收紧:成员只能还原自己创建的备份(防绕过列表直调接口);管理员不限
if (!new FamilyTreePermissionHelper(service).IsManager(targetTreeId) && data.ExportedBy != service.UserId)
throw new ValidatedException("该备份由其他用户创建,成员只能还原自己的备份.");
return ReplaceTreeCore(target, data, "RESTORE");
}
/// <summary>D7 审计:写 family_tree_file_log;失败不阻断主流程(日志丢了可以补,导不出去的体验才是大事)</summary>
private void WriteExchangeLog(string action, string treeId, string scope, string anchorPersonId, string fileName, int personCount, string result, string message)
{
try
{
IRepository<FamilyTreeFileLogEntity> repositoryLog = repository.GetRepository<IRepository<FamilyTreeFileLogEntity>>();
DateTime now = DateTime.Now;
repositoryLog.InsertNow(new FamilyTreeFileLogEntity
{
CreatedDatetime = now,
CreatedBy = service.UserId,
UpdatedDatetime = now,
UpdatedBy = service.UserId,
TreeId = treeId,
Action = action,
Scope = scope,
AnchorPersonId = string.IsNullOrEmpty(anchorPersonId) ? null : anchorPersonId,
FileName = fileName,
PersonCount = personCount,
Result = result,
Message = message,
});
}
catch (Exception ex) { Console.WriteLine($"[FamilyTreeExchange] 审计写入失败: {ex.Message}"); }
}
/// <summary>
/// 族谱「统计」 tab 的聚合查询:总数/男/女/世代层数 + 各世代人数分布。
/// 世代 = 始祖起 BFS 深度(始祖=1 世);未连到始祖的旁支不入世代桶,只计入 Total。
/// 与 ListExchangeFiles/SaveExchangeFile 同套成员闸:非本族成员不能拉本族聚合数据。
/// </summary>
[PermissionOperate(name: MasterPermissionPolicy.FamilyTree, operate: Operate.Find)]
public FamilyTreeStatisticsData GetStatistics(string treeId)
{
FamilyTreeStatisticsData data = new FamilyTreeStatisticsData();
if (string.IsNullOrEmpty(treeId)) return data;
FamilyTreeEntity d = repository.DetachedEntities.Where(x => x.TreeId == treeId).FirstOrDefault();
if (d == null) throw new ValidatedException("族谱不存在.");
var helper = new FamilyTreePermissionHelper(service);
if (!helper.IsMember(treeId) && !helper.IsManager(treeId))
throw new ValidatedException("您不是该族谱成员.");
IRepository<FamilyPersonEntity> repositoryPerson = repository.GetRepository<IRepository<FamilyPersonEntity>>();
IRepository<FamilyTreeChildrenMapEntity> repositoryChildrenMap = repository.GetRepository<IRepository<FamilyTreeChildrenMapEntity>>();
FamilyPersonEntity[] persons = repositoryPerson.DetachedEntities.Where(x => x.TreeId == treeId).ToArray();
FamilyTreeChildrenMapEntity[] childrenMaps = repositoryChildrenMap.DetachedEntities.Where(x => x.TreeId == treeId).ToArray();
data.Total = persons.Length;
data.Male = persons.Count(x => x.Gender == "M");
data.Female = persons.Count(x => x.Gender == "W");
// BFS 从始祖起逐层下钻,记录每人世代号;genByPerson 去重兼做环保护
Dictionary<string, int> genByPerson = new Dictionary<string, int>();
if (!string.IsNullOrEmpty(d.AncestorPersonId))
{
Queue<KeyValuePair<string, int>> q = new Queue<KeyValuePair<string, int>>();
q.Enqueue(new KeyValuePair<string, int>(d.AncestorPersonId, 1));
genByPerson[d.AncestorPersonId] = 1;
while (q.Count > 0)
{
var cur = q.Dequeue();
foreach (var c in childrenMaps.Where(x => x.PersonId == cur.Key))
{
if (string.IsNullOrEmpty(c.ChildrenId) || genByPerson.ContainsKey(c.ChildrenId)) continue;
genByPerson[c.ChildrenId] = cur.Value + 1;
q.Enqueue(new KeyValuePair<string, int>(c.ChildrenId, cur.Value + 1));
}
}
}
data.GenerationCount = genByPerson.Count > 0 ? genByPerson.Values.Max() : 0;
// 按世代升序聚合;未入桶的旁支不入世代分布(仅计 Total)
data.PerGeneration = genByPerson.Values
.GroupBy(g => g)
.OrderBy(grp => grp.Key)
.Select(grp => new FamilyTreeGenerationCountData { Generation = grp.Key, Count = grp.Count() })
.ToArray();
return data;
}
}
}