AsnShipperService.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. namespace Admin.NET.Plugin.AiDOP.Order;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  3. using Admin.NET.Plugin.AiDOP.Universal;
  4. /// <summary>
  5. /// 订单发货服务 🚚
  6. /// 路由前缀:/api/Order/asnshipper/...
  7. ///
  8. /// 【租户安全边界】需认证访问(已移除类级 <c>[AllowAnonymous]</c>);租户一律经
  9. /// <c>AidopTenantScope.ResolveOrThrow</c> 从认证后 JWT 解析,无有效租户即拒绝,
  10. /// 不读前端 tenantId、无默认回退(原实现:类级匿名 + 直接取 <c>_userManager.TenantId</c>,
  11. /// 匿名时 TenantId=0 使租户过滤失效/查不到数据)。
  12. /// </summary>
  13. [ApiDescriptionSettings(Order = 280, Description = "订单发货")]
  14. [Route("api/Order")]
  15. [NonUnify]
  16. public class AsnShipperService : IDynamicApiController, ITransient
  17. {
  18. private readonly ISqlSugarClient _db;
  19. private readonly SqlSugarRepository<AsnShipperMaster> _masterRep;
  20. private readonly SqlSugarRepository<AsnShipperDetail> _detailRep;
  21. private readonly UserManager _userManager;
  22. public AsnShipperService(
  23. ISqlSugarClient db,
  24. SqlSugarRepository<AsnShipperMaster> masterRep,
  25. SqlSugarRepository<AsnShipperDetail> detailRep,
  26. UserManager userManager)
  27. {
  28. _db = db;
  29. _masterRep = masterRep;
  30. _detailRep = detailRep;
  31. _userManager = userManager;
  32. }
  33. // ══════════════════════════════════════════════════════════════
  34. // 列表 GET /api/Order/asnshipper/list
  35. // ══════════════════════════════════════════════════════════════
  36. /// <summary>获取订单发货分页列表 🚚</summary>
  37. [DisplayName("获取订单发货列表")]
  38. [HttpGet("asnshipper/list")]
  39. public async Task<object> GetAsnShipperList([FromQuery] AsnShipperListInput input)
  40. {
  41. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  42. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  43. var conditions = new List<string>
  44. {
  45. "a.shtype = 'SH'",
  46. "a.Typed <> 'S'",
  47. "a.IsActive = 1",
  48. "a.tenant_id = @TenantId"
  49. };
  50. if (!string.IsNullOrWhiteSpace(input.Id))
  51. {
  52. conditions.Add("a.Id LIKE @Id");
  53. pars.Add(new SugarParameter("@Id", $"%{input.Id.Trim()}%"));
  54. }
  55. if (!string.IsNullOrWhiteSpace(input.OrdNbr))
  56. {
  57. conditions.Add("a.OrdNbr LIKE @OrdNbr");
  58. pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%"));
  59. }
  60. if (!string.IsNullOrWhiteSpace(input.Department))
  61. {
  62. conditions.Add("(b.Department LIKE @Dept OR b.Descr LIKE @Dept)");
  63. pars.Add(new SugarParameter("@Dept", $"%{input.Department.Trim()}%"));
  64. }
  65. if (!string.IsNullOrWhiteSpace(input.ShipDateFrom))
  66. {
  67. conditions.Add("a.ShipDate >= @ShipDateFrom");
  68. pars.Add(new SugarParameter("@ShipDateFrom", input.ShipDateFrom.Trim()));
  69. }
  70. var whereClause = "WHERE " + string.Join(" AND ", conditions);
  71. var baseSql = $"""
  72. SELECT
  73. a.RecID AS Id,
  74. a.Id AS Id1,
  75. a.OrdNbr,
  76. TRIM(CONCAT(IFNULL(b.Department,''), ' ', IFNULL(b.Descr,''))) AS DepartmentName,
  77. a.ShipDate,
  78. a.Status,
  79. a.Remark,
  80. CASE WHEN EXISTS (
  81. SELECT 1 FROM ASNBOLShipperDetail d
  82. WHERE d.ASNBOLShipperRecID = a.RecID
  83. AND d.tenant_id = a.tenant_id
  84. AND IFNULL(d.RealQty, 0) > 0
  85. ) THEN 1 ELSE 0 END AS HasShipped
  86. FROM ASNBOLShipperMaster a
  87. LEFT JOIN DepartmentMaster b
  88. ON a.Department = b.Department AND b.tenant_id = a.tenant_id
  89. {whereClause}
  90. """;
  91. var offset = (input.Page - 1) * input.PageSize;
  92. var total = await _db.Ado.GetIntAsync(
  93. $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  94. var list = await _db.Ado.SqlQueryAsync<AsnShipperListRow>(
  95. $"SELECT * FROM ({baseSql}) AS t ORDER BY t.ShipDate DESC, t.Id DESC LIMIT {input.PageSize} OFFSET {offset}",
  96. pars);
  97. return new { total, page = input.Page, pageSize = input.PageSize, list };
  98. }
  99. // ══════════════════════════════════════════════════════════════
  100. // 详情 GET /api/Order/asnshipper/{id}
  101. // ══════════════════════════════════════════════════════════════
  102. /// <summary>获取订单发货详情(含明细)🚚</summary>
  103. [DisplayName("获取订单发货详情")]
  104. [HttpGet("asnshipper/{id}")]
  105. public async Task<object> GetAsnShipperDetail(int id)
  106. {
  107. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  108. var master = await _masterRep.GetFirstAsync(u => u.RecID == id && u.TenantId == tenantId)
  109. ?? throw Oops.Oh("发货单不存在");
  110. // 查询客户名称,用于前端下拉回显"编号+名称"
  111. var soldToName = (await _db.Queryable<CustMaster>()
  112. .Where(u => u.Cust == master.SoldTo && u.TenantId == tenantId)
  113. .Select(u => u.SortName)
  114. .FirstAsync()) ?? string.Empty;
  115. // 查询销售单客户名称,用于前端下拉回显"编号_客户名称"
  116. var ordNbrName = string.IsNullOrWhiteSpace(master.OrdNbr) ? string.Empty
  117. : ((await _db.Queryable<SeOrder>()
  118. .Where(u => u.BillNo == master.OrdNbr && u.TenantId == tenantId)
  119. .Select(u => u.CustomName)
  120. .FirstAsync()) ?? string.Empty);
  121. // 查询部门描述,用于前端下拉回显"编号+描述"
  122. var deptName = string.IsNullOrWhiteSpace(master.Department) ? string.Empty
  123. : ((await _db.Queryable<AdoS0DepartmentMaster>()
  124. .Where(u => u.Department == master.Department && u.TenantId == tenantId)
  125. .Select(u => u.Descr)
  126. .FirstAsync()) ?? string.Empty);
  127. // 主键关联:ASNBOLShipperDetail.ASNBOLShipperRecID = 主表 RecID
  128. var details = await _detailRep.GetListAsync(u => u.AsnShipperRecID == id && u.TenantId == tenantId);
  129. // 兼容历史数据:若外键未写入或曾用错误方式取 RecID,则按业务单号 Id + 销售单号 OrdNbr 兜底
  130. if (details.Count == 0 && !string.IsNullOrWhiteSpace(master.Id) && !string.IsNullOrWhiteSpace(master.OrdNbr))
  131. {
  132. details = await _detailRep.GetListAsync(u =>
  133. u.Id == master.Id && u.OrdNbr == master.OrdNbr && u.TenantId == tenantId);
  134. }
  135. return new
  136. {
  137. recID = master.RecID,
  138. id = master.Id,
  139. ordNbr = master.OrdNbr,
  140. ordNbrName = ordNbrName,
  141. soldTo = master.SoldTo,
  142. soldToName = soldToName,
  143. department = master.Department,
  144. departmentName = deptName,
  145. shipDate = master.ShipDate?.ToString("yyyy-MM-dd"),
  146. status = master.Status ?? string.Empty,
  147. remark = master.Remark,
  148. details = details.OrderBy(d => d.Line).Select(d => new
  149. {
  150. recID = d.RecID,
  151. line = d.Line,
  152. ordNbr = d.OrdNbr,
  153. ordLine = d.OrdLine,
  154. containerItem = d.ContainerItem,
  155. descr = d.Descr,
  156. um = d.UM,
  157. location = d.Location,
  158. lotSerial = d.LotSerial,
  159. qtyToShip = d.QtyToShip,
  160. pickingQty = d.PickingQty,
  161. realQty = d.RealQty,
  162. status = d.Status,
  163. remark = d.Remark,
  164. })
  165. };
  166. }
  167. // ══════════════════════════════════════════════════════════════
  168. // 保存(新增/编辑)POST /api/Order/asnshipper/save
  169. // ══════════════════════════════════════════════════════════════
  170. /// <summary>保存订单发货(新增或编辑)🚚</summary>
  171. [DisplayName("保存订单发货")]
  172. [ApiDescriptionSettings(Name = "SaveAsnShipper"), HttpPost("asnshipper/save")]
  173. public async Task<object> SaveAsnShipper([FromBody] AsnShipperSaveInput input)
  174. {
  175. var now = DateTime.Now;
  176. var user = _userManager.Account ?? "system";
  177. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  178. // 验证明细行至少有一行数据
  179. if (input.Details == null || input.Details.Count == 0)
  180. {
  181. throw Oops.Oh("发货单明细行不能为空,请至少添加一行明细");
  182. }
  183. if (input.RecID is null or 0)
  184. {
  185. // ── 新增:参照 SysJobService.AddJobDetail ──
  186. // 若发货单号为空则自动生成 SH+当前日期+四位流水号
  187. var shipId = input.Id;
  188. if (string.IsNullOrWhiteSpace(shipId))
  189. {
  190. shipId = await GenerateShipIdAsync(tenantId);
  191. }
  192. var entity = new AsnShipperMaster
  193. {
  194. Id = shipId,
  195. OrdNbr = input.OrdNbr,
  196. SoldTo = input.SoldTo,
  197. Department = input.Department,
  198. ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate),
  199. Status = input.Status,
  200. Remark = input.Remark,
  201. ShType = "SH",
  202. Typed = string.Empty,
  203. IsActive = 1,
  204. IsConfirm = 0,
  205. CreateUser = user,
  206. CreateTime = now,
  207. };
  208. var inserted = await _masterRep.AsInsertable(entity).ExecuteReturnEntityAsync();
  209. var newId = inserted.RecID;
  210. await SaveDetailsAsync(newId, input.Id ?? string.Empty, input.Details, user, now, isNew: true);
  211. return new { id = newId, message = "新增成功" };
  212. }
  213. else
  214. {
  215. // ── 编辑:参照 SysJobService.UpdateJobDetail ──
  216. var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID.Value && u.TenantId == tenantId)
  217. ?? throw Oops.Oh("发货单不存在");
  218. entity.Id = input.Id;
  219. entity.OrdNbr = input.OrdNbr;
  220. entity.SoldTo = input.SoldTo;
  221. entity.Department = input.Department;
  222. entity.ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate);
  223. entity.Status = input.Status;
  224. entity.Remark = input.Remark;
  225. entity.UpdateUser = user;
  226. entity.UpdateTime = now;
  227. await _masterRep.UpdateAsync(entity);
  228. await SaveDetailsAsync(input.RecID.Value, input.Id ?? string.Empty, input.Details, user, now, isNew: false);
  229. return new { id = input.RecID, message = "编辑成功" };
  230. }
  231. }
  232. /// <summary>
  233. /// 明细三路合并(参照 SeOrderService.SaveEntriesAsync):
  234. /// ① DB有且入参有 → 更新
  235. /// ② DB无入参有 → 新增
  236. /// ③ DB有入参无 → 删除
  237. /// </summary>
  238. private async Task SaveDetailsAsync(int masterRecId, string shipperId,
  239. List<AsnShipperDetailInput> details, string user, DateTime now, bool isNew)
  240. {
  241. var dbDetails = isNew ? new List<AsnShipperDetail>()
  242. : await _detailRep.GetListAsync(u => u.AsnShipperRecID == masterRecId && u.TenantId == AidopTenantScope.ResolveOrThrow(_userManager));
  243. var dbById = dbDetails.ToDictionary(u => u.RecID);
  244. var inputIds = new HashSet<int>(details.Where(d => d.RecID is > 0).Select(d => d.RecID!.Value));
  245. for (var i = 0; i < details.Count; i++)
  246. {
  247. var d = details[i];
  248. var lineNo = d.Line ?? (i + 1);
  249. if (d.RecID > 0 && dbById.TryGetValue(d.RecID!.Value, out var existing))
  250. {
  251. // ① 更新
  252. existing.Line = lineNo;
  253. existing.OrdNbr = d.OrdNbr;
  254. existing.OrdLine = d.OrdLine;
  255. existing.ContainerItem = d.ContainerItem;
  256. existing.Descr = d.Descr;
  257. existing.UM = d.UM;
  258. existing.Location = d.Location;
  259. existing.LotSerial = d.LotSerial;
  260. existing.QtyToShip = d.QtyToShip;
  261. existing.PickingQty = d.PickingQty;
  262. existing.RealQty = d.RealQty;
  263. existing.Status = d.Status;
  264. existing.Remark = d.Remark;
  265. existing.UpdateUser = user;
  266. existing.UpdateTime = now;
  267. await _detailRep.UpdateAsync(existing);
  268. }
  269. else
  270. {
  271. // ② 新增
  272. var detail = new AsnShipperDetail
  273. {
  274. Id = shipperId,
  275. AsnShipperRecID = masterRecId,
  276. Line = lineNo,
  277. OrdNbr = d.OrdNbr,
  278. OrdLine = d.OrdLine,
  279. ContainerItem = d.ContainerItem,
  280. Descr = d.Descr,
  281. UM = d.UM,
  282. Location = d.Location,
  283. LotSerial = d.LotSerial,
  284. QtyToShip = d.QtyToShip,
  285. PickingQty = d.PickingQty,
  286. RealQty = d.RealQty,
  287. Status = d.Status,
  288. Remark = d.Remark,
  289. ShType = "SH",
  290. IsActive = 1,
  291. CreateUser = user,
  292. CreateTime = now,
  293. };
  294. await _detailRep.InsertAsync(detail);
  295. }
  296. }
  297. // ③ 删除:DB有、入参无
  298. foreach (var toDelete in dbDetails.Where(u => !inputIds.Contains(u.RecID)))
  299. {
  300. await _detailRep.DeleteAsync(u => u.RecID == toDelete.RecID);
  301. }
  302. }
  303. // ══════════════════════════════════════════════════════════════
  304. // 删除 POST /api/Order/asnshipper/delete
  305. // ══════════════════════════════════════════════════════════════
  306. /// <summary>删除发货单(状态非关闭才可删除)🚚</summary>
  307. [DisplayName("删除订单发货")]
  308. [ApiDescriptionSettings(Name = "DeleteAsnShipper"), HttpPost("asnshipper/delete")]
  309. public async Task<object> DeleteAsnShipper([FromBody] AsnShipperDeleteInput input)
  310. {
  311. // 参照 SysJobService.DeleteJobDetail:先查再删
  312. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  313. var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID && u.TenantId == tenantId)
  314. ?? throw Oops.Oh("发货单不存在");
  315. if (entity.Status == "C")
  316. throw Oops.Oh("已关闭的发货单不允许删除");
  317. await _detailRep.DeleteAsync(u => u.AsnShipperRecID == input.RecID);
  318. await _masterRep.DeleteAsync(u => u.RecID == input.RecID);
  319. return new { message = "删除成功" };
  320. }
  321. // ══════════════════════════════════════════════════════════════
  322. // 销售单明细行 GET /api/Order/asnshipper/salesord/{ordNbr}/entries
  323. // ══════════════════════════════════════════════════════════════
  324. /// <summary>获取销售单明细行(用于发货单新建时自动填入)🚚</summary>
  325. [DisplayName("获取销售单明细行")]
  326. [HttpGet("asnshipper/salesord/{ordNbr}/entries")]
  327. public async Task<object> GetSalesOrdEntries(string ordNbr)
  328. {
  329. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  330. // 查询销售单主表,获取客户信息
  331. var order = await _db.Queryable<SeOrder>()
  332. .Where(u => u.BillNo == ordNbr && u.TenantId == tenantId && u.IsDeleted == 0)
  333. .Select(u => new { u.CustomNo, u.CustomName })
  334. .FirstAsync();
  335. // 查询销售单明细行(物料信息)
  336. var entries = await _db.Queryable<SeOrderEntry>()
  337. .Where(u => u.BillNo == ordNbr && u.TenantId == tenantId && u.IsDeleted == 0)
  338. .OrderBy(u => u.EntrySeq)
  339. .Select(u => new
  340. {
  341. entrySeq = u.EntrySeq,
  342. itemNumber = u.ItemNumber,
  343. itemName = u.ItemName,
  344. specification = u.Specification,
  345. unit = u.Unit,
  346. qty = u.Qty,
  347. })
  348. .ToListAsync();
  349. return new
  350. {
  351. ordNbr,
  352. customNo = order?.CustomNo,
  353. customName = order?.CustomName,
  354. entries,
  355. };
  356. }
  357. // ══════════════════════════════════════════════════════════════
  358. // 下拉数据源 GET /api/Order/asnshipper/options/salesords
  359. // ══════════════════════════════════════════════════════════════
  360. /// <summary>获取销售单下拉列表 🚚</summary>
  361. [DisplayName("获取销售单下拉")]
  362. [HttpGet("asnshipper/options/salesords")]
  363. public async Task<object> GetSalesOrdOptions([FromQuery] string? keyword)
  364. {
  365. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  366. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  367. var where = "s.IsDeleted = 0 AND s.tenant_id = @TenantId";
  368. if (!string.IsNullOrWhiteSpace(keyword))
  369. {
  370. where += " AND (s.bill_no LIKE @kw OR s.custom_name LIKE @kw)";
  371. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  372. }
  373. // MySQL:字符串拼接用 CONCAT(对应 SQL Server 的 bill_no+'_'+custom_name)
  374. var sql = $"""
  375. SELECT s.bill_no AS Value,
  376. CONCAT(s.bill_no, '_', IFNULL(s.custom_name, '')) AS Label
  377. FROM crm_seorder s
  378. WHERE {where}
  379. ORDER BY s.bill_no DESC
  380. LIMIT 100
  381. """;
  382. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  383. return list;
  384. }
  385. // ══════════════════════════════════════════════════════════════
  386. // 下拉数据源 GET /api/Order/asnshipper/options/customers
  387. // ══════════════════════════════════════════════════════════════
  388. /// <summary>获取客户下拉列表 🚚</summary>
  389. [DisplayName("获取客户下拉")]
  390. [HttpGet("asnshipper/options/customers")]
  391. public async Task<object> GetCustomerOptions([FromQuery] string? keyword)
  392. {
  393. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  394. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  395. var where = "tenant_id = @TenantId";
  396. if (!string.IsNullOrWhiteSpace(keyword))
  397. {
  398. where += " AND (Cust LIKE @kw OR SortName LIKE @kw)";
  399. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  400. }
  401. var sql = $"""
  402. SELECT Cust AS Value,
  403. TRIM(CONCAT(Cust, ' ', IFNULL(SortName,''))) AS Label
  404. FROM CustMaster
  405. WHERE {where}
  406. ORDER BY Cust
  407. LIMIT 100
  408. """;
  409. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  410. return list;
  411. }
  412. // ══════════════════════════════════════════════════════════════
  413. // 下拉数据源 GET /api/Order/asnshipper/options/departments
  414. // ══════════════════════════════════════════════════════════════
  415. /// <summary>获取部门下拉列表 🚚</summary>
  416. [DisplayName("获取部门下拉")]
  417. [HttpGet("asnshipper/options/departments")]
  418. public async Task<object> GetDepartmentOptions([FromQuery] string? keyword)
  419. {
  420. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  421. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  422. var where = "tenant_id = @TenantId";
  423. if (!string.IsNullOrWhiteSpace(keyword))
  424. {
  425. where += " AND (Department LIKE @kw OR Descr LIKE @kw)";
  426. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  427. }
  428. var sql = $"""
  429. SELECT Department AS Value,
  430. TRIM(CONCAT(Department, ' ', IFNULL(Descr,''))) AS Label
  431. FROM DepartmentMaster
  432. WHERE {where}
  433. ORDER BY Department
  434. LIMIT 100
  435. """;
  436. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  437. return list;
  438. }
  439. // ══════════════════════════════════════════════════════════════
  440. // 下拉数据源 GET /api/Order/asnshipper/options/locations
  441. // ══════════════════════════════════════════════════════════════
  442. /// <summary>获取库位下拉列表 🚚</summary>
  443. [DisplayName("获取库位下拉")]
  444. [HttpGet("asnshipper/options/locations")]
  445. public async Task<object> GetLocationOptions([FromQuery] string? keyword)
  446. {
  447. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  448. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  449. var where = "tenant_id = @TenantId AND IsActive = 1";
  450. if (!string.IsNullOrWhiteSpace(keyword))
  451. {
  452. where += " AND (location LIKE @kw OR descr LIKE @kw)";
  453. pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
  454. }
  455. var sql = $"""
  456. SELECT location AS Value,
  457. TRIM(CONCAT(location, ' ', IFNULL(descr, ''))) AS Label
  458. FROM LocationMaster
  459. WHERE {where}
  460. ORDER BY location
  461. LIMIT 100
  462. """;
  463. var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
  464. return list;
  465. }
  466. // ──────────────── 号生成 ────────────────
  467. /// <summary>
  468. /// 生成发货单号:SH + yyyyMMdd + 4位流水号,例如 SH202605210001
  469. /// </summary>
  470. private async Task<string> GenerateShipIdAsync(long tenantId)
  471. {
  472. var today = DateTime.Now.ToString("yyyyMMdd");
  473. var prefix = $"SH{today}";
  474. // 查询当天已有的最大发货单号
  475. var maxId = await _db.Queryable<AsnShipperMaster>()
  476. .Where(u => u.Id!.StartsWith(prefix) && u.IsActive == 1 && u.TenantId == tenantId)
  477. .OrderBy(u => u.Id, OrderByType.Desc)
  478. .Select(u => u.Id)
  479. .FirstAsync();
  480. if (string.IsNullOrEmpty(maxId))
  481. return $"{prefix}0001";
  482. // 取后4位转为数字并加1,不足4位补零
  483. var last4 = maxId.Length >= 4 ? maxId[^4..] : maxId;
  484. if (int.TryParse(last4, out var serial))
  485. return $"{prefix}{(serial + 1).ToString().PadLeft(4, '0')}";
  486. return $"{prefix}0001";
  487. }
  488. // ──────────────── 内部查询结果映射类 ────────────────
  489. private sealed class AsnShipperListRow
  490. {
  491. public int Id { get; set; }
  492. public string? Id1 { get; set; }
  493. public string? OrdNbr { get; set; }
  494. public string? DepartmentName { get; set; }
  495. public DateTime? ShipDate { get; set; }
  496. public string? Status { get; set; }
  497. public string? Remark { get; set; }
  498. public bool HasShipped { get; set; }
  499. }
  500. private sealed class SimpleKvRow
  501. {
  502. public string? Value { get; set; }
  503. public string? Label { get; set; }
  504. }
  505. }