namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
///
/// S7-FQC-INITIATOR-TENANT-GUARD-1:S7 成品检验「检验负责人 / 流程发起人」的租户门禁。
///
/// 要守住的不变量:对任何新进入 S7 FQC 流程的单据,
/// BusinessTenant == jyfzr 用户.TenantId == ApprovalFlowInstance.Initiator 用户.TenantId。
/// S7 的契约是三者恒等(FqcInspBillFlowService.SubmitResult 的负责人门与
/// FqcTaskEntryService.GenerateInspBill 的认领人门共同保证
/// jyfzr == 当前用户 == N1 Assignee),因此只要 jyfzr 一定同租户,
/// Initiator 就一定同租户。
///
/// 为什么原来会破(已取证):写入侧与读取侧的租户口径不一致 ——
///
/// - Claim 写 _userManager.UserId,而 UserManager.UserId/TenantId 都取自 JWT claim;
/// SysTenantService.ChangeTenant 只把**内存中**的 user.TenantId 改成目标租户后重签 token、
/// 并不持久化,于是超管切租户后 _userManager.TenantId=目标租户 而 SysUser.TenantId=默认租户,
/// 一个默认租户身份的用户就被写成了目标租户单据的负责人。
/// - Assign 写 InspectorUserId 时**完全不查用户**,只过滤单据租户。
/// - FlowEngineService:125 起流时 InitiatorId = _userManager.UserId,照实捕获该用户;
/// 而 ResolveApprovers 的 Initiator 分支用 _userRep.GetByIdAsync(无 ClearFilter)
/// 按**环境租户**过滤 → 该发起人此后永远解析不出来 → ReturnToPrev 到 N1 时报「未配置审批人」。
///
///
/// 为什么守在 S7 业务边界而不是引擎:引擎照实记录操作者是正确行为,
/// 改 FlowEngineService 会把影响面扩散到全部 BizType。
/// 真正错的是「让一个不属于该租户的人成为该租户单据的负责人」,那是 S7 的业务规则,
/// 因此三道防线全部落在 S7 自己的入口上(Assign / Claim / 起流边界)。
///
/// 平台权限 ≠ 业务身份:超管有跨租户访问与管理能力,但除非它真实属于该租户,
/// 就不能成为该租户的 FQC 检验负责人与流程发起人 —— 否则它发起的流程在退回 N1 时无人可解析。
///
internal static class S7FqcInitiatorTenantGuard
{
///
/// 校验「 可否作为租户 单据的检验负责人/发起人」。
///
/// 用 ClearFilter<ITenantIdFilter>() 跨租户查用户是**故意的**:
/// 若带租户过滤查,跨租户用户会表现为「查不到」,就无法把「用户不存在」与
/// 「用户存在但不属于本租户」区分开,只能给出误导性的文案。
///
/// 对外文案不暴露 TenantId / RoleId / 内部过滤器细节。
///
/// SqlSugar 客户端
/// 待校验用户 Id
/// 单据所属租户(由业务行/业务上下文确定,非登录 token 直接透传)
/// 文案主语:如「指定检验负责人」「当前用户」
public static async Task EnsureCanOwnFqcAsync(
ISqlSugarClient db, long userId, long businessTenantId, string subject)
{
if (userId <= 0)
throw Oops.Oh($"{subject}无效,无法作为检验负责人");
var user = await db.Queryable()
.ClearFilter()
.Where(u => u.Id == userId)
.Select(u => new GuardUser { Id = u.Id, TenantId = u.TenantId, Status = u.Status })
.FirstAsync();
if (user == null)
throw Oops.Oh($"{subject}不存在,无法作为检验负责人");
if (user.Status != StatusEnum.Enable)
throw Oops.Oh($"{subject}已停用,无法作为检验负责人");
if (user.TenantId != businessTenantId)
throw Oops.Oh($"{subject}不属于当前单据租户,不能作为该单据的检验负责人");
}
///
/// 起流前的最后一道:校验业务行上记录的 jyfzr 本身合法且同租户。
///
/// 这道防线不可省:库内**已存在**先于本守卫写入的坏 jyfzr
/// (既有跨租户用户 Id,也有根本不是用户 Id 的字符串)。只在 Assign/Claim 设卡,
/// 挡不住这些存量单据将来被起流而再造一条跨租户 Initiator 实例。
///
public static async Task EnsureBillOwnerTenantSafeAsync(
ISqlSugarClient db, string? jyfzr, long businessTenantId)
{
var raw = (jyfzr ?? string.Empty).Trim();
if (raw.Length == 0)
throw Oops.Oh("该报检尚未指定检验负责人,无法启动检验流程");
// jyfzr 语义是 SysUser.Id;存量数据里存在非 Id 的历史值,必须显式拒绝而不是当 0 处理。
if (!long.TryParse(raw, out var ownerId) || ownerId <= 0)
throw Oops.Oh("该报检的检验负责人记录无效,请重新指定负责人后再启动检验流程");
await EnsureCanOwnFqcAsync(db, ownerId, businessTenantId, "该报检的检验负责人");
}
/// 只取校验所需三列,避免整实体投影把无关列带进来。
private sealed class GuardUser
{
public long Id { get; set; }
public long? TenantId { get; set; }
public StatusEnum Status { get; set; }
}
}