SeOrderService.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. using Yitter.IdGenerator;
  2. namespace Admin.NET.Plugin.AiDOP.Order;
  3. /// <summary>
  4. /// 销售订单评审服务 🗂️
  5. /// 路由前缀:/api/Order/seorder/...
  6. ///
  7. /// 【租户安全边界】需认证访问;租户一律经 <c>AidopTenantScope.ResolveOrThrow</c> 从认证后 JWT 解析,
  8. /// 无有效租户即拒绝,不读前端 tenantId、无默认回退(原实现:多处宽松解析
  9. /// <c>AidopTenantHelper.Resolve(App.HttpContext)</c> / 直接取 <c>_userManager.TenantId</c>,
  10. /// 可被 <c>?tenantId=</c> query 覆盖并有默认租户兜底)。
  11. /// </summary>
  12. [ApiDescriptionSettings(Order = 300, Description = "销售订单评审")]
  13. [Route("api/Order")]
  14. [NonUnify]
  15. public class SeOrderService : IDynamicApiController, ITransient
  16. {
  17. private readonly ISqlSugarClient _db;
  18. private readonly SqlSugarRepository<SeOrder> _seOrderRep;
  19. private readonly SqlSugarRepository<SeOrderEntry> _seOrderEntryRep;
  20. private readonly SqlSugarRepository<SeOrderChange> _seOrderChangeRep;
  21. private readonly UserManager _userManager;
  22. private readonly SeOrderOperationLogService _orderOpLog;
  23. private readonly OrderReviewOrchestrationService _reviewOrchestration;
  24. public SeOrderService(
  25. ISqlSugarClient db,
  26. SqlSugarRepository<SeOrder> seOrderRep,
  27. SqlSugarRepository<SeOrderEntry> seOrderEntryRep,
  28. SqlSugarRepository<SeOrderChange> seOrderChangeRep,
  29. UserManager userManager,
  30. SeOrderOperationLogService orderOpLog,
  31. OrderReviewOrchestrationService reviewOrchestration)
  32. {
  33. _db = db;
  34. _seOrderRep = seOrderRep;
  35. _seOrderEntryRep = seOrderEntryRep;
  36. _seOrderChangeRep = seOrderChangeRep;
  37. _userManager = userManager;
  38. _orderOpLog = orderOpLog;
  39. _reviewOrchestration = reviewOrchestration;
  40. }
  41. // ══════════════════════════════════════════════════════════════
  42. // 订单列表 GET /api/Order/seorder/list
  43. // ══════════════════════════════════════════════════════════════
  44. /// <summary>获取订单评审分页列表 🗂️</summary>
  45. [DisplayName("获取订单评审列表")]
  46. [HttpGet("seorder/list")]
  47. public async Task<object> GetSeOrderList([FromQuery] SeOrderListInput input)
  48. {
  49. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  50. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  51. var innerConditions = new List<string> { "o.IsDeleted = 0", "o.tenant_id = @TenantId" };
  52. if (!string.IsNullOrWhiteSpace(input.BillNo))
  53. {
  54. innerConditions.Add("o.bill_no LIKE @BillNo");
  55. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  56. }
  57. if (!string.IsNullOrWhiteSpace(input.CustomNo))
  58. {
  59. innerConditions.Add("o.custom_no LIKE @CustomNo");
  60. pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%"));
  61. }
  62. if (input.OrderType.HasValue)
  63. {
  64. innerConditions.Add("o.order_type = @OrderType");
  65. pars.Add(new SugarParameter("@OrderType", input.OrderType.Value));
  66. }
  67. var baseSql = BuildListBaseSql(string.Join(" AND ", innerConditions));
  68. // state 是 CASE 计算列,需包一层再过滤
  69. var outerWhere = "";
  70. if (!string.IsNullOrWhiteSpace(input.State))
  71. {
  72. outerWhere = "WHERE t.State = @State";
  73. pars.Add(new SugarParameter("@State", input.State.Trim()));
  74. }
  75. var offset = (input.Page - 1) * input.PageSize;
  76. var total = await _db.Ado.GetIntAsync(
  77. $"SELECT COUNT(*) FROM ({baseSql}) AS t {outerWhere}", pars);
  78. var list = await _db.Ado.SqlQueryAsync<SeOrderListRow>(
  79. $"SELECT * FROM ({baseSql}) AS t {outerWhere} ORDER BY t.RDate DESC, t.Id DESC LIMIT {input.PageSize} OFFSET {offset}", pars);
  80. return new { total, page = input.Page, pageSize = input.PageSize, list };
  81. }
  82. private static string BuildListBaseSql(string innerWhere) => $"""
  83. SELECT
  84. o.Id AS Id,
  85. o.bill_no AS BillNo,
  86. o.order_type AS OrderType,
  87. o.custom_no AS CustomNo,
  88. o.custom_name AS SortName,
  89. o.rdate AS RDate,
  90. o.flowstate AS FlowState,
  91. COALESCE(NULLIF(e.min_progress, ''), '1') AS Progress,
  92. CASE
  93. WHEN COALESCE(NULLIF(e.min_progress, ''), '1') = '1' THEN '新建'
  94. WHEN COALESCE(NULLIF(e.min_progress, ''), '1') = '2' THEN '评审'
  95. WHEN COALESCE(NULLIF(e.min_progress, ''), '1') = '0' THEN '再评审'
  96. WHEN COALESCE(NULLIF(e.min_progress, ''), '1') = '3' THEN '确认'
  97. ELSE '确认'
  98. END AS State,
  99. CASE WHEN o.flowstate = '审批完成' THEN chg.change_content ELSE NULL END AS ChangeContent,
  100. cN.changeNum AS ChangeNum
  101. FROM crm_seorder o
  102. LEFT JOIN (
  103. SELECT seorder_id, MIN(COALESCE(NULLIF(progress, ''), '1')) AS min_progress
  104. FROM crm_seorderentry
  105. WHERE IsDeleted = 0
  106. GROUP BY seorder_id
  107. ) e ON e.seorder_id = o.Id
  108. LEFT JOIN (
  109. SELECT seorder_id, change_content
  110. FROM (
  111. SELECT seorder_id, change_content,
  112. ROW_NUMBER() OVER (PARTITION BY seorder_id ORDER BY create_time DESC, Id DESC) AS RowNum
  113. FROM crm_seorder_change
  114. WHERE IsDeleted = 0 AND tenant_id = @TenantId
  115. ) ranked
  116. WHERE RowNum = 1
  117. ) chg ON chg.seorder_id = o.Id
  118. LEFT JOIN (
  119. SELECT seorder_id, COUNT(*) AS changeNum
  120. FROM crm_seorder_change
  121. WHERE IsDeleted = 0 AND tenant_id = @TenantId
  122. GROUP BY seorder_id
  123. ) cN ON cN.seorder_id = o.Id
  124. WHERE {innerWhere}
  125. """;
  126. // ══════════════════════════════════════════════════════════════
  127. // 客户列表 GET /api/Order/seorder/customers
  128. // 注:字面量路由优先于 {id} 参数路由,无需额外排序
  129. // ══════════════════════════════════════════════════════════════
  130. /// <summary>获取客户选择列表 🗂️</summary>
  131. [DisplayName("获取客户列表")]
  132. [HttpGet("seorder/customers")]
  133. public async Task<object> GetCustomers([FromQuery] KeywordPageInput input)
  134. {
  135. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  136. var pars = new List<SugarParameter>();
  137. var where = "tenant_id = @TenantId";
  138. pars.Add(new SugarParameter("@TenantId", tenantId));
  139. if (!string.IsNullOrWhiteSpace(input.Keyword))
  140. {
  141. where += " AND (Cust LIKE @Keyword OR SortName LIKE @Keyword)";
  142. pars.Add(new SugarParameter("@Keyword", $"%{input.Keyword.Trim()}%"));
  143. }
  144. var offset = (input.Page - 1) * input.PageSize;
  145. var total = await _db.Ado.GetIntAsync(
  146. $"SELECT COUNT(*) FROM CustMaster WHERE {where}", pars);
  147. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(
  148. $"SELECT Cust AS Value, SortName AS Label, NULL AS Extra, NULL AS Id FROM CustMaster WHERE {where} ORDER BY Cust LIMIT {input.PageSize} OFFSET {offset}",
  149. pars);
  150. return new { total, page = input.Page, pageSize = input.PageSize, list };
  151. }
  152. // ══════════════════════════════════════════════════════════════
  153. // 物料列表 GET /api/Order/seorder/items
  154. // TODO: 将 bd_material 替换为实际物料主数据表名
  155. // ══════════════════════════════════════════════════════════════
  156. /// <summary>获取物料选择列表 🗂️</summary>
  157. [DisplayName("获取物料列表")]
  158. [HttpGet("seorder/items")]
  159. public async Task<object> GetItems([FromQuery] KeywordPageInput input)
  160. {
  161. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  162. var pars = new List<SugarParameter>();
  163. var where = "IsDeleted = 0 AND tenant_id = @TenantId";
  164. pars.Add(new SugarParameter("@TenantId", tenantId));
  165. if (!string.IsNullOrWhiteSpace(input.Keyword))
  166. {
  167. where += " AND (item_number LIKE @Keyword OR item_name LIKE @Keyword)";
  168. pars.Add(new SugarParameter("@Keyword", $"%{input.Keyword.Trim()}%"));
  169. }
  170. var offset = (input.Page - 1) * input.PageSize;
  171. var total = await _db.Ado.GetIntAsync(
  172. $"SELECT COUNT(*) FROM bd_material WHERE {where}", pars);
  173. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(
  174. $"SELECT item_number AS Value, item_name AS Label, CONCAT(IFNULL(specification,''),'|',IFNULL(unit,'')) AS Extra, NULL AS Id FROM bd_material WHERE {where} ORDER BY item_number LIMIT {input.PageSize} OFFSET {offset}",
  175. pars);
  176. return new { total, page = input.Page, pageSize = input.PageSize, list };
  177. }
  178. // ══════════════════════════════════════════════════════════════
  179. // 订单详情 GET /api/Order/seorder/{id}
  180. // ══════════════════════════════════════════════════════════════
  181. /// <summary>获取订单详情(含明细)🗂️</summary>
  182. [DisplayName("获取订单详情")]
  183. [HttpGet("seorder/{id}")]
  184. public async Task<object> GetSeOrderDetail(long id)
  185. {
  186. const string orderSql = """
  187. SELECT Id, bill_no AS BillNo, order_type AS OrderType,
  188. custom_id AS CustomId, custom_no AS CustomNo, custom_name AS CustomName,
  189. date AS Date, custom_level AS CustomLevel, urgent AS Urgent,
  190. bill_from AS BillFrom, country AS Country, rdate AS RDate, flowstate AS FlowState
  191. FROM crm_seorder
  192. WHERE Id = @Id AND IsDeleted = 0
  193. LIMIT 1
  194. """;
  195. var order = (await _db.Ado.SqlQueryAsync<SeOrderDetailRow>(orderSql, new { Id = id }))
  196. .FirstOrDefault()
  197. ?? throw Oops.Oh("订单不存在");
  198. const string entrySql = """
  199. SELECT Id, entry_seq AS EntrySeq, company_id AS CompanyId, item_number AS ItemNumber, item_name AS ItemName,
  200. specification AS Specification, qty AS Qty, plan_date AS PlanDate,
  201. sys_capacity_date AS SysCapacityDate, date AS Date, remark AS Remark,
  202. unit AS Unit, deliver_count AS DeliverCount, progress AS Progress
  203. FROM crm_seorderentry
  204. WHERE seorder_id = @Id AND IsDeleted = 0
  205. ORDER BY entry_seq
  206. """;
  207. var entries = await _db.Ado.SqlQueryAsync<SeOrderEntryRow>(entrySql, new { Id = id });
  208. return new
  209. {
  210. order.Id, order.BillNo, order.OrderType,
  211. order.CustomId, order.CustomNo, order.CustomName,
  212. order.Date, order.CustomLevel, order.Urgent,
  213. order.BillFrom, order.Country, order.RDate, order.FlowState,
  214. entries
  215. };
  216. }
  217. // ══════════════════════════════════════════════════════════════
  218. // 新增 / 编辑 POST /api/Order/seorder/save
  219. // ══════════════════════════════════════════════════════════════
  220. /// <summary>保存销售订单(新增或编辑)🗂️</summary>
  221. [DisplayName("保存销售订单")]
  222. [ApiDescriptionSettings(Name = "SaveSeOrder"), HttpPost("seorder/save")]
  223. public async Task<object> SaveSeOrder([FromBody] SeOrderSaveInput input)
  224. {
  225. // 子表订单明细至少一条
  226. if (input.Entries == null || input.Entries.Count == 0)
  227. throw Oops.Oh("订单明细至少需要一条数据");
  228. if (input.Id is null or 0)
  229. {
  230. // ── 新增:参照 SysJobService.AddJobDetail ──
  231. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  232. // 如果未填写订单编号,自动生成:SO + yyyyMMdd + 4位流水号
  233. if (string.IsNullOrWhiteSpace(input.BillNo))
  234. {
  235. input.BillNo = await GenerateBillNoAsync(tenantId);
  236. }
  237. var isExist = await _seOrderRep.IsAnyAsync(u => u.BillNo == input.BillNo && u.IsDeleted == 0 && u.TenantId == tenantId);
  238. if (isExist) throw Oops.Oh("订单编号已存在");
  239. var entity = input.Adapt<SeOrder>();
  240. entity.Id = YitIdHelper.NextId();
  241. entity.IsDeleted = 0;
  242. entity.Closed = 0;
  243. entity.CreateTime = DateTime.Now;
  244. entity.TenantId = tenantId;
  245. await _seOrderRep.InsertAsync(entity);
  246. await SaveEntriesAsync(entity.Id, input.BillNo, input.Entries);
  247. var orderAfter = await _seOrderRep.GetFirstAsync(u => u.Id == entity.Id && u.IsDeleted == 0 && u.TenantId == tenantId);
  248. var entriesAfter = await _seOrderEntryRep.GetListAsync(u => u.SeOrderId == entity.Id && u.IsDeleted == 0 && u.TenantId == tenantId);
  249. var afterJson = SeOrderAuditHelper.BuildCombinedSnapshotJson(orderAfter!, entriesAfter);
  250. await _orderOpLog.WriteAsync(new SeOrderOperationLogDto
  251. {
  252. UserId = _userManager.UserId,
  253. Action = "新增订单",
  254. TableName = "crm_seorder",
  255. KeyValue = entity.Id.ToString(),
  256. Content = $"新增订单,订单编号:{entity.BillNo},订单主键:{entity.Id}。",
  257. BeforeJson = null,
  258. AfterJson = afterJson
  259. });
  260. return new { id = entity.Id, message = "新增成功" };
  261. }
  262. else
  263. {
  264. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  265. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  266. var entity = await _seOrderRep.GetFirstAsync(u => u.Id == input.Id.Value && u.IsDeleted == 0)
  267. ?? throw Oops.Oh("订单不存在");
  268. var beforeEntries = await _seOrderEntryRep.GetListAsync(u => u.SeOrderId == input.Id.Value && u.IsDeleted == 0);
  269. var orderBefore = entity.Adapt<SeOrder>();
  270. var entriesBefore = beforeEntries.Select(e => e.Adapt<SeOrderEntry>()).ToList();
  271. input.Adapt(entity);
  272. entity.UpdateTime = DateTime.Now;
  273. await _seOrderRep.UpdateAsync(entity);
  274. await SaveEntriesAsync(input.Id.Value, input.BillNo, input.Entries);
  275. var afterOrder = await _seOrderRep.GetFirstAsync(u => u.Id == input.Id.Value && u.IsDeleted == 0 && u.TenantId == tenantId);
  276. var afterEntries = await _seOrderEntryRep.GetListAsync(u => u.SeOrderId == input.Id.Value && u.IsDeleted == 0 && u.TenantId == tenantId);
  277. var (content, beforeJson, afterJson) = SeOrderAuditHelper.BuildUpdateDiff(
  278. orderBefore!, entriesBefore, afterOrder!, afterEntries);
  279. await _orderOpLog.WriteAsync(new SeOrderOperationLogDto
  280. {
  281. UserId = _userManager.UserId,
  282. Action = "修改订单",
  283. TableName = "crm_seorder",
  284. KeyValue = input.Id.Value.ToString(),
  285. Content = content,
  286. BeforeJson = beforeJson,
  287. AfterJson = afterJson
  288. });
  289. return new { id = input.Id, message = "编辑成功" };
  290. }
  291. }
  292. /// <summary>
  293. /// 明细三路合并:
  294. /// ① DB有且入参有(按 Id 匹配)→ 更新
  295. /// ② DB无但入参有 → 新增(补填 bill_no / progress=0 / create_by)
  296. /// ③ DB有但入参无 → 删除
  297. /// </summary>
  298. private async Task SaveEntriesAsync(long orderId, string billNo, List<SeOrderEntryInput> entries)
  299. {
  300. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  301. // 取 DB 现有明细,以 Id 为 key 建索引
  302. var dbEntries = await _seOrderEntryRep.GetListAsync(u => u.SeOrderId == orderId && u.IsDeleted == 0 && u.TenantId == tenantId);
  303. var dbById = dbEntries.ToDictionary(u => u.Id);
  304. // 入参中携带 Id 的集合(用于判断 DB 行是否需要删除)
  305. var inputIds = new HashSet<long>(entries.Where(e => e.Id > 0).Select(e => e.Id!.Value));
  306. for (var i = 0; i < entries.Count; i++)
  307. {
  308. var e = entries[i];
  309. var seq = e.EntrySeq ?? (i + 1);
  310. if (e.Id > 0 && dbById.TryGetValue(e.Id.Value, out var existing))
  311. {
  312. // ① DB有、参数有 → 更新(参照 SysJobService.UpdateJobDetail)
  313. var originalCompanyId = existing.CompanyId;
  314. e.Adapt(existing);
  315. existing.CompanyId = e.CompanyId ?? originalCompanyId;
  316. existing.EntrySeq = seq;
  317. existing.UpdateTime = DateTime.Now;
  318. await _seOrderEntryRep.UpdateAsync(existing);
  319. }
  320. else
  321. {
  322. // ② DB无、参数有 → 新增(参照 SysJobService.AddJobDetail)
  323. var entry = e.Adapt<SeOrderEntry>();
  324. entry.Id = YitIdHelper.NextId();
  325. entry.SeOrderId = orderId;
  326. entry.BillNo = billNo;
  327. entry.EntrySeq = seq;
  328. entry.CompanyId ??= 1000;
  329. entry.Progress ??= "0";
  330. entry.CreateBy = _userManager.UserId;
  331. entry.IsDeleted = 0;
  332. entry.CreateTime = DateTime.Now;
  333. entry.TenantId = tenantId;
  334. await _seOrderEntryRep.InsertAsync(entry);
  335. }
  336. }
  337. // ③ DB有、参数无 → 删除(参照 SysJobService.DeleteJobDetail)
  338. foreach (var toDelete in dbEntries.Where(u => !inputIds.Contains(u.Id)))
  339. {
  340. await _seOrderEntryRep.DeleteAsync(u => u.Id == toDelete.Id);
  341. }
  342. }
  343. /// <summary>
  344. /// 生成订单编号:SO + yyyyMMdd + 4位流水号,例如 SO202605210001
  345. /// </summary>
  346. private async Task<string> GenerateBillNoAsync(long tenantId)
  347. {
  348. var today = DateTime.Now.ToString("yyyyMMdd");
  349. var prefix = $"SO{today}";
  350. // 查询当天已有的最大订单编号
  351. var maxBillNo = await _db.Queryable<SeOrder>()
  352. .Where(u => u.BillNo!.StartsWith(prefix) && u.IsDeleted == 0 && u.TenantId == tenantId)
  353. .OrderBy(u => u.BillNo, OrderByType.Desc)
  354. .Select(u => u.BillNo)
  355. .FirstAsync();
  356. if (string.IsNullOrEmpty(maxBillNo))
  357. {
  358. return $"{prefix}0001";
  359. }
  360. // 取后4位转为数字并加1,不足4位补零
  361. var last4 = maxBillNo.Length >= 4 ? maxBillNo[^4..] : maxBillNo;
  362. if (int.TryParse(last4, out var serial))
  363. {
  364. return $"{prefix}{(serial + 1).ToString().PadLeft(4, '0')}";
  365. }
  366. return $"{prefix}0001";
  367. }
  368. // ══════════════════════════════════════════════════════════════
  369. // 订单评审 POST /api/Order/seorder/review
  370. // ══════════════════════════════════════════════════════════════
  371. /// <summary>订单评审(资源检查 + 工单生成)🗂️</summary>
  372. [DisplayName("订单评审")]
  373. [ApiDescriptionSettings(Name = "ReviewSeOrders"), HttpPost("seorder/review")]
  374. public async Task<SeOrderReviewExecuteResult> ReviewSeOrders([FromBody] SeOrderReviewExecuteInput input) =>
  375. await _reviewOrchestration.ReviewAsync(input.Ids);
  376. // ══════════════════════════════════════════════════════════════
  377. // 交期确认 POST /api/Order/seorder/confirm-delivery
  378. // ══════════════════════════════════════════════════════════════
  379. /// <summary>交期确认 🗂️</summary>
  380. [DisplayName("交期确认")]
  381. [ApiDescriptionSettings(Name = "ConfirmSeOrderDelivery"), HttpPost("seorder/confirm-delivery")]
  382. public async Task<SeOrderReviewExecuteResult> ConfirmSeOrderDelivery([FromBody] SeOrderReviewExecuteInput input) =>
  383. await _reviewOrchestration.ConfirmDeliveryAsync(input.Ids);
  384. // ══════════════════════════════════════════════════════════════
  385. // 变更重排 POST /api/Order/seorder/{id}/refresh-plan
  386. // ══════════════════════════════════════════════════════════════
  387. /// <summary>订单变更后 3 级计划重排 🗂️</summary>
  388. [DisplayName("订单变更重排")]
  389. [ApiDescriptionSettings(Name = "RefreshSeOrderPlan"), HttpPost("seorder/{id}/refresh-plan")]
  390. public async Task<SeOrderReviewExecuteResult> RefreshSeOrderPlan(long id, [FromBody] SeOrderRefreshPlanInput? input) =>
  391. await _reviewOrchestration.RefreshPlanAsync(id, input?.Reason);
  392. // ══════════════════════════════════════════════════════════════
  393. // 资源解锁 POST /api/Order/seorder/{id}/unlock
  394. // ══════════════════════════════════════════════════════════════
  395. /// <summary>资源解锁 🔓</summary>
  396. [DisplayName("资源解锁")]
  397. [ApiDescriptionSettings(Name = "UnlockSeOrder"), HttpPost("seorder/{id}/unlock")]
  398. public async Task<object> UnlockSeOrder(long id)
  399. {
  400. return new { message = "解锁成功" };
  401. }
  402. // ══════════════════════════════════════════════════════════════
  403. // 变更申请 POST /api/Order/seorder/{id}/change
  404. // ══════════════════════════════════════════════════════════════
  405. /// <summary>提交订单变更申请 🔖</summary>
  406. [DisplayName("提交订单变更申请")]
  407. [ApiDescriptionSettings(Name = "CreateSeOrderChange"), HttpPost("seorder/{id}/change")]
  408. public async Task<object> CreateChange(long id, [FromBody] SeOrderChangeSaveInput input)
  409. {
  410. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  411. // 参照 SysJobService.AddJobDetail:Adapt → 设主键与审计字段 → InsertAsync
  412. var entity = input.Adapt<SeOrderChange>();
  413. entity.Id = YitIdHelper.NextId();
  414. entity.SeOrderId = id;
  415. entity.IsDeleted = 0;
  416. entity.CreateTime = DateTime.Now;
  417. entity.TenantId = tenantId;
  418. await _seOrderChangeRep.InsertAsync(entity);
  419. return new { id = entity.Id, message = "变更申请已保存" };
  420. }
  421. // ══════════════════════════════════════════════════════════════
  422. // 获取变更记录 GET /api/Order/seorder/{id}/change
  423. // ══════════════════════════════════════════════════════════════
  424. /// <summary>获取订单最新变更记录 🔖</summary>
  425. [DisplayName("获取订单变更记录")]
  426. [ApiDescriptionSettings(Name = "GetSeOrderChange"), HttpGet("seorder/{id}/change")]
  427. public async Task<object> GetChange(long id)
  428. {
  429. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  430. var change = await _db.Ado.SqlQuerySingleAsync<SeOrderChange>(
  431. """
  432. SELECT * FROM crm_seorder_change
  433. WHERE seorder_id = @Id AND tenant_id = @TenantId AND IsDeleted = 0
  434. ORDER BY create_time DESC LIMIT 1
  435. """,
  436. new SugarParameter("@Id", id),
  437. new SugarParameter("@TenantId", tenantId));
  438. if (change == null)
  439. return new { changeReason = "", changeType = "", changeContent = "" };
  440. return new { changeReason = change.ChangeReason ?? "", changeType = change.ChangeType ?? "", changeContent = change.ChangeContent ?? "" };
  441. }
  442. // ──────────────── 内部查询结果映射类 ────────────────
  443. private sealed class SeOrderListRow
  444. {
  445. public long Id { get; set; }
  446. public string? BillNo { get; set; }
  447. public int? OrderType { get; set; }
  448. public string? CustomNo { get; set; }
  449. public string? SortName { get; set; }
  450. public DateTime? RDate { get; set; }
  451. public string? Progress { get; set; }
  452. public string? State { get; set; }
  453. public string? ChangeContent { get; set; }
  454. public int? ChangeNum { get; set; }
  455. public string? FlowState { get; set; }
  456. }
  457. private sealed class SeOrderDetailRow
  458. {
  459. public long Id { get; set; }
  460. public string? BillNo { get; set; }
  461. public int? OrderType { get; set; }
  462. public long? CustomId { get; set; }
  463. public string? CustomNo { get; set; }
  464. public string? CustomName { get; set; }
  465. public DateTime? Date { get; set; }
  466. public int? CustomLevel { get; set; }
  467. public int? Urgent { get; set; }
  468. public string? BillFrom { get; set; }
  469. public string? Country { get; set; }
  470. public DateTime? RDate { get; set; }
  471. public string? FlowState { get; set; }
  472. }
  473. private sealed class SeOrderEntryRow
  474. {
  475. public long Id { get; set; }
  476. public int? EntrySeq { get; set; }
  477. public long? CompanyId { get; set; }
  478. public string? ItemNumber { get; set; }
  479. public string? ItemName { get; set; }
  480. public string? Specification { get; set; }
  481. public decimal? Qty { get; set; }
  482. public DateTime? PlanDate { get; set; }
  483. public DateTime? SysCapacityDate { get; set; }
  484. public DateTime? Date { get; set; }
  485. public string? Remark { get; set; }
  486. public string? Unit { get; set; }
  487. public decimal? DeliverCount { get; set; }
  488. public string? Progress { get; set; }
  489. }
  490. private sealed class SimpleKvRow
  491. {
  492. public string? Value { get; set; }
  493. public string? Label { get; set; }
  494. public string? Extra { get; set; }
  495. public long? Id { get; set; }
  496. }
  497. // ══════════════════════════════════════════════════════════════
  498. // 提交订单评审审批
  499. // ══════════════════════════════════════════════════════════════
  500. /// <summary>提交订单评审审批流程</summary>
  501. [DisplayName("提交订单评审审批")]
  502. [HttpPost("seorder/submitReview")]
  503. public async Task<object> SubmitOrderReview([FromBody] SubmitReviewInput input)
  504. {
  505. var order = await _seOrderRep.GetByIdAsync(input.OrderId)
  506. ?? throw Oops.Oh("订单不存在");
  507. var engine = App.GetRequiredService<Admin.NET.Plugin.ApprovalFlow.Service.FlowEngineService>();
  508. var instanceId = await engine.StartFlow(new Admin.NET.Plugin.ApprovalFlow.Service.StartFlowInput
  509. {
  510. BizType = "ORDER_REVIEW",
  511. BizId = order.Id,
  512. BizNo = order.BillNo,
  513. Title = $"订单评审-{order.BillNo}",
  514. Comment = input.Comment,
  515. });
  516. return new { instanceId };
  517. }
  518. public class SubmitReviewInput
  519. {
  520. public long OrderId { get; set; }
  521. public string? Comment { get; set; }
  522. }
  523. }