SeOrderService.cs 24 KB

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