AsnShipperService.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. /// <summary>
  3. /// 订单发货服务 🚚
  4. /// 路由前缀:/api/Order/asnshipper/...
  5. /// </summary>
  6. [ApiDescriptionSettings(Order = 280, Description = "订单发货")]
  7. [Route("api/Order")]
  8. [AllowAnonymous]
  9. [NonUnify]
  10. public class AsnShipperService : IDynamicApiController, ITransient
  11. {
  12. private readonly ISqlSugarClient _db;
  13. private readonly SqlSugarRepository<AsnShipperMaster> _masterRep;
  14. private readonly SqlSugarRepository<AsnShipperDetail> _detailRep;
  15. private readonly UserManager _userManager;
  16. public AsnShipperService(
  17. ISqlSugarClient db,
  18. SqlSugarRepository<AsnShipperMaster> masterRep,
  19. SqlSugarRepository<AsnShipperDetail> detailRep,
  20. UserManager userManager)
  21. {
  22. _db = db;
  23. _masterRep = masterRep;
  24. _detailRep = detailRep;
  25. _userManager = userManager;
  26. }
  27. // ══════════════════════════════════════════════════════════════
  28. // 列表 GET /api/Order/asnshipper/list
  29. // ══════════════════════════════════════════════════════════════
  30. /// <summary>获取订单发货分页列表 🚚</summary>
  31. [DisplayName("获取订单发货列表")]
  32. [HttpGet("asnshipper/list")]
  33. public async Task<object> GetAsnShipperList([FromQuery] AsnShipperListInput input)
  34. {
  35. var pars = new List<SugarParameter>();
  36. var conditions = new List<string>
  37. {
  38. "a.shtype = 'SH'",
  39. "a.Typed <> 'S'",
  40. "a.IsActive = 1"
  41. };
  42. if (!string.IsNullOrWhiteSpace(input.Id))
  43. {
  44. conditions.Add("a.Id LIKE @Id");
  45. pars.Add(new SugarParameter("@Id", $"%{input.Id.Trim()}%"));
  46. }
  47. if (!string.IsNullOrWhiteSpace(input.OrdNbr))
  48. {
  49. conditions.Add("a.OrdNbr LIKE @OrdNbr");
  50. pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%"));
  51. }
  52. if (!string.IsNullOrWhiteSpace(input.Department))
  53. {
  54. conditions.Add("(b.Department LIKE @Dept OR b.Descr LIKE @Dept)");
  55. pars.Add(new SugarParameter("@Dept", $"%{input.Department.Trim()}%"));
  56. }
  57. if (!string.IsNullOrWhiteSpace(input.ShipDateFrom))
  58. {
  59. conditions.Add("a.ShipDate >= @ShipDateFrom");
  60. pars.Add(new SugarParameter("@ShipDateFrom", input.ShipDateFrom.Trim()));
  61. }
  62. var whereClause = "WHERE " + string.Join(" AND ", conditions);
  63. var baseSql = $"""
  64. SELECT
  65. a.RecID AS Id,
  66. a.Id AS Id1,
  67. a.OrdNbr,
  68. TRIM(CONCAT(IFNULL(b.Department,''), ' ', IFNULL(b.Descr,''))) AS DepartmentName,
  69. a.ShipDate,
  70. a.Status,
  71. a.Remark
  72. FROM ASNBOLShipperMaster a
  73. LEFT JOIN DepartmentMaster b
  74. ON a.Department = b.Department AND a.Domain = b.Domain
  75. {whereClause}
  76. """;
  77. var offset = (input.Page - 1) * input.PageSize;
  78. var total = await _db.Ado.GetIntAsync(
  79. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  80. var list = await _db.Ado.SqlQueryAsync<AsnShipperListRow>(
  81. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  82. pars);
  83. return new { total, page = input.Page, pageSize = input.PageSize, list };
  84. }
  85. // ══════════════════════════════════════════════════════════════
  86. // 详情 GET /api/Order/asnshipper/{id}
  87. // ══════════════════════════════════════════════════════════════
  88. /// <summary>获取订单发货详情(含明细)🚚</summary>
  89. [DisplayName("获取订单发货详情")]
  90. [HttpGet("asnshipper/{id}")]
  91. public async Task<object> GetAsnShipperDetail(int id)
  92. {
  93. var master = await _masterRep.GetFirstAsync(u => u.RecID == id)
  94. ?? throw Oops.Oh("发货单不存在");
  95. // 主键关联:ASNBOLShipperDetail.ASNBOLShipperRecID = 主表 RecID
  96. var details = await _detailRep.GetListAsync(u => u.AsnShipperRecID == id);
  97. // 兼容历史数据:若外键未写入或曾用错误方式取 RecID,则按业务单号 Id + 销售单号 OrdNbr 兜底
  98. if (details.Count == 0 && !string.IsNullOrWhiteSpace(master.Id) && !string.IsNullOrWhiteSpace(master.OrdNbr))
  99. {
  100. details = await _detailRep.GetListAsync(u =>
  101. u.Id == master.Id && u.OrdNbr == master.OrdNbr);
  102. }
  103. return new
  104. {
  105. recID = master.RecID,
  106. id = master.Id,
  107. ordNbr = master.OrdNbr,
  108. soldTo = master.SoldTo,
  109. department = master.Department,
  110. shipDate = master.ShipDate?.ToString("yyyy-MM-dd"),
  111. status = master.Status,
  112. remark = master.Remark,
  113. details = details.OrderBy(d => d.Line).Select(d => new
  114. {
  115. recID = d.RecID,
  116. line = d.Line,
  117. ordNbr = d.OrdNbr,
  118. ordLine = d.OrdLine,
  119. containerItem = d.ContainerItem,
  120. descr = d.Descr,
  121. um = d.UM,
  122. location = d.Location,
  123. lotSerial = d.LotSerial,
  124. qtyToShip = d.QtyToShip,
  125. pickingQty = d.PickingQty,
  126. realQty = d.RealQty,
  127. status = d.Status,
  128. remark = d.Remark,
  129. })
  130. };
  131. }
  132. // ══════════════════════════════════════════════════════════════
  133. // 保存(新增/编辑)POST /api/Order/asnshipper/save
  134. // ══════════════════════════════════════════════════════════════
  135. /// <summary>保存订单发货(新增或编辑)🚚</summary>
  136. [DisplayName("保存订单发货")]
  137. [ApiDescriptionSettings(Name = "SaveAsnShipper"), HttpPost("asnshipper/save")]
  138. public async Task<object> SaveAsnShipper([FromBody] AsnShipperSaveInput input)
  139. {
  140. var now = DateTime.Now;
  141. var user = _userManager.Account ?? "system";
  142. if (input.RecID is null or 0)
  143. {
  144. // ── 新增:参照 SysJobService.AddJobDetail ──
  145. var entity = new AsnShipperMaster
  146. {
  147. Id = input.Id,
  148. OrdNbr = input.OrdNbr,
  149. SoldTo = input.SoldTo,
  150. Department = input.Department,
  151. ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate),
  152. Status = input.Status,
  153. Remark = input.Remark,
  154. ShType = "SH",
  155. Typed = string.Empty,
  156. IsActive = 1,
  157. IsConfirm = 0,
  158. CreateUser = user,
  159. CreateTime = now,
  160. };
  161. var inserted = await _masterRep.AsInsertable(entity).ExecuteReturnEntityAsync();
  162. var newId = inserted.RecID;
  163. await SaveDetailsAsync(newId, input.Id ?? string.Empty, input.Details, user, now, isNew: true);
  164. return new { id = newId, message = "新增成功" };
  165. }
  166. else
  167. {
  168. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  169. var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID.Value)
  170. ?? throw Oops.Oh("发货单不存在");
  171. entity.Id = input.Id;
  172. entity.OrdNbr = input.OrdNbr;
  173. entity.SoldTo = input.SoldTo;
  174. entity.Department = input.Department;
  175. entity.ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate);
  176. entity.Status = input.Status;
  177. entity.Remark = input.Remark;
  178. entity.UpdateUser = user;
  179. entity.UpdateTime = now;
  180. await _masterRep.UpdateAsync(entity);
  181. await SaveDetailsAsync(input.RecID.Value, input.Id ?? string.Empty, input.Details, user, now, isNew: false);
  182. return new { id = input.RecID, message = "编辑成功" };
  183. }
  184. }
  185. /// <summary>
  186. /// 明细三路合并(参照 SeOrderService.SaveEntriesAsync):
  187. /// ① DB有且入参有 → 更新
  188. /// ② DB无入参有 → 新增
  189. /// ③ DB有入参无 → 删除
  190. /// </summary>
  191. private async Task SaveDetailsAsync(int masterRecId, string shipperId,
  192. List<AsnShipperDetailInput> details, string user, DateTime now, bool isNew)
  193. {
  194. var dbDetails = isNew ? new List<AsnShipperDetail>()
  195. : await _detailRep.GetListAsync(u => u.AsnShipperRecID == masterRecId);
  196. var dbById = dbDetails.ToDictionary(u => u.RecID);
  197. var inputIds = new HashSet<int>(details.Where(d => d.RecID is > 0).Select(d => d.RecID!.Value));
  198. for (var i = 0; i < details.Count; i++)
  199. {
  200. var d = details[i];
  201. var lineNo = d.Line ?? (i + 1);
  202. if (d.RecID > 0 && dbById.TryGetValue(d.RecID!.Value, out var existing))
  203. {
  204. // ① 更新
  205. existing.Line = lineNo;
  206. existing.OrdNbr = d.OrdNbr;
  207. existing.OrdLine = d.OrdLine;
  208. existing.ContainerItem = d.ContainerItem;
  209. existing.Descr = d.Descr;
  210. existing.UM = d.UM;
  211. existing.Location = d.Location;
  212. existing.LotSerial = d.LotSerial;
  213. existing.QtyToShip = d.QtyToShip;
  214. existing.PickingQty = d.PickingQty;
  215. existing.RealQty = d.RealQty;
  216. existing.Status = d.Status;
  217. existing.Remark = d.Remark;
  218. existing.UpdateUser = user;
  219. existing.UpdateTime = now;
  220. await _detailRep.UpdateAsync(existing);
  221. }
  222. else
  223. {
  224. // ② 新增
  225. var detail = new AsnShipperDetail
  226. {
  227. Id = shipperId,
  228. AsnShipperRecID = masterRecId,
  229. Line = lineNo,
  230. OrdNbr = d.OrdNbr,
  231. OrdLine = d.OrdLine,
  232. ContainerItem = d.ContainerItem,
  233. Descr = d.Descr,
  234. UM = d.UM,
  235. Location = d.Location,
  236. LotSerial = d.LotSerial,
  237. QtyToShip = d.QtyToShip,
  238. PickingQty = d.PickingQty,
  239. RealQty = d.RealQty,
  240. Status = d.Status,
  241. Remark = d.Remark,
  242. ShType = "SH",
  243. IsActive = 1,
  244. CreateUser = user,
  245. CreateTime = now,
  246. };
  247. await _detailRep.InsertAsync(detail);
  248. }
  249. }
  250. // ③ 删除:DB有、入参无
  251. foreach (var toDelete in dbDetails.Where(u => !inputIds.Contains(u.RecID)))
  252. {
  253. await _detailRep.DeleteAsync(u => u.RecID == toDelete.RecID);
  254. }
  255. }
  256. // ══════════════════════════════════════════════════════════════
  257. // 删除 POST /api/Order/asnshipper/delete
  258. // ══════════════════════════════════════════════════════════════
  259. /// <summary>删除发货单(状态非关闭才可删除)🚚</summary>
  260. [DisplayName("删除订单发货")]
  261. [ApiDescriptionSettings(Name = "DeleteAsnShipper"), HttpPost("asnshipper/delete")]
  262. public async Task<object> DeleteAsnShipper([FromBody] AsnShipperDeleteInput input)
  263. {
  264. // 参照 SysJobService.DeleteJobDetail:先查再删
  265. var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID)
  266. ?? throw Oops.Oh("发货单不存在");
  267. if (entity.Status == "C")
  268. throw Oops.Oh("已关闭的发货单不允许删除");
  269. await _detailRep.DeleteAsync(u => u.AsnShipperRecID == input.RecID);
  270. await _masterRep.DeleteAsync(u => u.RecID == input.RecID);
  271. return new { message = "删除成功" };
  272. }
  273. // ══════════════════════════════════════════════════════════════
  274. // 下拉数据源 GET /api/Order/asnshipper/options/salesords
  275. // ══════════════════════════════════════════════════════════════
  276. /// <summary>获取销售单下拉列表 🚚</summary>
  277. [DisplayName("获取销售单下拉")]
  278. [HttpGet("asnshipper/options/salesords")]
  279. public async Task<object> GetSalesOrdOptions([FromQuery] string? keyword)
  280. {
  281. var pars = new List<SugarParameter>();
  282. var where = "s.IsDeleted = 0";
  283. if (!string.IsNullOrWhiteSpace(keyword))
  284. {
  285. where += " AND (s.bill_no LIKE @kw OR s.custom_name LIKE @kw)";
  286. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  287. }
  288. // MySQL:字符串拼接用 CONCAT(对应 SQL Server 的 bill_no+'_'+custom_name)
  289. var sql = $"""
  290. SELECT s.bill_no AS Value,
  291. CONCAT(s.bill_no, '_', IFNULL(s.custom_name, '')) AS Label
  292. FROM crm_seorder s
  293. WHERE {where}
  294. ORDER BY s.bill_no DESC
  295. LIMIT 100
  296. """;
  297. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  298. return list;
  299. }
  300. // ══════════════════════════════════════════════════════════════
  301. // 下拉数据源 GET /api/Order/asnshipper/options/customers
  302. // ══════════════════════════════════════════════════════════════
  303. /// <summary>获取客户下拉列表 🚚</summary>
  304. [DisplayName("获取客户下拉")]
  305. [HttpGet("asnshipper/options/customers")]
  306. public async Task<object> GetCustomerOptions([FromQuery] string? keyword)
  307. {
  308. var pars = new List<SugarParameter>();
  309. var where = "1=1";
  310. if (!string.IsNullOrWhiteSpace(keyword))
  311. {
  312. where += " AND (Cust LIKE @kw OR SortName LIKE @kw)";
  313. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  314. }
  315. var sql = $"""
  316. SELECT Cust AS Value,
  317. TRIM(CONCAT(Cust, ' ', IFNULL(SortName,''))) AS Label
  318. FROM CustMaster
  319. WHERE {where}
  320. ORDER BY Cust
  321. LIMIT 100
  322. """;
  323. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  324. return list;
  325. }
  326. // ══════════════════════════════════════════════════════════════
  327. // 下拉数据源 GET /api/Order/asnshipper/options/departments
  328. // ══════════════════════════════════════════════════════════════
  329. /// <summary>获取部门下拉列表 🚚</summary>
  330. [DisplayName("获取部门下拉")]
  331. [HttpGet("asnshipper/options/departments")]
  332. public async Task<object> GetDepartmentOptions([FromQuery] string? keyword)
  333. {
  334. var pars = new List<SugarParameter>();
  335. var where = "1=1";
  336. if (!string.IsNullOrWhiteSpace(keyword))
  337. {
  338. where += " AND (Department LIKE @kw OR Descr LIKE @kw)";
  339. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  340. }
  341. var sql = $"""
  342. SELECT Department AS Value,
  343. TRIM(CONCAT(Department, ' ', IFNULL(Descr,''))) AS Label
  344. FROM DepartmentMaster
  345. WHERE {where}
  346. ORDER BY Department
  347. LIMIT 100
  348. """;
  349. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  350. return list;
  351. }
  352. // ──────────────── 内部查询结果映射类 ────────────────
  353. private sealed class AsnShipperListRow
  354. {
  355. public int Id { get; set; }
  356. public string? Id1 { get; set; }
  357. public string? OrdNbr { get; set; }
  358. public string? DepartmentName { get; set; }
  359. public DateTime? ShipDate { get; set; }
  360. public string? Status { get; set; }
  361. public string? Remark { get; set; }
  362. }
  363. private sealed class SimpleKvRow
  364. {
  365. public string? Value { get; set; }
  366. public string? Label { get; set; }
  367. }
  368. }