AdoS0MfgLinePostsController.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing;
  2. using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
  3. using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure;
  5. namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
  6. [ApiController]
  7. [Route("api/s0/manufacturing/line-posts")]
  8. [NonUnify]
  9. public class AdoS0MfgLinePostsController : ControllerBase
  10. {
  11. private readonly SqlSugarRepository<AdoS0LineSkillMaster> _postRep;
  12. private readonly SqlSugarRepository<AdoS0LineSkillDetail> _skillRep;
  13. private readonly SqlSugarRepository<AdoS0MfgPersonSkill> _personSkillRep;
  14. private readonly SqlSugarRepository<AdoS0LineMaster> _lineMasterRep;
  15. public AdoS0MfgLinePostsController(
  16. SqlSugarRepository<AdoS0LineSkillMaster> postRep,
  17. SqlSugarRepository<AdoS0LineSkillDetail> skillRep,
  18. SqlSugarRepository<AdoS0MfgPersonSkill> personSkillRep,
  19. SqlSugarRepository<AdoS0LineMaster> lineMasterRep)
  20. {
  21. _postRep = postRep;
  22. _skillRep = skillRep;
  23. _personSkillRep = personSkillRep;
  24. _lineMasterRep = lineMasterRep;
  25. }
  26. [HttpGet]
  27. public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0LineSkillMasterQueryDto q)
  28. {
  29. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  30. var page = q.Page;
  31. var pageSize = q.PageSize;
  32. (page, pageSize) = PagingGuard.Normalize(page, pageSize);
  33. var domain = q.Domain?.Trim();
  34. var query = _postRep.ScopedTo(tenantId)
  35. .WhereIF(!string.IsNullOrWhiteSpace(domain), x => x.Domain == domain)
  36. .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
  37. x => x.ProdLine.Contains(q.Keyword!) || x.JOBNo.Contains(q.Keyword!) || (x.JOBDescr != null && x.JOBDescr.Contains(q.Keyword!)));
  38. var total = await query.CountAsync();
  39. var list = await query
  40. .OrderByDescending(x => x.UpdateTime ?? x.CreateTime)
  41. .Skip((page - 1) * pageSize)
  42. .Take(pageSize)
  43. .ToListAsync();
  44. // 补展示字段:按 (Domain, ProdLine) 元组反查 LineMaster.Describe,避免跨域错配
  45. var domains = list.Select(x => x.Domain).Distinct().ToList();
  46. var lineCodes = list.Select(x => x.ProdLine).Distinct().ToList();
  47. var lineMap = await _lineMasterRep.ScopedTo(tenantId)
  48. .Where(lm => domains.Contains(lm.Domain) && lineCodes.Contains(lm.Line))
  49. .Select(lm => new { lm.Domain, lm.Line, lm.Describe })
  50. .ToListAsync();
  51. var describeByKey = lineMap
  52. .GroupBy(x => (x.Domain, x.Line))
  53. .ToDictionary(g => g.Key, g => g.First().Describe ?? string.Empty);
  54. var shaped = list.Select(x => new
  55. {
  56. id = x.Id,
  57. domain = x.Domain,
  58. prodLine = x.ProdLine,
  59. prodLineDisplay = $"{x.ProdLine} {(describeByKey.TryGetValue((x.Domain, x.ProdLine), out var d) ? d : string.Empty)}".Trim(),
  60. jobNo = x.JOBNo,
  61. jobDescr = x.JOBDescr,
  62. site = x.Site,
  63. orderby = x.Orderby,
  64. wType = x.WType,
  65. runCrew = x.RunCrew,
  66. createUser = x.CreateUser,
  67. createTime = x.CreateTime,
  68. updateUser = x.UpdateUser,
  69. updateTime = x.UpdateTime
  70. }).ToList();
  71. return Ok(new { total, page, pageSize, list = shaped });
  72. }
  73. [HttpGet("{id:long}")]
  74. public async Task<IActionResult> GetDetailAsync(long id)
  75. {
  76. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  77. var header = await _postRep.ByIdScopedAsync(id, tenantId);
  78. if (header == null) return NotFound();
  79. var skills = await _skillRep.ScopedTo(tenantId)
  80. .Where(x => x.LineSkillMasterId == id)
  81. .OrderBy(x => x.Id)
  82. .ToListAsync();
  83. // 补显人员技能的业务编码/名称:一次批量查询 + 字典映射,杜绝 N+1;
  84. // 显式限定当前租户,不跨租户取名。
  85. await FillPersonSkillDisplayAsync(skills, tenantId);
  86. return Ok(new { header, linePostSkills = skills });
  87. }
  88. [HttpPost]
  89. public async Task<IActionResult> CreateAsync([FromBody] AdoS0LineSkillMasterUpsertDto dto)
  90. {
  91. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  92. var (fkErr, skillMap) = await ResolvePersonSkillCodesAsync(dto, tenantId);
  93. if (fkErr != null) return BadRequest(new { message = fkErr });
  94. var db = _postRep.Context;
  95. await db.Ado.BeginTranAsync();
  96. try
  97. {
  98. var now = DateTime.Now;
  99. var post = new AdoS0LineSkillMaster
  100. {
  101. TenantId = tenantId,
  102. Domain = dto.Domain.Trim(),
  103. ProdLine = dto.ProdLine.Trim(),
  104. JOBNo = dto.JOBNo.Trim(),
  105. JOBDescr = dto.JOBDescr,
  106. Site = dto.Site,
  107. Orderby = dto.Orderby,
  108. WType = dto.WType,
  109. RunCrew = dto.RunCrew,
  110. CreateUser = dto.CreateUser ?? dto.UpdateUser,
  111. CreateTime = dto.CreateTime ?? dto.UpdateTime ?? now,
  112. UpdateUser = dto.UpdateUser,
  113. UpdateTime = dto.UpdateTime
  114. };
  115. post = await _postRep.AsInsertable(post).ExecuteReturnEntityAsync();
  116. foreach (var s in dto.Skills)
  117. {
  118. var row = MapSkill(post, s, tenantId, skillMap);
  119. await _skillRep.AsInsertable(row).ExecuteCommandAsync();
  120. }
  121. await db.Ado.CommitTranAsync();
  122. return await GetDetailAsync(post.Id);
  123. }
  124. catch
  125. {
  126. await db.Ado.RollbackTranAsync();
  127. throw;
  128. }
  129. }
  130. [HttpPut("{id:long}")]
  131. public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0LineSkillMasterUpsertDto dto)
  132. {
  133. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  134. var header = await _postRep.ByIdScopedAsync(id, tenantId);
  135. if (header == null) return NotFound();
  136. var (fkErr, skillMap) = await ResolvePersonSkillCodesAsync(dto, tenantId);
  137. if (fkErr != null) return BadRequest(new { message = fkErr });
  138. var db = _postRep.Context;
  139. await db.Ado.BeginTranAsync();
  140. try
  141. {
  142. header.Domain = dto.Domain.Trim();
  143. header.ProdLine = dto.ProdLine.Trim();
  144. header.JOBNo = dto.JOBNo.Trim();
  145. header.JOBDescr = dto.JOBDescr;
  146. header.Site = dto.Site;
  147. header.Orderby = dto.Orderby;
  148. header.WType = dto.WType;
  149. header.RunCrew = dto.RunCrew;
  150. header.UpdateUser = dto.UpdateUser;
  151. header.UpdateTime = dto.UpdateTime ?? DateTime.Now;
  152. await _postRep.AsUpdateable(header).ExecuteCommandAsync();
  153. await _skillRep.AsDeleteable().Where(x => x.LineSkillMasterId == id && x.TenantId == tenantId).ExecuteCommandAsync();
  154. foreach (var s in dto.Skills)
  155. {
  156. var row = MapSkill(header, s, tenantId, skillMap);
  157. await _skillRep.AsInsertable(row).ExecuteCommandAsync();
  158. }
  159. await db.Ado.CommitTranAsync();
  160. return await GetDetailAsync(id);
  161. }
  162. catch
  163. {
  164. await db.Ado.RollbackTranAsync();
  165. throw;
  166. }
  167. }
  168. [HttpDelete("{id:long}")]
  169. public async Task<IActionResult> DeleteAsync(long id)
  170. {
  171. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  172. var header = await _postRep.ByIdScopedAsync(id, tenantId);
  173. if (header == null) return NotFound();
  174. var db = _postRep.Context;
  175. await db.Ado.BeginTranAsync();
  176. try
  177. {
  178. await _skillRep.AsDeleteable().Where(x => x.LineSkillMasterId == id && x.TenantId == tenantId).ExecuteCommandAsync();
  179. await _postRep.DeleteAsync(header);
  180. await db.Ado.CommitTranAsync();
  181. return Ok(new { message = "删除成功" });
  182. }
  183. catch
  184. {
  185. await db.Ado.RollbackTranAsync();
  186. throw;
  187. }
  188. }
  189. /// <summary>
  190. /// 把提交的人员技能【业务编码】解析为当前租户下的 PersonSkill.Id。
  191. ///
  192. /// 作用域依据:uk_PersonSkill_code = UNIQUE (tenant_id, CompanyRefId, FactoryRefId, Code)。
  193. /// PersonSkill 表**没有 Domain 列**,故不按 Domain 收窄;产线岗位主表也不带
  194. /// CompanyRefId / FactoryRefId,无法据此再收窄,因此按 (租户, Code) 解析并对
  195. /// 多义(同租户下跨公司/工厂重码)显式报错,而不是随便取一条。
  196. ///
  197. /// 返回 error 时不写库;成功时返回 code -&gt; id 映射。
  198. /// </summary>
  199. private async Task<(string? Error, Dictionary<string, long> Map)> ResolvePersonSkillCodesAsync(
  200. AdoS0LineSkillMasterUpsertDto dto, long tenantId)
  201. {
  202. var map = new Dictionary<string, long>(StringComparer.Ordinal);
  203. if (dto.Skills == null || dto.Skills.Count == 0)
  204. return (null, map);
  205. var codes = dto.Skills
  206. .Select(s => (s.PersonSkillCode ?? string.Empty).Trim())
  207. .Where(c => c.Length > 0)
  208. .Distinct(StringComparer.Ordinal)
  209. .ToList();
  210. if (codes.Count == 0)
  211. return ("人员技能不能为空", map);
  212. var rows = await _personSkillRep.ScopedTo(tenantId)
  213. .Where(p => codes.Contains(p.Code))
  214. .Select(p => new { p.Id, p.Code })
  215. .ToListAsync();
  216. foreach (var code in codes)
  217. {
  218. var hits = rows.Where(r => string.Equals(r.Code, code, StringComparison.Ordinal)).ToList();
  219. if (hits.Count == 0)
  220. return ($"人员技能 {code} 不存在或不属于当前作用域", map);
  221. if (hits.Count > 1)
  222. return ($"人员技能编码 {code} 在当前租户下存在 {hits.Count} 条同码主数据,无法唯一确定,请先处理主数据重码", map);
  223. map[code] = hits[0].Id;
  224. }
  225. return (null, map);
  226. }
  227. /// <summary>批量补显 PersonSkill 的 Code / Name(单次查询 + 字典映射,无 N+1)。</summary>
  228. private async Task FillPersonSkillDisplayAsync(List<AdoS0LineSkillDetail> rows, long tenantId)
  229. {
  230. if (rows.Count == 0) return;
  231. var ids = rows.Select(r => r.PersonSkillId).Distinct().ToList();
  232. var skills = await _personSkillRep.ScopedTo(tenantId)
  233. .Where(p => ids.Contains(p.Id))
  234. .Select(p => new { p.Id, p.Code, p.Name })
  235. .ToListAsync();
  236. var map = skills.GroupBy(x => x.Id).ToDictionary(g => g.Key, g => g.First());
  237. foreach (var r in rows)
  238. {
  239. if (map.TryGetValue(r.PersonSkillId, out var p))
  240. {
  241. r.PersonSkillCode = p.Code;
  242. r.PersonSkillName = p.Name;
  243. }
  244. }
  245. }
  246. /// <summary>
  247. /// 明细行映射。除新体系字段外,同时回填旧系统遗留业务键
  248. /// (Domain / Site / ProdLine / JOBNo 取自主表,SkillNo 取自提交的技能业务编码),
  249. /// 避免 Update 的「删除+重插」把这些列写成 NULL。
  250. /// </summary>
  251. private static AdoS0LineSkillDetail MapSkill(AdoS0LineSkillMaster master, AdoS0LineSkillDetailUpsertDto s,
  252. long tenantId, IReadOnlyDictionary<string, long> skillMap)
  253. {
  254. var code = (s.PersonSkillCode ?? string.Empty).Trim();
  255. return new AdoS0LineSkillDetail
  256. {
  257. TenantId = tenantId,
  258. LineSkillMasterId = master.Id,
  259. PersonSkillId = skillMap[code],
  260. Domain = master.Domain,
  261. Site = master.Site,
  262. ProdLine = master.ProdLine,
  263. JOBNo = master.JOBNo,
  264. SkillNo = code,
  265. RequiredLevel = s.RequiredLevel,
  266. Remark = s.Remark,
  267. EffectiveDate = s.EffectiveDate,
  268. IsEnabled = s.IsEnabled,
  269. CreateTime = DateTime.Now
  270. };
  271. }
  272. }