namespace Admin.NET.Plugin.AiDOP.Order; /// /// 订单发货服务 🚚 /// 路由前缀:/api/Order/asnshipper/... /// [ApiDescriptionSettings(Order = 280, Description = "订单发货")] [Route("api/Order")] [AllowAnonymous] [NonUnify] public class AsnShipperService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly SqlSugarRepository _masterRep; private readonly SqlSugarRepository _detailRep; private readonly UserManager _userManager; public AsnShipperService( ISqlSugarClient db, SqlSugarRepository masterRep, SqlSugarRepository detailRep, UserManager userManager) { _db = db; _masterRep = masterRep; _detailRep = detailRep; _userManager = userManager; } // ══════════════════════════════════════════════════════════════ // 列表 GET /api/Order/asnshipper/list // ══════════════════════════════════════════════════════════════ /// 获取订单发货分页列表 🚚 [DisplayName("获取订单发货列表")] [HttpGet("asnshipper/list")] public async Task GetAsnShipperList([FromQuery] AsnShipperListInput input) { var pars = new List(); var conditions = new List { "a.shtype = 'SH'", "a.Typed <> 'S'", "a.IsActive = 1" }; if (!string.IsNullOrWhiteSpace(input.Id)) { conditions.Add("a.Id LIKE @Id"); pars.Add(new SugarParameter("@Id", $"%{input.Id.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.OrdNbr)) { conditions.Add("a.OrdNbr LIKE @OrdNbr"); pars.Add(new SugarParameter("@OrdNbr", $"%{input.OrdNbr.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.Department)) { conditions.Add("(b.Department LIKE @Dept OR b.Descr LIKE @Dept)"); pars.Add(new SugarParameter("@Dept", $"%{input.Department.Trim()}%")); } if (!string.IsNullOrWhiteSpace(input.ShipDateFrom)) { conditions.Add("a.ShipDate >= @ShipDateFrom"); pars.Add(new SugarParameter("@ShipDateFrom", input.ShipDateFrom.Trim())); } var whereClause = "WHERE " + string.Join(" AND ", conditions); var baseSql = $""" SELECT a.RecID AS Id, a.Id AS Id1, a.OrdNbr, TRIM(CONCAT(IFNULL(b.Department,''), ' ', IFNULL(b.Descr,''))) AS DepartmentName, a.ShipDate, a.Status, a.Remark FROM ASNBOLShipperMaster a LEFT JOIN DepartmentMaster b ON a.Department = b.Department AND a.Domain = b.Domain {whereClause} """; var offset = (input.Page - 1) * input.PageSize; var total = await _db.Ado.GetIntAsync( $"SELECT COUNT(*) FROM ({baseSql}) AS t", pars); var list = await _db.Ado.SqlQueryAsync( $"SELECT * FROM ({baseSql}) AS t ORDER BY t.Id DESC LIMIT {input.PageSize} OFFSET {offset}", pars); return new { total, page = input.Page, pageSize = input.PageSize, list }; } // ══════════════════════════════════════════════════════════════ // 详情 GET /api/Order/asnshipper/{id} // ══════════════════════════════════════════════════════════════ /// 获取订单发货详情(含明细)🚚 [DisplayName("获取订单发货详情")] [HttpGet("asnshipper/{id}")] public async Task GetAsnShipperDetail(int id) { var master = await _masterRep.GetFirstAsync(u => u.RecID == id) ?? throw Oops.Oh("发货单不存在"); // 主键关联:ASNBOLShipperDetail.ASNBOLShipperRecID = 主表 RecID var details = await _detailRep.GetListAsync(u => u.AsnShipperRecID == id); // 兼容历史数据:若外键未写入或曾用错误方式取 RecID,则按业务单号 Id + 销售单号 OrdNbr 兜底 if (details.Count == 0 && !string.IsNullOrWhiteSpace(master.Id) && !string.IsNullOrWhiteSpace(master.OrdNbr)) { details = await _detailRep.GetListAsync(u => u.Id == master.Id && u.OrdNbr == master.OrdNbr); } return new { recID = master.RecID, id = master.Id, ordNbr = master.OrdNbr, soldTo = master.SoldTo, department = master.Department, shipDate = master.ShipDate?.ToString("yyyy-MM-dd"), status = master.Status, remark = master.Remark, details = details.OrderBy(d => d.Line).Select(d => new { recID = d.RecID, line = d.Line, ordNbr = d.OrdNbr, ordLine = d.OrdLine, containerItem = d.ContainerItem, descr = d.Descr, um = d.UM, location = d.Location, lotSerial = d.LotSerial, qtyToShip = d.QtyToShip, pickingQty = d.PickingQty, realQty = d.RealQty, status = d.Status, remark = d.Remark, }) }; } // ══════════════════════════════════════════════════════════════ // 保存(新增/编辑)POST /api/Order/asnshipper/save // ══════════════════════════════════════════════════════════════ /// 保存订单发货(新增或编辑)🚚 [DisplayName("保存订单发货")] [ApiDescriptionSettings(Name = "SaveAsnShipper"), HttpPost("asnshipper/save")] public async Task SaveAsnShipper([FromBody] AsnShipperSaveInput input) { var now = DateTime.Now; var user = _userManager.Account ?? "system"; if (input.RecID is null or 0) { // ── 新增:参照 SysJobService.AddJobDetail ── var entity = new AsnShipperMaster { Id = input.Id, OrdNbr = input.OrdNbr, SoldTo = input.SoldTo, Department = input.Department, ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate), Status = input.Status, Remark = input.Remark, ShType = "SH", Typed = string.Empty, IsActive = 1, IsConfirm = 0, CreateUser = user, CreateTime = now, }; var inserted = await _masterRep.AsInsertable(entity).ExecuteReturnEntityAsync(); var newId = inserted.RecID; await SaveDetailsAsync(newId, input.Id ?? string.Empty, input.Details, user, now, isNew: true); return new { id = newId, message = "新增成功" }; } else { // ── 编辑:参照 SysJobService.UpdateJobDetail ── var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID.Value) ?? throw Oops.Oh("发货单不存在"); entity.Id = input.Id; entity.OrdNbr = input.OrdNbr; entity.SoldTo = input.SoldTo; entity.Department = input.Department; entity.ShipDate = string.IsNullOrWhiteSpace(input.ShipDate) ? null : DateTime.Parse(input.ShipDate); entity.Status = input.Status; entity.Remark = input.Remark; entity.UpdateUser = user; entity.UpdateTime = now; await _masterRep.UpdateAsync(entity); await SaveDetailsAsync(input.RecID.Value, input.Id ?? string.Empty, input.Details, user, now, isNew: false); return new { id = input.RecID, message = "编辑成功" }; } } /// /// 明细三路合并(参照 SeOrderService.SaveEntriesAsync): /// ① DB有且入参有 → 更新 /// ② DB无入参有 → 新增 /// ③ DB有入参无 → 删除 /// private async Task SaveDetailsAsync(int masterRecId, string shipperId, List details, string user, DateTime now, bool isNew) { var dbDetails = isNew ? new List() : await _detailRep.GetListAsync(u => u.AsnShipperRecID == masterRecId); var dbById = dbDetails.ToDictionary(u => u.RecID); var inputIds = new HashSet(details.Where(d => d.RecID is > 0).Select(d => d.RecID!.Value)); for (var i = 0; i < details.Count; i++) { var d = details[i]; var lineNo = d.Line ?? (i + 1); if (d.RecID > 0 && dbById.TryGetValue(d.RecID!.Value, out var existing)) { // ① 更新 existing.Line = lineNo; existing.OrdNbr = d.OrdNbr; existing.OrdLine = d.OrdLine; existing.ContainerItem = d.ContainerItem; existing.Descr = d.Descr; existing.UM = d.UM; existing.Location = d.Location; existing.LotSerial = d.LotSerial; existing.QtyToShip = d.QtyToShip; existing.PickingQty = d.PickingQty; existing.RealQty = d.RealQty; existing.Status = d.Status; existing.Remark = d.Remark; existing.UpdateUser = user; existing.UpdateTime = now; await _detailRep.UpdateAsync(existing); } else { // ② 新增 var detail = new AsnShipperDetail { Id = shipperId, AsnShipperRecID = masterRecId, Line = lineNo, OrdNbr = d.OrdNbr, OrdLine = d.OrdLine, ContainerItem = d.ContainerItem, Descr = d.Descr, UM = d.UM, Location = d.Location, LotSerial = d.LotSerial, QtyToShip = d.QtyToShip, PickingQty = d.PickingQty, RealQty = d.RealQty, Status = d.Status, Remark = d.Remark, ShType = "SH", IsActive = 1, CreateUser = user, CreateTime = now, }; await _detailRep.InsertAsync(detail); } } // ③ 删除:DB有、入参无 foreach (var toDelete in dbDetails.Where(u => !inputIds.Contains(u.RecID))) { await _detailRep.DeleteAsync(u => u.RecID == toDelete.RecID); } } // ══════════════════════════════════════════════════════════════ // 删除 POST /api/Order/asnshipper/delete // ══════════════════════════════════════════════════════════════ /// 删除发货单(状态非关闭才可删除)🚚 [DisplayName("删除订单发货")] [ApiDescriptionSettings(Name = "DeleteAsnShipper"), HttpPost("asnshipper/delete")] public async Task DeleteAsnShipper([FromBody] AsnShipperDeleteInput input) { // 参照 SysJobService.DeleteJobDetail:先查再删 var entity = await _masterRep.GetFirstAsync(u => u.RecID == input.RecID) ?? throw Oops.Oh("发货单不存在"); if (entity.Status == "C") throw Oops.Oh("已关闭的发货单不允许删除"); await _detailRep.DeleteAsync(u => u.AsnShipperRecID == input.RecID); await _masterRep.DeleteAsync(u => u.RecID == input.RecID); return new { message = "删除成功" }; } // ══════════════════════════════════════════════════════════════ // 下拉数据源 GET /api/Order/asnshipper/options/salesords // ══════════════════════════════════════════════════════════════ /// 获取销售单下拉列表 🚚 [DisplayName("获取销售单下拉")] [HttpGet("asnshipper/options/salesords")] public async Task GetSalesOrdOptions([FromQuery] string? keyword) { var pars = new List(); var where = "s.IsDeleted = 0"; if (!string.IsNullOrWhiteSpace(keyword)) { where += " AND (s.bill_no LIKE @kw OR s.custom_name LIKE @kw)"; pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%")); } // MySQL:字符串拼接用 CONCAT(对应 SQL Server 的 bill_no+'_'+custom_name) var sql = $""" SELECT s.bill_no AS Value, CONCAT(s.bill_no, '_', IFNULL(s.custom_name, '')) AS Label FROM crm_seorder s WHERE {where} ORDER BY s.bill_no DESC LIMIT 100 """; var list = await _db.Ado.SqlQueryAsync(sql, pars); return list; } // ══════════════════════════════════════════════════════════════ // 下拉数据源 GET /api/Order/asnshipper/options/customers // ══════════════════════════════════════════════════════════════ /// 获取客户下拉列表 🚚 [DisplayName("获取客户下拉")] [HttpGet("asnshipper/options/customers")] public async Task GetCustomerOptions([FromQuery] string? keyword) { var pars = new List(); var where = "1=1"; if (!string.IsNullOrWhiteSpace(keyword)) { where += " AND (Cust LIKE @kw OR SortName LIKE @kw)"; pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%")); } var sql = $""" SELECT Cust AS Value, TRIM(CONCAT(Cust, ' ', IFNULL(SortName,''))) AS Label FROM CustMaster WHERE {where} ORDER BY Cust LIMIT 100 """; var list = await _db.Ado.SqlQueryAsync(sql, pars); return list; } // ══════════════════════════════════════════════════════════════ // 下拉数据源 GET /api/Order/asnshipper/options/departments // ══════════════════════════════════════════════════════════════ /// 获取部门下拉列表 🚚 [DisplayName("获取部门下拉")] [HttpGet("asnshipper/options/departments")] public async Task GetDepartmentOptions([FromQuery] string? keyword) { var pars = new List(); var where = "1=1"; if (!string.IsNullOrWhiteSpace(keyword)) { where += " AND (Department LIKE @kw OR Descr LIKE @kw)"; pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%")); } var sql = $""" SELECT Department AS Value, TRIM(CONCAT(Department, ' ', IFNULL(Descr,''))) AS Label FROM DepartmentMaster WHERE {where} ORDER BY Department LIMIT 100 """; var list = await _db.Ado.SqlQueryAsync(sql, pars); return list; } // ──────────────── 内部查询结果映射类 ──────────────── private sealed class AsnShipperListRow { public int Id { get; set; } public string? Id1 { get; set; } public string? OrdNbr { get; set; } public string? DepartmentName { get; set; } public DateTime? ShipDate { get; set; } public string? Status { get; set; } public string? Remark { get; set; } } private sealed class SimpleKvRow { public string? Value { get; set; } public string? Label { get; set; } } }