using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing; using Admin.NET.Plugin.AiDOP.Dto.S0.Sales; using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing; [ApiController] [Route("api/s0/manufacturing/line-posts")] [NonUnify] public class AdoS0MfgLinePostsController : ControllerBase { private readonly SqlSugarRepository _postRep; private readonly SqlSugarRepository _skillRep; private readonly SqlSugarRepository _personSkillRep; private readonly SqlSugarRepository _lineMasterRep; public AdoS0MfgLinePostsController( SqlSugarRepository postRep, SqlSugarRepository skillRep, SqlSugarRepository personSkillRep, SqlSugarRepository lineMasterRep) { _postRep = postRep; _skillRep = skillRep; _personSkillRep = personSkillRep; _lineMasterRep = lineMasterRep; } [HttpGet] public async Task GetPagedAsync([FromQuery] AdoS0LineSkillMasterQueryDto q) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var page = q.Page; var pageSize = q.PageSize; (page, pageSize) = PagingGuard.Normalize(page, pageSize); var domain = q.Domain?.Trim(); var query = _postRep.ScopedTo(tenantId) .WhereIF(!string.IsNullOrWhiteSpace(domain), x => x.Domain == domain) .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.ProdLine.Contains(q.Keyword!) || x.JOBNo.Contains(q.Keyword!) || (x.JOBDescr != null && x.JOBDescr.Contains(q.Keyword!))); var total = await query.CountAsync(); var list = await query .OrderByDescending(x => x.UpdateTime ?? x.CreateTime) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); // 补展示字段:按 (Domain, ProdLine) 元组反查 LineMaster.Describe,避免跨域错配 var domains = list.Select(x => x.Domain).Distinct().ToList(); var lineCodes = list.Select(x => x.ProdLine).Distinct().ToList(); var lineMap = await _lineMasterRep.ScopedTo(tenantId) .Where(lm => domains.Contains(lm.Domain) && lineCodes.Contains(lm.Line)) .Select(lm => new { lm.Domain, lm.Line, lm.Describe }) .ToListAsync(); var describeByKey = lineMap .GroupBy(x => (x.Domain, x.Line)) .ToDictionary(g => g.Key, g => g.First().Describe ?? string.Empty); var shaped = list.Select(x => new { id = x.Id, domain = x.Domain, prodLine = x.ProdLine, prodLineDisplay = $"{x.ProdLine} {(describeByKey.TryGetValue((x.Domain, x.ProdLine), out var d) ? d : string.Empty)}".Trim(), jobNo = x.JOBNo, jobDescr = x.JOBDescr, site = x.Site, orderby = x.Orderby, wType = x.WType, runCrew = x.RunCrew, createUser = x.CreateUser, createTime = x.CreateTime, updateUser = x.UpdateUser, updateTime = x.UpdateTime }).ToList(); return Ok(new { total, page, pageSize, list = shaped }); } [HttpGet("{id:long}")] public async Task GetDetailAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var header = await _postRep.ByIdScopedAsync(id, tenantId); if (header == null) return NotFound(); var skills = await _skillRep.ScopedTo(tenantId) .Where(x => x.LineSkillMasterId == id) .OrderBy(x => x.Id) .ToListAsync(); // 补显人员技能的业务编码/名称:一次批量查询 + 字典映射,杜绝 N+1; // 显式限定当前租户,不跨租户取名。 await FillPersonSkillDisplayAsync(skills, tenantId); return Ok(new { header, linePostSkills = skills }); } [HttpPost] public async Task CreateAsync([FromBody] AdoS0LineSkillMasterUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var (fkErr, skillMap) = await ResolvePersonSkillCodesAsync(dto, tenantId); if (fkErr != null) return BadRequest(new { message = fkErr }); var db = _postRep.Context; await db.Ado.BeginTranAsync(); try { var now = DateTime.Now; var post = new AdoS0LineSkillMaster { TenantId = tenantId, Domain = dto.Domain.Trim(), ProdLine = dto.ProdLine.Trim(), JOBNo = dto.JOBNo.Trim(), JOBDescr = dto.JOBDescr, Site = dto.Site, Orderby = dto.Orderby, WType = dto.WType, RunCrew = dto.RunCrew, CreateUser = dto.CreateUser ?? dto.UpdateUser, CreateTime = dto.CreateTime ?? dto.UpdateTime ?? now, UpdateUser = dto.UpdateUser, UpdateTime = dto.UpdateTime }; post = await _postRep.AsInsertable(post).ExecuteReturnEntityAsync(); foreach (var s in dto.Skills) { var row = MapSkill(post, s, tenantId, skillMap); await _skillRep.AsInsertable(row).ExecuteCommandAsync(); } await db.Ado.CommitTranAsync(); return await GetDetailAsync(post.Id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpPut("{id:long}")] public async Task UpdateAsync(long id, [FromBody] AdoS0LineSkillMasterUpsertDto dto) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var header = await _postRep.ByIdScopedAsync(id, tenantId); if (header == null) return NotFound(); var (fkErr, skillMap) = await ResolvePersonSkillCodesAsync(dto, tenantId); if (fkErr != null) return BadRequest(new { message = fkErr }); var db = _postRep.Context; await db.Ado.BeginTranAsync(); try { header.Domain = dto.Domain.Trim(); header.ProdLine = dto.ProdLine.Trim(); header.JOBNo = dto.JOBNo.Trim(); header.JOBDescr = dto.JOBDescr; header.Site = dto.Site; header.Orderby = dto.Orderby; header.WType = dto.WType; header.RunCrew = dto.RunCrew; header.UpdateUser = dto.UpdateUser; header.UpdateTime = dto.UpdateTime ?? DateTime.Now; await _postRep.AsUpdateable(header).ExecuteCommandAsync(); await _skillRep.AsDeleteable().Where(x => x.LineSkillMasterId == id && x.TenantId == tenantId).ExecuteCommandAsync(); foreach (var s in dto.Skills) { var row = MapSkill(header, s, tenantId, skillMap); await _skillRep.AsInsertable(row).ExecuteCommandAsync(); } await db.Ado.CommitTranAsync(); return await GetDetailAsync(id); } catch { await db.Ado.RollbackTranAsync(); throw; } } [HttpDelete("{id:long}")] public async Task DeleteAsync(long id) { if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!; var header = await _postRep.ByIdScopedAsync(id, tenantId); if (header == null) return NotFound(); var db = _postRep.Context; await db.Ado.BeginTranAsync(); try { await _skillRep.AsDeleteable().Where(x => x.LineSkillMasterId == id && x.TenantId == tenantId).ExecuteCommandAsync(); await _postRep.DeleteAsync(header); await db.Ado.CommitTranAsync(); return Ok(new { message = "删除成功" }); } catch { await db.Ado.RollbackTranAsync(); throw; } } /// /// 把提交的人员技能【业务编码】解析为当前租户下的 PersonSkill.Id。 /// /// 作用域依据:uk_PersonSkill_code = UNIQUE (tenant_id, CompanyRefId, FactoryRefId, Code)。 /// PersonSkill 表**没有 Domain 列**,故不按 Domain 收窄;产线岗位主表也不带 /// CompanyRefId / FactoryRefId,无法据此再收窄,因此按 (租户, Code) 解析并对 /// 多义(同租户下跨公司/工厂重码)显式报错,而不是随便取一条。 /// /// 返回 error 时不写库;成功时返回 code -> id 映射。 /// private async Task<(string? Error, Dictionary Map)> ResolvePersonSkillCodesAsync( AdoS0LineSkillMasterUpsertDto dto, long tenantId) { var map = new Dictionary(StringComparer.Ordinal); if (dto.Skills == null || dto.Skills.Count == 0) return (null, map); var codes = dto.Skills .Select(s => (s.PersonSkillCode ?? string.Empty).Trim()) .Where(c => c.Length > 0) .Distinct(StringComparer.Ordinal) .ToList(); if (codes.Count == 0) return ("人员技能不能为空", map); var rows = await _personSkillRep.ScopedTo(tenantId) .Where(p => codes.Contains(p.Code)) .Select(p => new { p.Id, p.Code }) .ToListAsync(); foreach (var code in codes) { var hits = rows.Where(r => string.Equals(r.Code, code, StringComparison.Ordinal)).ToList(); if (hits.Count == 0) return ($"人员技能 {code} 不存在或不属于当前作用域", map); if (hits.Count > 1) return ($"人员技能编码 {code} 在当前租户下存在 {hits.Count} 条同码主数据,无法唯一确定,请先处理主数据重码", map); map[code] = hits[0].Id; } return (null, map); } /// 批量补显 PersonSkill 的 Code / Name(单次查询 + 字典映射,无 N+1)。 private async Task FillPersonSkillDisplayAsync(List rows, long tenantId) { if (rows.Count == 0) return; var ids = rows.Select(r => r.PersonSkillId).Distinct().ToList(); var skills = await _personSkillRep.ScopedTo(tenantId) .Where(p => ids.Contains(p.Id)) .Select(p => new { p.Id, p.Code, p.Name }) .ToListAsync(); var map = skills.GroupBy(x => x.Id).ToDictionary(g => g.Key, g => g.First()); foreach (var r in rows) { if (map.TryGetValue(r.PersonSkillId, out var p)) { r.PersonSkillCode = p.Code; r.PersonSkillName = p.Name; } } } /// /// 明细行映射。除新体系字段外,同时回填旧系统遗留业务键 /// (Domain / Site / ProdLine / JOBNo 取自主表,SkillNo 取自提交的技能业务编码), /// 避免 Update 的「删除+重插」把这些列写成 NULL。 /// private static AdoS0LineSkillDetail MapSkill(AdoS0LineSkillMaster master, AdoS0LineSkillDetailUpsertDto s, long tenantId, IReadOnlyDictionary skillMap) { var code = (s.PersonSkillCode ?? string.Empty).Trim(); return new AdoS0LineSkillDetail { TenantId = tenantId, LineSkillMasterId = master.Id, PersonSkillId = skillMap[code], Domain = master.Domain, Site = master.Site, ProdLine = master.ProdLine, JOBNo = master.JOBNo, SkillNo = code, RequiredLevel = s.RequiredLevel, Remark = s.Remark, EffectiveDate = s.EffectiveDate, IsEnabled = s.IsEnabled, CreateTime = DateTime.Now }; } }