SeOrderService.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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. public SeOrderService(
  18. ISqlSugarClient db,
  19. SqlSugarRepository<SeOrder> seOrderRep,
  20. SqlSugarRepository<SeOrderEntry> seOrderEntryRep,
  21. SqlSugarRepository<SeOrderChange> seOrderChangeRep)
  22. {
  23. _db = db;
  24. _seOrderRep = seOrderRep;
  25. _seOrderEntryRep = seOrderEntryRep;
  26. _seOrderChangeRep = seOrderChangeRep;
  27. }
  28. // ══════════════════════════════════════════════════════════════
  29. // 订单列表 GET /api/Order/seorder/list
  30. // ══════════════════════════════════════════════════════════════
  31. /// <summary>获取订单评审分页列表 🗂️</summary>
  32. [DisplayName("获取订单评审列表")]
  33. [HttpGet("seorder/list")]
  34. public async Task<object> GetSeOrderList([FromQuery] SeOrderListInput input)
  35. {
  36. var pars = new List<SugarParameter>();
  37. var innerConditions = new List<string> { "a.IsDeleted = 0" };
  38. if (!string.IsNullOrWhiteSpace(input.BillNo))
  39. {
  40. innerConditions.Add("a.bill_no LIKE @BillNo");
  41. pars.Add(new SugarParameter("@BillNo", $"%{input.BillNo.Trim()}%"));
  42. }
  43. if (!string.IsNullOrWhiteSpace(input.CustomNo))
  44. {
  45. innerConditions.Add("a.custom_no LIKE @CustomNo");
  46. pars.Add(new SugarParameter("@CustomNo", $"%{input.CustomNo.Trim()}%"));
  47. }
  48. if (input.OrderType.HasValue)
  49. {
  50. innerConditions.Add("a.order_type = @OrderType");
  51. pars.Add(new SugarParameter("@OrderType", input.OrderType.Value));
  52. }
  53. var baseSql = BuildListBaseSql(string.Join(" AND ", innerConditions));
  54. // state 是 CASE 计算列,需包一层再过滤
  55. var outerWhere = "";
  56. if (!string.IsNullOrWhiteSpace(input.State))
  57. {
  58. outerWhere = "WHERE t.State = @State";
  59. pars.Add(new SugarParameter("@State", input.State.Trim()));
  60. }
  61. var offset = (input.Page - 1) * input.PageSize;
  62. var total = await _db.Ado.GetIntAsync(
  63. $"SELECT COUNT(*) FROM ({baseSql}) AS t {outerWhere}", pars);
  64. var list = await _db.Ado.SqlQueryAsync<SeOrderListRow>(
  65. $"SELECT * FROM ({baseSql}) AS t {outerWhere} ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}", pars);
  66. return new { total, page = input.Page, pageSize = input.PageSize, list };
  67. }
  68. private static string BuildListBaseSql(string innerWhere) => $"""
  69. SELECT
  70. a.id AS Id,
  71. a.bill_no AS BillNo,
  72. a.order_type AS OrderType,
  73. a.custom_no AS CustomNo,
  74. b.SortName AS SortName,
  75. a.rdate AS RDate,
  76. a.flowstate AS FlowState,
  77. entry.progress AS Progress,
  78. CASE
  79. WHEN entry.progress = 1 THEN '新建'
  80. WHEN entry.progress = 2 THEN '评审'
  81. WHEN entry.progress = 0 THEN '再评审'
  82. WHEN entry.progress = 3 THEN '确认'
  83. ELSE '确认'
  84. END AS State,
  85. chg.change_content AS ChangeContent,
  86. cN.changeNum AS ChangeNum
  87. FROM crm_seorder a
  88. LEFT JOIN CustMaster b ON a.custom_no = b.Cust
  89. LEFT JOIN (
  90. SELECT sorderid, MAX(create_time) AS create_time
  91. FROM b_examine_result
  92. GROUP BY sorderid
  93. ) d ON a.id = d.sorderid
  94. LEFT JOIN (
  95. SELECT bill_no, change_content
  96. FROM (
  97. SELECT bill_no, change_content, update_time,
  98. ROW_NUMBER() OVER (PARTITION BY bill_no ORDER BY update_time DESC) AS RowNum
  99. FROM crm_seorder_change
  100. WHERE bill_no IS NOT NULL
  101. ) ranked
  102. WHERE RowNum = 1
  103. ) chg ON chg.bill_no = a.bill_no
  104. LEFT JOIN (
  105. SELECT COUNT(*) AS changeNum, bill_no
  106. FROM crm_seorder_change
  107. WHERE bill_no IS NOT NULL
  108. GROUP BY bill_no
  109. ) cN ON cN.bill_no = a.bill_no
  110. LEFT JOIN (
  111. SELECT seorder_id, progress,
  112. ROW_NUMBER() OVER (PARTITION BY seorder_id ORDER BY Id) AS rn
  113. FROM crm_seorderentry
  114. WHERE IsDeleted = 0
  115. ) entry ON a.id = entry.seorder_id AND entry.rn = 1
  116. WHERE {innerWhere}
  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, 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 isExist = await _seOrderRep.IsAnyAsync(u => u.BillNo == input.BillNo && u.IsDeleted == 0);
  217. if (isExist) throw Oops.Oh("订单编号已存在");
  218. var entity = input.Adapt<SeOrder>();
  219. entity.Id = YitIdHelper.NextId();
  220. entity.IsDeleted = 0;
  221. entity.CreateTime = DateTime.Now;
  222. await _seOrderRep.InsertAsync(entity);
  223. await SaveEntriesAsync(entity.Id, input.Entries);
  224. return new { id = entity.Id, message = "新增成功" };
  225. }
  226. else
  227. {
  228. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  229. var entity = await _seOrderRep.GetFirstAsync(u => u.Id == input.Id.Value && u.IsDeleted == 0)
  230. ?? throw Oops.Oh("订单不存在");
  231. input.Adapt(entity);
  232. entity.UpdateTime = DateTime.Now;
  233. await _seOrderRep.UpdateAsync(entity);
  234. await SaveEntriesAsync(input.Id.Value, input.Entries);
  235. return new { id = input.Id, message = "编辑成功" };
  236. }
  237. }
  238. /// <summary>
  239. /// 明细三路合并:
  240. /// ① DB有且入参有(按 Id 匹配)→ 更新
  241. /// ② DB无但入参有 → 新增
  242. /// ③ DB有但入参无 → 删除
  243. /// </summary>
  244. private async Task SaveEntriesAsync(long orderId, List<SeOrderEntryInput> entries)
  245. {
  246. // 取 DB 现有明细,以 Id 为 key 建索引
  247. var dbEntries = await _seOrderEntryRep.GetListAsync(u => u.SeOrderId == orderId && u.IsDeleted == 0);
  248. var dbById = dbEntries.ToDictionary(u => u.Id);
  249. // 入参中携带 Id 的集合(用于判断 DB 行是否需要删除)
  250. var inputIds = new HashSet<long>(entries.Where(e => e.Id > 0).Select(e => e.Id!.Value));
  251. for (var i = 0; i < entries.Count; i++)
  252. {
  253. var e = entries[i];
  254. var seq = e.EntrySeq ?? (i + 1);
  255. if (e.Id > 0 && dbById.TryGetValue(e.Id.Value, out var existing))
  256. {
  257. // ① DB有、参数有 → 更新(参照 SysJobService.UpdateJobDetail)
  258. e.Adapt(existing);
  259. existing.EntrySeq = seq;
  260. existing.UpdateTime = DateTime.Now;
  261. await _seOrderEntryRep.UpdateAsync(existing);
  262. }
  263. else
  264. {
  265. // ② DB无、参数有 → 新增(参照 SysJobService.AddJobDetail)
  266. var entry = e.Adapt<SeOrderEntry>();
  267. entry.Id = YitIdHelper.NextId();
  268. entry.SeOrderId = orderId;
  269. entry.EntrySeq = seq;
  270. entry.IsDeleted = 0;
  271. entry.CreateTime = DateTime.Now;
  272. await _seOrderEntryRep.InsertAsync(entry);
  273. }
  274. }
  275. // ③ DB有、参数无 → 删除(参照 SysJobService.DeleteJobDetail)
  276. foreach (var toDelete in dbEntries.Where(u => !inputIds.Contains(u.Id)))
  277. {
  278. await _seOrderEntryRep.DeleteAsync(u => u.Id == toDelete.Id);
  279. }
  280. }
  281. // ══════════════════════════════════════════════════════════════
  282. // 订单评审 POST /api/Order/seorder/{id}/review
  283. // ══════════════════════════════════════════════════════════════
  284. /// <summary>订单评审(设进度=2)⏰</summary>
  285. [DisplayName("订单评审")]
  286. [ApiDescriptionSettings(Name = "ReviewSeOrder"), HttpPost("seorder/{id}/review")]
  287. public async Task<object> ReviewSeOrder(long id)
  288. {
  289. // 参照 SysJobService 中 AsUpdateable().SetColumns().Where() 模式
  290. await _seOrderEntryRep.AsUpdateable()
  291. .SetColumns(u => new SeOrderEntry { Progress = 2, UpdateTime = DateTime.Now })
  292. .Where(u => u.SeOrderId == id && u.IsDeleted == 0)
  293. .ExecuteCommandAsync();
  294. return new { message = "评审完成", estimatedDeliveryDate = DateTime.Now.AddDays(30).ToString("yyyy-MM-dd") };
  295. }
  296. // ══════════════════════════════════════════════════════════════
  297. // 交期确认 POST /api/Order/seorder/{id}/confirm-delivery
  298. // ══════════════════════════════════════════════════════════════
  299. /// <summary>交期确认(设进度=3)⏰</summary>
  300. [DisplayName("交期确认")]
  301. [ApiDescriptionSettings(Name = "ConfirmSeOrderDelivery"), HttpPost("seorder/{id}/confirm-delivery")]
  302. public async Task<object> ConfirmDelivery(long id)
  303. {
  304. await _seOrderEntryRep.AsUpdateable()
  305. .SetColumns(u => new SeOrderEntry { Progress = 3, UpdateTime = DateTime.Now })
  306. .Where(u => u.SeOrderId == id && u.IsDeleted == 0)
  307. .ExecuteCommandAsync();
  308. return new { message = "交期已确认" };
  309. }
  310. // ══════════════════════════════════════════════════════════════
  311. // 资源解锁 POST /api/Order/seorder/{id}/unlock
  312. // ══════════════════════════════════════════════════════════════
  313. /// <summary>资源解锁 🔓</summary>
  314. [DisplayName("资源解锁")]
  315. [ApiDescriptionSettings(Name = "UnlockSeOrder"), HttpPost("seorder/{id}/unlock")]
  316. public async Task<object> UnlockSeOrder(long id)
  317. {
  318. return new { message = "解锁成功" };
  319. }
  320. // ══════════════════════════════════════════════════════════════
  321. // 变更申请 POST /api/Order/seorder/{id}/change
  322. // ══════════════════════════════════════════════════════════════
  323. /// <summary>提交订单变更申请 🔖</summary>
  324. [DisplayName("提交订单变更申请")]
  325. [ApiDescriptionSettings(Name = "CreateSeOrderChange"), HttpPost("seorder/{id}/change")]
  326. public async Task<object> CreateChange(long id, [FromBody] SeOrderChangeSaveInput input)
  327. {
  328. // 参照 SysJobService.AddJobDetail:Adapt → 设主键与审计字段 → InsertAsync
  329. var entity = input.Adapt<SeOrderChange>();
  330. entity.Id = YitIdHelper.NextId();
  331. entity.SeOrderId = id;
  332. entity.IsDeleted = 0;
  333. entity.CreateTime = DateTime.Now;
  334. await _seOrderChangeRep.InsertAsync(entity);
  335. return new { id = entity.Id, message = "变更申请已保存" };
  336. }
  337. // ──────────────── 内部查询结果映射类 ────────────────
  338. private sealed class SeOrderListRow
  339. {
  340. public long Id { get; set; }
  341. public string? BillNo { get; set; }
  342. public int? OrderType { get; set; }
  343. public string? CustomNo { get; set; }
  344. public string? SortName { get; set; }
  345. public DateTime? RDate { get; set; }
  346. public int? Progress { get; set; }
  347. public string? State { get; set; }
  348. public string? ChangeContent { get; set; }
  349. public int? ChangeNum { get; set; }
  350. public string? FlowState { get; set; }
  351. }
  352. private sealed class SeOrderDetailRow
  353. {
  354. public long Id { get; set; }
  355. public string? BillNo { get; set; }
  356. public int? OrderType { get; set; }
  357. public long? CustomId { get; set; }
  358. public string? CustomNo { get; set; }
  359. public string? CustomName { get; set; }
  360. public DateTime? Date { get; set; }
  361. public int? CustomLevel { get; set; }
  362. public int? Urgent { get; set; }
  363. public string? BillFrom { get; set; }
  364. public string? Country { get; set; }
  365. public DateTime? RDate { get; set; }
  366. public string? FlowState { get; set; }
  367. }
  368. private sealed class SeOrderEntryRow
  369. {
  370. public long Id { get; set; }
  371. public int? EntrySeq { get; set; }
  372. public string? ItemNumber { get; set; }
  373. public string? ItemName { get; set; }
  374. public string? Specification { get; set; }
  375. public decimal? Qty { get; set; }
  376. public DateTime? PlanDate { get; set; }
  377. public DateTime? SysCapacityDate { get; set; }
  378. public DateTime? Date { get; set; }
  379. public string? Remark { get; set; }
  380. public string? Unit { get; set; }
  381. public decimal? DeliverCount { get; set; }
  382. public int? Progress { get; set; }
  383. }
  384. private sealed class SimpleKvRow
  385. {
  386. public string? Value { get; set; }
  387. public string? Label { get; set; }
  388. public string? Extra { get; set; }
  389. public long? Id { get; set; }
  390. }
  391. }