AdoS0ProductDesignCyclesController.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. using Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. namespace Admin.NET.Plugin.AiDOP.Controllers.S0.Sales;
  5. [ApiController]
  6. [Route("api/s0/sales/product-design-cycles")]
  7. [NonUnify]
  8. public class AdoS0ProductDesignCyclesController : ControllerBase
  9. {
  10. private readonly SqlSugarRepository<AdoS0ProductDesignCycle> _rep;
  11. private readonly SqlSugarRepository<AdoS0ItemMaster> _itemMasterRep;
  12. public AdoS0ProductDesignCyclesController(
  13. SqlSugarRepository<AdoS0ProductDesignCycle> rep,
  14. SqlSugarRepository<AdoS0ItemMaster> itemMasterRep)
  15. {
  16. _rep = rep;
  17. _itemMasterRep = itemMasterRep;
  18. }
  19. [HttpGet]
  20. public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS0ProductDesignCycleQueryDto q)
  21. {
  22. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  23. (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
  24. var query = _rep.AsQueryable()
  25. .Where(x => x.TenantId == tenantId)
  26. .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
  27. .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
  28. .WhereIF(!string.IsNullOrWhiteSpace(q.DomainCode), x => x.DomainCode == q.DomainCode)
  29. .WhereIF(!string.IsNullOrWhiteSpace(q.ItemType), x => x.ItemType == q.ItemType)
  30. .WhereIF(!string.IsNullOrWhiteSpace(q.OwnerApplication), x => x.OwnerApplication == q.OwnerApplication)
  31. .WhereIF(q.IsActive.HasValue, x => x.IsActive == q.IsActive!.Value);
  32. var total = await query.CountAsync();
  33. var entities = await query
  34. .OrderByDescending(x => x.CreateTime)
  35. .Skip((q.Page - 1) * q.PageSize)
  36. .Take(q.PageSize)
  37. .ToListAsync();
  38. var list = await BuildRowsWithItemMasterMatchAsync(entities);
  39. return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
  40. }
  41. /// <summary>
  42. /// 在 PDC 实体基础上附带 ItemMaster 命中校验信息。
  43. /// 统计口径:
  44. /// - ItemMaster.ItemType = PDC.item_type;
  45. /// - PDC.domain_code 有值 → ItemMaster.Domain = domain_code;为 NULL → 不限 Domain;
  46. /// - OwnerApplication / Status 均不参与过滤。
  47. /// 实现:2 次查询(PDC 主列表上游已查 + ItemMaster 按 ItemType IN 一次性拉取相关列),
  48. /// 内存按 (ItemType, Domain) 分组计算 count / top3,避免 N+1。
  49. /// </summary>
  50. private async Task<List<AdoS0ProductDesignCycleRowDto>> BuildRowsWithItemMasterMatchAsync(
  51. List<AdoS0ProductDesignCycle> entities)
  52. {
  53. if (entities.Count == 0) return new List<AdoS0ProductDesignCycleRowDto>();
  54. var distinctItemTypes = entities
  55. .Select(e => e.ItemType)
  56. .Where(t => !string.IsNullOrWhiteSpace(t))
  57. .Distinct()
  58. .ToList();
  59. // 候选 ItemMaster 行(仅当前页相关 ItemType);空集合不发起查询。
  60. var candidates = distinctItemTypes.Count == 0
  61. ? new List<AdoS0ItemMaster>()
  62. : await _itemMasterRep.AsQueryable()
  63. .Where(x => distinctItemTypes.Contains(x.ItemType!))
  64. .Select(x => new AdoS0ItemMaster
  65. {
  66. Id = x.Id,
  67. ItemType = x.ItemType,
  68. DomainCode = x.DomainCode,
  69. ItemNum = x.ItemNum,
  70. Descr = x.Descr,
  71. })
  72. .ToListAsync();
  73. return entities.Select(e =>
  74. {
  75. var matched = candidates.Where(c =>
  76. c.ItemType == e.ItemType &&
  77. (string.IsNullOrWhiteSpace(e.DomainCode) || c.DomainCode == e.DomainCode))
  78. .ToList();
  79. var samples = matched
  80. .OrderBy(c => c.Id)
  81. .Take(3)
  82. .Select(c => string.IsNullOrWhiteSpace(c.Descr) ? c.ItemNum : $"{c.ItemNum} / {c.Descr}")
  83. .ToList();
  84. return new AdoS0ProductDesignCycleRowDto
  85. {
  86. Id = e.Id,
  87. CompanyRefId = e.CompanyRefId,
  88. FactoryRefId = e.FactoryRefId,
  89. DomainCode = e.DomainCode,
  90. ItemType = e.ItemType,
  91. OwnerApplication = e.OwnerApplication,
  92. StdHours = e.StdHours,
  93. IsActive = e.IsActive,
  94. CreateUser = e.CreateUser,
  95. CreateTime = e.CreateTime,
  96. UpdateUser = e.UpdateUser,
  97. UpdateTime = e.UpdateTime,
  98. ItemMasterMatchedCount = matched.Count,
  99. ItemMasterSampleItems = samples,
  100. };
  101. }).ToList();
  102. }
  103. [HttpGet("{id:long}")]
  104. public async Task<IActionResult> GetAsync(long id)
  105. {
  106. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  107. var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
  108. return item == null ? NotFound() : Ok(item);
  109. }
  110. [HttpPost]
  111. public async Task<IActionResult> CreateAsync([FromBody] AdoS0ProductDesignCycleUpsertDto dto)
  112. {
  113. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  114. var (companyId, factoryId) = await AdoS0OrgScope.ResolveAsync(_rep.Context, tenantId);
  115. if (companyId <= 0 || factoryId <= 0) return AdoS0OrgScope.NotConfiguredError("产品设计周期");
  116. if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.FactoryRefId == factoryId && x.ItemType == dto.ItemType && x.OwnerApplication == dto.OwnerApplication))
  117. return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "该工厂下此物料类型+属性组合已存在");
  118. var entity = new AdoS0ProductDesignCycle
  119. {
  120. TenantId = tenantId,
  121. // 组织字段后端强制 stamp 当前租户内解析出的组织,忽略 payload 组织值(杜绝跨租户组织引用)。
  122. CompanyRefId = companyId,
  123. FactoryRefId = factoryId,
  124. DomainCode = dto.DomainCode,
  125. ItemType = dto.ItemType,
  126. OwnerApplication = dto.OwnerApplication,
  127. StdHours = dto.StdHours,
  128. IsActive = dto.IsActive,
  129. CreateUser = dto.CreateUser,
  130. CreateTime = DateTime.Now,
  131. };
  132. await _rep.AsInsertable(entity).ExecuteReturnEntityAsync();
  133. return Ok(entity);
  134. }
  135. [HttpPut("{id:long}")]
  136. public async Task<IActionResult> UpdateAsync(long id, [FromBody] AdoS0ProductDesignCycleUpsertDto dto)
  137. {
  138. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  139. var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
  140. if (entity == null) return NotFound();
  141. if (await _rep.IsAnyAsync(x => x.TenantId == tenantId && x.Id != id && x.FactoryRefId == entity.FactoryRefId && x.ItemType == dto.ItemType && x.OwnerApplication == dto.OwnerApplication))
  142. return AdoS0ApiErrors.Conflict(AdoS0ErrorCodes.DuplicateCode, "该工厂下此物料类型+属性组合已存在");
  143. // 组织字段 pin 到既有原值,忽略 payload 组织值:既杜绝篡改成他租户组织,
  144. // 也不因一次普通编辑就顺手迁移历史 CROSS-TENANT 存量。
  145. entity.DomainCode = dto.DomainCode;
  146. entity.ItemType = dto.ItemType;
  147. entity.OwnerApplication = dto.OwnerApplication;
  148. entity.StdHours = dto.StdHours;
  149. entity.IsActive = dto.IsActive;
  150. entity.UpdateUser = dto.UpdateUser;
  151. entity.UpdateTime = DateTime.Now;
  152. await _rep.AsUpdateable(entity).ExecuteCommandAsync();
  153. return Ok(entity);
  154. }
  155. [HttpPatch("{id:long}/toggle-enabled")]
  156. public async Task<IActionResult> ToggleActiveAsync(long id, [FromBody] AdoS0ProductDesignCycleToggleActiveDto dto)
  157. {
  158. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  159. var entity = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
  160. if (entity == null) return NotFound();
  161. entity.IsActive = dto.IsActive;
  162. entity.UpdateTime = DateTime.Now;
  163. await _rep.AsUpdateable(entity).ExecuteCommandAsync();
  164. return Ok(entity);
  165. }
  166. [HttpDelete("{id:long}")]
  167. public async Task<IActionResult> DeleteAsync(long id)
  168. {
  169. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  170. var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
  171. if (item == null) return NotFound();
  172. await _rep.DeleteAsync(item);
  173. return Ok(new { message = "删除成功" });
  174. }
  175. }