Files
2026-10-29 11:34:35 +08:00

147 lines
7.9 KiB
C#

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
{
/// <summary>查询本人运单;跨账号按订单查询仅供管理员使用且必须指定订单。</summary>
public async Task<ShopPage<WaybillInfo>> 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<WaybillEntity>().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);
}
/// <summary>导入运单;揽收时间或绑定订单至少提供一个,运单号全库去重。</summary>
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<WaybillEntity>().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();
}
/// <summary>只删除本人尚未使用的运单,已预占或使用的运单保持不变。</summary>
public async Task DeleteWaybills(WaybillDeleteRequest request, CancellationToken ct)
{
RequestValidator.Validate(request);
Ids(request.WaybillIds);
var actor = await Actor();
await db.Delete<WaybillEntity>().Where(w => w.UserName == actor.UserName && w.Status == 0
&& request.WaybillIds.Contains(w.WaybillId)).ExecuteAffrowsAsync(ct);
}
/// <summary>预占或直接使用运单;先复用同订单记录,再按绑定订单及次日揽收条件选择。</summary>
public async Task<WaybillInfo> 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<WaybillEntity>()
.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<WaybillEntity>().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<WaybillEntity>().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<WaybillEntity>().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);
}
/// <summary>仅对本人的预占运单确认使用或释放;不能释放已正式使用的运单。</summary>
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<WaybillEntity>().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<WaybillEntity>().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();
}
}