using System.ComponentModel.DataAnnotations; using MAX.Framework.Commons; using MAX.Framework.Validation; using Store.Shops.Dtos; using Store.Shops.Entities; using Store.Shops.Mappers; namespace Store.Shops.Services; public sealed partial class ShopService { /// 查询本人运单;跨账号按订单查询仅供管理员使用且必须指定订单。 public async Task> Waybills(WaybillSearchRequest request, bool allUsers, CancellationToken ct) { RequestValidator.Validate(request); Page(request.Page, request.PageSize); if (request.Status?.Any(s => s is < 0 or > 2) == true) throw new ValidationException("运单状态只能为0、1、2"); var actor = await Actor(); if (allUsers) { RequireAdmin(actor); if (request.OrderIds == null) throw new ValidationException("订单编号不能为空"); Ids(request.OrderIds); } var query = db.Select().WhereIf(!allUsers, w => w.UserName == actor.UserName) .WhereIf(request.Status != null, w => request.Status!.Contains(w.Status)) .WhereIf(request.OrderIds != null, w => request.OrderIds!.Contains(w.OrderId)); var count = request.NotCount ? -1 : await query.CountAsync(ct); var rows = await query.OrderByDescending(w => w.Id).Page(request.Page, request.PageSize).ToListAsync(ct); return new(rows.Select(new ShopDetailsMapper().Map).ToList(), count); } /// 导入运单;揽收时间或绑定订单至少提供一个,运单号全库去重。 public async Task AddWaybill(WaybillAddRequest request, CancellationToken ct) { RequestValidator.Validate(request); if (request.WaybillPickupTime == null && string.IsNullOrWhiteSpace(request.BindOrderId)) throw new ValidationException("揽收时间和绑定订单不能同时为空"); var actor = await Actor(); using var work = db.CreateUnitOfWork(); work.IsolationLevel = System.Data.IsolationLevel.Serializable; work.GetOrBeginTransaction(); var orm = work.Orm; if (await orm.Select().Where(w => w.WaybillId == request.WaybillId).ForUpdate().AnyAsync(ct)) throw new AppException("运单号已存在"); await orm.Insert(new WaybillEntity { UserName = actor.UserName, DeliveryId = request.DeliveryId, DeliveryName = request.DeliveryName, WaybillId = request.WaybillId, BindOrderId = request.BindOrderId ?? "", WaybillPickupTime = request.WaybillPickupTime, Status = 0, AddTime = DateTime.Now }).ExecuteAffrowsAsync(ct); ct.ThrowIfCancellationRequested(); work.Commit(); } /// 只删除本人尚未使用的运单,已预占或使用的运单保持不变。 public async Task DeleteWaybills(WaybillDeleteRequest request, CancellationToken ct) { RequestValidator.Validate(request); Ids(request.WaybillIds); var actor = await Actor(); await db.Delete().Where(w => w.UserName == actor.UserName && w.Status == 0 && request.WaybillIds.Contains(w.WaybillId)).ExecuteAffrowsAsync(ct); } /// 预占或直接使用运单;先复用同订单记录,再按绑定订单及次日揽收条件选择。 public async Task SelectWaybill(WaybillSelectRequest request, bool useImmediately, CancellationToken ct) { RequestValidator.Validate(request); if (request.OrderTime?.Date >= DateTime.MaxValue.Date.AddDays(-2)) throw new ValidationException("订单时间超出可匹配范围"); var actor = await Actor(); using var work = db.CreateUnitOfWork(); work.IsolationLevel = System.Data.IsolationLevel.Serializable; work.GetOrBeginTransaction(); var orm = work.Orm; await RequireShop(orm, actor, request.ShopId, ct); var row = await orm.Select() .Where(w => w.UserName == actor.UserName && w.OrderId == request.OrderId && (w.Status == 1 || w.Status == 2)) .OrderBy(w => w.Id).ForUpdate().FirstAsync(ct); if (row != null && !string.IsNullOrEmpty(row.ShopId) && row.ShopId != request.ShopId) throw new AppException("订单已绑定其他店铺的运单"); if (row == null) { row = await orm.Select().Where(w => w.UserName == actor.UserName && w.Status == 0 && w.BindOrderId == request.OrderId && (w.OrderId == "" || w.OrderId == null)) .OrderBy("RAND()").ForUpdate().FirstAsync(ct); if (row == null && (request.OrderTime.HasValue || useImmediately)) { var available = orm.Select().Where(w => w.UserName == actor.UserName && w.Status == 0 && (w.OrderId == "" || w.OrderId == null) && (w.BindOrderId == "" || w.BindOrderId == null)); if (!useImmediately) { var start = request.OrderTime!.Value.Date.AddDays(1); var end = start.AddDays(1); available.Where(w => w.WaybillPickupTime >= start && w.WaybillPickupTime < end); } row = await available.OrderBy("RAND()").ForUpdate().FirstAsync(ct); } if (row == null) throw new AppException("没有可用的运单号"); } if (row.Status != 2) { var status = useImmediately ? 2 : 1; var useTime = useImmediately ? DateTime.Now : (DateTime?)null; // 行锁与状态条件一起保护分配;请求重试不会重复占用第二张运单。 var affected = await orm.Update().Where(w => w.Id == row.Id && w.Status == row.Status) .Set(w => w.OrderId, request.OrderId).Set(w => w.ShopId, request.ShopId) .Set(w => w.Status, status).Set(w => w.UseTime, useTime).ExecuteAffrowsAsync(ct); if (affected == 0 && row.Status == 0) throw new AppException("运单已被占用,请重试"); row.OrderId = request.OrderId; row.ShopId = request.ShopId; row.Status = status; row.UseTime = useTime; } ct.ThrowIfCancellationRequested(); work.Commit(); return new ShopDetailsMapper().Map(row); } /// 仅对本人的预占运单确认使用或释放;不能释放已正式使用的运单。 public async Task UseWaybill(WaybillUseRequest request, CancellationToken ct) { RequestValidator.Validate(request); var actor = await Actor(); using var work = db.CreateUnitOfWork(); work.GetOrBeginTransaction(); var orm = work.Orm; await RequireShop(orm, actor, request.ShopId, ct); var row = await orm.Select().Where(w => w.UserName == actor.UserName && w.OrderId == request.OrderId && w.WaybillId == request.WaybillId) .ForUpdate().FirstAsync(ct); if (row == null || (row.ShopId != "" && row.ShopId != null && row.ShopId != request.ShopId)) throw new AppException("运单不存在或不属于指定店铺订单"); if (!request.IsRollback && row.Status == 2) return; if (row.Status != 1) throw new AppException("只有预占运单可以确认或释放"); var changed = await orm.Update().Where(w => w.Id == row.Id && w.Status == 1) .Set(w => w.ShopId, request.IsRollback ? "" : request.ShopId) .Set(w => w.OrderId, request.IsRollback ? "" : request.OrderId) .Set(w => w.Status, request.IsRollback ? 0 : 2) .Set(w => w.UseTime, request.IsRollback ? null : (DateTime?)DateTime.Now).ExecuteAffrowsAsync(ct); if (changed != 1) throw new AppException("运单状态已变化,请重新查询"); ct.ThrowIfCancellationRequested(); work.Commit(); } }