AdoS0ProcessFlowCardsController.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Manufacturing;
  5. /// <summary>
  6. /// 工序流转卡主数据 CRUD(数据源:S0ProcessFlowCard 主表)。
  7. ///
  8. /// 恢复主表链路:本 Controller 完全读写 S0ProcessFlowCard,替换此前误接 legacy MissedPrint 的只读投影。
  9. /// 路由沿用 /api/s0/manufacturing/process-flow-cards,接口地址不变 → 前端 / 菜单 / 权限链路兼容。
  10. ///
  11. /// 组织兜底(后端强制,不只依赖前端 payload):
  12. /// - 新增:公司/工厂由后端按 S0 默认组织(SysOrg Code=S0-C001 / S0-F001)解析并 stamp,忽略 payload 组织值 → 杜绝跨厂注入;
  13. /// - 编辑:公司/工厂 pin 到既有记录原值,忽略 payload 组织值 → 杜绝跨公司/跨工厂篡改;
  14. /// - 详情/删除/启停:按 id 操作,不接受组织入参。
  15. /// 唯一性:依赖 DB 唯一索引 uk_S0ProcessFlowCard_factory_flowcard (factory_ref_id, flow_card_no);
  16. /// 预检友好 409,并发竞态由唯一索引兜底(AdoS0ExceptionFilter 将重复键映射为友好 409)。
  17. /// </summary>
  18. [ApiController]
  19. [Route("api/s0/manufacturing/process-flow-cards")]
  20. [AllowAnonymous]
  21. [NonUnify]
  22. public class AdoS0ProcessFlowCardsController : ControllerBase
  23. {
  24. private const string DefaultCompanyCode = "S0-C001";
  25. private const string DefaultFactoryCode = "S0-F001";
  26. private readonly SqlSugarRepository<AdoS0ProcessFlowCard> _rep;
  27. private readonly SqlSugarRepository<AdoS0MfgRoutingOpDetail> _routingRep;
  28. private readonly SqlSugarRepository<SysOrg> _orgRep;
  29. public AdoS0ProcessFlowCardsController(
  30. SqlSugarRepository<AdoS0ProcessFlowCard> rep,
  31. SqlSugarRepository<AdoS0MfgRoutingOpDetail> routingRep,
  32. SqlSugarRepository<SysOrg> orgRep)
  33. {
  34. _rep = rep;
  35. _routingRep = routingRep;
  36. _orgRep = orgRep;
  37. }
  38. [HttpGet]
  39. public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ProcessFlowCardQueryDto q)
  40. {
  41. var page = q.EffectivePage;
  42. var pageSize = q.PageSize;
  43. (page, pageSize) = PagingGuard.Normalize(page, pageSize);
  44. var query = _rep.AsQueryable()
  45. .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
  46. .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
  47. .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
  48. .WhereIF(!string.IsNullOrWhiteSpace(q.WorkOrderNo), x => x.WorkOrderNo.Contains(q.WorkOrderNo!))
  49. .WhereIF(!string.IsNullOrWhiteSpace(q.FlowCardNo), x => x.FlowCardNo.Contains(q.FlowCardNo!))
  50. .WhereIF(!string.IsNullOrWhiteSpace(q.ItemNum), x => x.ItemNum.Contains(q.ItemNum!))
  51. .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value)
  52. .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword),
  53. x => x.WorkOrderNo.Contains(q.Keyword!)
  54. || x.FlowCardNo.Contains(q.Keyword!)
  55. || x.ItemNum.Contains(q.Keyword!)
  56. || (x.ItemName != null && x.ItemName.Contains(q.Keyword!)));
  57. var total = await query.CountAsync();
  58. var list = await query
  59. .OrderByDescending(x => x.CreateTime)
  60. .OrderByDescending(x => x.Id)
  61. .Skip((page - 1) * pageSize).Take(pageSize)
  62. .ToListAsync();
  63. return Ok(new { total, page, pageSize, list });
  64. }
  65. [HttpGet("{id:long}")]
  66. public async Task<IActionResult> GetAsync(long id)
  67. {
  68. var item = await _rep.GetByIdAsync(id);
  69. return item == null ? AdoS0ApiErrors.NotFound("流转卡不存在") : Ok(item);
  70. }
  71. [HttpPost]
  72. public async Task<IActionResult> CreateAsync([FromBody] AdoS0ProcessFlowCardUpsertDto dto)
  73. {
  74. var (companyId, factoryId) = await ResolveDefaultOrgAsync();
  75. if (companyId <= 0 || factoryId <= 0)
  76. return AdoS0ApiErrors.InvalidRequest("S0 默认组织未初始化(SysOrg S0-C001 / S0-F001 缺失),无法创建流转卡");
  77. var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
  78. if (await _rep.IsAnyAsync(x => x.FactoryRefId == factoryId && x.FlowCardNo == flowCardNo))
  79. return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在");
  80. var now = DateTime.Now;
  81. var entity = new AdoS0ProcessFlowCard
  82. {
  83. // 组织字段后端强制 stamp 默认 S0 组织,忽略 payload 组织值(杜绝跨厂注入)。
  84. CompanyRefId = companyId,
  85. FactoryRefId = factoryId,
  86. DomainCode = dto.DomainCode?.Trim(),
  87. WorkOrderNo = dto.WorkOrderNo?.Trim() ?? string.Empty,
  88. WorkOrderBatchNo = dto.WorkOrderBatchNo?.Trim(),
  89. FlowCardNo = flowCardNo,
  90. Nature = dto.Nature?.Trim(),
  91. ItemNum = dto.ItemNum?.Trim() ?? string.Empty,
  92. ItemName = dto.ItemName?.Trim(),
  93. DrawingNo = dto.DrawingNo?.Trim(),
  94. BomVersion = dto.BomVersion?.Trim(),
  95. RoutingCode = dto.RoutingCode?.Trim(),
  96. Remarks = dto.Remarks?.Trim(),
  97. IsActive = dto.IsActive,
  98. CreateUser = dto.CreateUser,
  99. CreateTime = now,
  100. UpdateUser = dto.UpdateUser,
  101. UpdateTime = null
  102. };
  103. await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  104. return Ok(entity);
  105. }
  106. [HttpPut("{id:long}")]
  107. public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0ProcessFlowCardUpsertDto dto)
  108. {
  109. var entity = await _rep.GetByIdAsync(id);
  110. if (entity == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
  111. var flowCardNo = dto.FlowCardNo?.Trim() ?? string.Empty;
  112. // 唯一性预检以既有记录的工厂为界(组织 pin 原值,不接受 payload 迁移工厂)。
  113. if (await _rep.IsAnyAsync(x => x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.FlowCardNo == flowCardNo))
  114. return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "流转卡号已存在");
  115. // 组织字段 pin 到既有原值,忽略 payload 组织值(杜绝跨公司/跨工厂篡改)。
  116. entity.WorkOrderNo = dto.WorkOrderNo?.Trim() ?? string.Empty;
  117. entity.WorkOrderBatchNo = dto.WorkOrderBatchNo?.Trim();
  118. entity.FlowCardNo = flowCardNo;
  119. entity.Nature = dto.Nature?.Trim();
  120. entity.ItemNum = dto.ItemNum?.Trim() ?? string.Empty;
  121. entity.ItemName = dto.ItemName?.Trim();
  122. entity.DrawingNo = dto.DrawingNo?.Trim();
  123. entity.BomVersion = dto.BomVersion?.Trim();
  124. entity.RoutingCode = dto.RoutingCode?.Trim();
  125. entity.Remarks = dto.Remarks?.Trim();
  126. entity.IsActive = dto.IsActive;
  127. entity.DomainCode = dto.DomainCode?.Trim();
  128. entity.UpdateUser = dto.UpdateUser;
  129. entity.UpdateTime = DateTime.Now;
  130. await _rep.AsUpdateable(entity)
  131. .IgnoreColumns(x => new { x.CompanyRefId, x.FactoryRefId, x.CreateUser, x.CreateTime })
  132. .ExecuteCommandAsync();
  133. return Ok(entity);
  134. }
  135. [HttpPatch("{id:long}/toggle-enabled")]
  136. public async Task<IActionResult> ToggleActiveAsync(long id, [FromBody] AdoS0ProcessFlowCardToggleActiveDto dto)
  137. {
  138. var entity = await _rep.GetByIdAsync(id);
  139. if (entity == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
  140. entity.IsActive = dto.IsEnabled;
  141. entity.UpdateTime = DateTime.Now;
  142. await _rep.AsUpdateable(entity)
  143. .UpdateColumns(x => new { x.IsActive, x.UpdateTime })
  144. .ExecuteCommandAsync();
  145. return Ok(entity);
  146. }
  147. [HttpDelete("{id:long}")]
  148. public async Task<IActionResult> DeleteAsync(long id)
  149. {
  150. var item = await _rep.GetByIdAsync(id);
  151. if (item == null) return AdoS0ApiErrors.NotFound("流转卡不存在");
  152. await _rep.DeleteAsync(item);
  153. return Ok(new { message = "删除成功" });
  154. }
  155. /// <summary>
  156. /// 工艺路线联动选项:按所选物料编码过滤该物料关联的 RouteCode(去重)。
  157. /// 用稳定物料编码 ItemNum 关联(RoutingOpDetail.MaterialCode),不用物料名称。
  158. /// 返回空 → 前端允许手工填写兜底;返回非空 → 前端强制下拉。
  159. /// </summary>
  160. [HttpGet("routing-options")]
  161. public async Task<IActionResult> GetRoutingOptionsAsync([FromQuery] string? itemNum)
  162. {
  163. var key = itemNum?.Trim();
  164. if (string.IsNullOrWhiteSpace(key))
  165. return Ok(Array.Empty<object>());
  166. var list = await _routingRep.AsQueryable()
  167. .Where(x => x.MaterialCode == key)
  168. .GroupBy(x => new { x.RouteCode, x.RouteName })
  169. .Select(x => new { routeCode = x.RouteCode, routeName = x.RouteName })
  170. .ToListAsync();
  171. var options = list
  172. .Where(x => !string.IsNullOrWhiteSpace(x.routeCode))
  173. .OrderBy(x => x.routeCode)
  174. .ToList();
  175. return Ok(options);
  176. }
  177. /// <summary>
  178. /// 解析 S0 默认组织(公司 S0-C001 / 工厂 S0-F001)。config 级查找按 Code 全局唯一,ClearFilter 规避租户过滤。
  179. /// </summary>
  180. private async Task<(long CompanyId, long FactoryId)> ResolveDefaultOrgAsync()
  181. {
  182. var company = await _orgRep.AsQueryable().ClearFilter()
  183. .Where(x => x.Code == DefaultCompanyCode).Select(x => x.Id).FirstAsync();
  184. var factory = await _orgRep.AsQueryable().ClearFilter()
  185. .Where(x => x.Code == DefaultFactoryCode).Select(x => x.Id).FirstAsync();
  186. return (company, factory);
  187. }
  188. }