From cb2562c8b842f03f500e5114fc18e953b7af4d84 Mon Sep 17 00:00:00 2001 From: liguoyang Date: Fri, 12 Jan 2024 16:18:44 +0800 Subject: [PATCH] =?UTF-8?q?fix=20=E4=BC=98=E5=8C=96=E6=B6=88=E8=B4=B9?= =?UTF-8?q?=E5=85=85=E5=80=BC=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Order/Controllers/OrdersController.cs | 4 +- .../Order/Request/ConsumeRequest.cs | 41 ++ .../Order/Request/OrderAddRequest.cs | 42 ++- .../Order/Request/RechargeRequest.cs | 34 ++ .../Order/Response/BalanceRecordResponse.cs | 5 + .../Response/BalanceRecordUserResponse.cs | 5 + ...sumeResponse.cs => OrderResultResponse.cs} | 2 +- .../Order/Services/IOrderService.cs | 2 +- .../Services/Implements/BalanceService.cs | 3 + .../Order/Services/Implements/OrderService.cs | 349 ++++++++++++------ 10 files changed, 363 insertions(+), 124 deletions(-) create mode 100644 Src/Dlhis.Canteen/Order/Request/ConsumeRequest.cs create mode 100644 Src/Dlhis.Canteen/Order/Request/RechargeRequest.cs rename Src/Dlhis.Canteen/Order/Response/{OrderConsumeResponse.cs => OrderResultResponse.cs} (94%) diff --git a/Src/Dlhis.Canteen/Order/Controllers/OrdersController.cs b/Src/Dlhis.Canteen/Order/Controllers/OrdersController.cs index f802eece..86e9a5c6 100644 --- a/Src/Dlhis.Canteen/Order/Controllers/OrdersController.cs +++ b/Src/Dlhis.Canteen/Order/Controllers/OrdersController.cs @@ -34,12 +34,12 @@ public class OrdersController : ControllerBase } /// - /// 添加充值消费订单 + /// 添加充值/消费订单 /// /// /// [HttpPost] - public async Task AddAsync(OrderAddRequest request) + public async Task AddAsync(OrderAddRequest request) { return await _orderService.AddAsync(request); } diff --git a/Src/Dlhis.Canteen/Order/Request/ConsumeRequest.cs b/Src/Dlhis.Canteen/Order/Request/ConsumeRequest.cs new file mode 100644 index 00000000..87159bb6 --- /dev/null +++ b/Src/Dlhis.Canteen/Order/Request/ConsumeRequest.cs @@ -0,0 +1,41 @@ +using Dlhis.Entity.Canteen.Enums; + +namespace Dlhis.Canteen.Order.Request; + +public class ConsumeRequest +{ + /// + /// 是否离线数据 + /// + public bool IsOffline { get; set; } + + /// + /// 消费机器唯一信息 + /// + public string DeviceUniqueNo { get; set; } + + /// + /// 第三方订单号 + /// + public string ThirdNo { get; set; } + + /// + /// 档案号/QrCode/学工号 + /// + public string UserUniqueNo { get; set; } + + /// + /// 来源 + /// + public BalanceRecordSourceEnumType Source { get; set; } + + /// + /// 金额 + /// + public decimal Amount { get; set; } + + /// + /// 产生时间 + /// + public DateTime CostDateTime { get; set; } +} diff --git a/Src/Dlhis.Canteen/Order/Request/OrderAddRequest.cs b/Src/Dlhis.Canteen/Order/Request/OrderAddRequest.cs index 72c26956..9b8ff088 100644 --- a/Src/Dlhis.Canteen/Order/Request/OrderAddRequest.cs +++ b/Src/Dlhis.Canteen/Order/Request/OrderAddRequest.cs @@ -1,9 +1,10 @@ using System.ComponentModel.DataAnnotations; using Dlhis.Entity.Canteen.Enums; +using Kurisu.AspNetCore.Utils.Extensions; namespace Dlhis.Canteen.Order.Request; -public class OrderAddRequest +public class OrderAddRequest : IValidatableObject { /// /// 是否离线数据 @@ -23,6 +24,7 @@ public class OrderAddRequest /// /// 档案号/QrCode/学工号 /// + [Required(ErrorMessage = "人员档案号/QrCode/学工号不能为空")] public string UserUniqueNo { get; set; } /// @@ -44,7 +46,41 @@ public class OrderAddRequest public decimal Amount { get; set; } /// - /// 消费时间,由机器提供,否则获取当前时间 + /// 产生时间 /// - public DateTime CostDateTime { get; set; } = DateTime.Now; + public DateTime? CostDateTime { get; set; } + + + public IEnumerable Validate(ValidationContext validationContext) + { + //处理金额 + Amount = Type == BalanceRecordTypeEnumType.In ? Math.Abs(Amount) : -Math.Abs(Amount); + + if (Type == BalanceRecordTypeEnumType.Out) + { + //处理消费类型 + if ((int)Source >= (int)BalanceRecordSourceEnumType.Wechat) + { + yield return new ValidationResult($"不支持来源{Source.GetDescription()}", new[] { nameof(Source) }); + } + + //在线就以API时间为准 + if (!IsOffline) + { + CostDateTime = DateTime.Now; + } + + if (DeviceUniqueNo.IsEmpty()) + { + yield return new ValidationResult("设备唯一号不能为空", new[] { nameof(DeviceUniqueNo) }); + } + } + else if (Type == BalanceRecordTypeEnumType.In) + { + if ((int)Source < (int)BalanceRecordSourceEnumType.Wechat) + { + yield return new ValidationResult($"不支持来源{Source.GetDescription()}", new[] { nameof(Source) }); + } + } + } } diff --git a/Src/Dlhis.Canteen/Order/Request/RechargeRequest.cs b/Src/Dlhis.Canteen/Order/Request/RechargeRequest.cs new file mode 100644 index 00000000..0d4c8a7a --- /dev/null +++ b/Src/Dlhis.Canteen/Order/Request/RechargeRequest.cs @@ -0,0 +1,34 @@ +using Dlhis.Entity.Canteen.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dlhis.Canteen.Order.Request; + +public class RechargeRequest +{ + /// + /// 第三方订单号 + /// + public string ThirdNo { get; set; } + + /// + /// 档案号/QrCode/学工号 + /// + public string UserUniqueNo { get; set; } + + /// + /// 来源 + /// + [EnumDataType(typeof(BalanceRecordSourceEnumType))] + public BalanceRecordSourceEnumType Source { get; set; } + + /// + /// 金额 + /// + [Range(0.01, double.MaxValue)] + public decimal Amount { get; set; } + + /// + /// 产生时间 + /// + public DateTime CostDateTime { get; set; } = DateTime.Now; +} diff --git a/Src/Dlhis.Canteen/Order/Response/BalanceRecordResponse.cs b/Src/Dlhis.Canteen/Order/Response/BalanceRecordResponse.cs index 054ba498..4f684d96 100644 --- a/Src/Dlhis.Canteen/Order/Response/BalanceRecordResponse.cs +++ b/Src/Dlhis.Canteen/Order/Response/BalanceRecordResponse.cs @@ -109,4 +109,9 @@ public class BalanceRecordResponse /// 数据状态描述 /// public string StatusDesc => Status.GetDescription(); + + /// + /// 记录产生时间 + /// + public DateTime CostDateTime { get; set; } } diff --git a/Src/Dlhis.Canteen/Order/Response/BalanceRecordUserResponse.cs b/Src/Dlhis.Canteen/Order/Response/BalanceRecordUserResponse.cs index 70ae969d..1ab529aa 100644 --- a/Src/Dlhis.Canteen/Order/Response/BalanceRecordUserResponse.cs +++ b/Src/Dlhis.Canteen/Order/Response/BalanceRecordUserResponse.cs @@ -44,4 +44,9 @@ public class BalanceRecordUserResponse /// 数据状态描述 /// public string StatusDesc => Status.GetDescription(); + + /// + /// 记录产生日期 + /// + public DateTime CostDateTime { get; set; } } diff --git a/Src/Dlhis.Canteen/Order/Response/OrderConsumeResponse.cs b/Src/Dlhis.Canteen/Order/Response/OrderResultResponse.cs similarity index 94% rename from Src/Dlhis.Canteen/Order/Response/OrderConsumeResponse.cs rename to Src/Dlhis.Canteen/Order/Response/OrderResultResponse.cs index 969483d8..69418e4d 100644 --- a/Src/Dlhis.Canteen/Order/Response/OrderConsumeResponse.cs +++ b/Src/Dlhis.Canteen/Order/Response/OrderResultResponse.cs @@ -1,6 +1,6 @@ namespace Dlhis.Canteen.Order.Response; -public class OrderConsumeResponse +public class OrderResultResponse { /// /// 用户工号 diff --git a/Src/Dlhis.Canteen/Order/Services/IOrderService.cs b/Src/Dlhis.Canteen/Order/Services/IOrderService.cs index c7e53edb..b30a7e27 100644 --- a/Src/Dlhis.Canteen/Order/Services/IOrderService.cs +++ b/Src/Dlhis.Canteen/Order/Services/IOrderService.cs @@ -8,5 +8,5 @@ public interface IOrderService : IScopeDependency { Task> GetPageAsync(OrderPageRequest request); - Task AddAsync(OrderAddRequest request); + Task AddAsync(OrderAddRequest request); } diff --git a/Src/Dlhis.Canteen/Order/Services/Implements/BalanceService.cs b/Src/Dlhis.Canteen/Order/Services/Implements/BalanceService.cs index 2ebfb3d2..552c7ae1 100644 --- a/Src/Dlhis.Canteen/Order/Services/Implements/BalanceService.cs +++ b/Src/Dlhis.Canteen/Order/Services/Implements/BalanceService.cs @@ -56,6 +56,7 @@ public class BalanceService : IBalanceService DepartmentName = department.Name, BusinessPlaceName = bplace.Name, BusinessShiftName = bshift.Name, + CostDateTime = record.CreateTime // todo }) .ToPageListAsync(request); @@ -121,6 +122,7 @@ public class BalanceService : IBalanceService DepartmentName = department.Name, BusinessPlaceName = bplace.Name, BusinessShiftName = bshift.Name, + CostDateTime = record.CreateTime // todo }) .ToPageListAsync(request); @@ -151,6 +153,7 @@ public class BalanceService : IBalanceService No = record.No, BusinessPlaceName = bplace.Name, BusinessShiftName = bshift.Name, + CostDateTime = record.CreateTime // todo }) .ToPageListAsync(request); diff --git a/Src/Dlhis.Canteen/Order/Services/Implements/OrderService.cs b/Src/Dlhis.Canteen/Order/Services/Implements/OrderService.cs index 234abb59..44aaa029 100644 --- a/Src/Dlhis.Canteen/Order/Services/Implements/OrderService.cs +++ b/Src/Dlhis.Canteen/Order/Services/Implements/OrderService.cs @@ -24,7 +24,9 @@ public class OrderService : IOrderService private readonly ICanteenSettingService _canteenSettingService; private readonly ILogger _logger; - public OrderService(IDbContext dbContext, ICanteenSettingService canteenSettingService, ILogger logger) + public OrderService(IDbContext dbContext, + ICanteenSettingService canteenSettingService, + ILogger logger) { _dbContext = dbContext; _canteenSettingService = canteenSettingService; @@ -41,40 +43,73 @@ public class OrderService : IOrderService } [IgnoreTenant] - public async Task AddAsync(OrderAddRequest request) + public async Task AddAsync(OrderAddRequest request) + { + //根据档案号 + var canteenUserExists = await _dbContext.Queryable() + .Where(x => x.UserNo == request.UserUniqueNo || x.UserCode == request.UserUniqueNo) + .AnyAsync(); + canteenUserExists.ThrowIfFalse($"就餐用户{request.UserUniqueNo}不存在,无法操作"); + + var gainfo = await _dbContext.Queryable() + .Where(x => x.No == request.UserUniqueNo || x.Code == request.UserUniqueNo) + .FirstAsync(); + gainfo.ThrowIfNull($"用户{request.UserUniqueNo}不存在,无法操作"); + + //消费 + if (request.Type == BalanceRecordTypeEnumType.Out) + { + await ConsumeAsync(request.Adapt(), gainfo); + } + //充值 + else + { + await RechargeAsync(request.Adapt(), gainfo); + } + + //获取人员余额 + return await _dbContext.Queryable() + .LeftJoin((ga, department) => ga.DepartmentNo == department.No) + .LeftJoin((ga, department, mealUser) => ga.No == mealUser.UserNo) + .Where(ga => ga.Id == gainfo.Id) + .Select((ga, department, mealUser) => new OrderResultResponse + { + UserCode = ga.Code, + UserName = ga.Name, + DepartmentNo = ga.DepartmentNo, + DepartmentName = department.Name, + Balance = mealUser.Balance + }) + .SingleAsync(); + } + + /// + /// 消费 + /// + /// + /// + /// + private async Task ConsumeAsync(ConsumeRequest request, GainfoEntity gainfo) { var device = await _dbContext.Queryable().Where(x => x.UniqueNo == request.DeviceUniqueNo).Where(x => x.Enable).FirstAsync(); device.ThrowIfNull("设备不存在或者尚未启用"); var registry = await _dbContext.Queryable().Where(x => x.DeviceUniqueNo == request.DeviceUniqueNo).FirstAsync(); registry.ThrowIfNull("设备尚未注册"); - var businessPlace = await _dbContext.Queryable().Where(x => x.Code == registry.BusinessPlaceCode).FirstAsync(); businessPlace.ThrowIfNull("营业点尚未设置"); - var shifts = await _dbContext.Queryable().Where(x => x.BusinessPlaceCode == registry.BusinessPlaceCode).ToListAsync(); - //todo + //获取当前消费餐次 + var shifts = await _dbContext.Queryable().Where(x => x.BusinessPlaceCode == businessPlace.Code).ToListAsync(); + //todo var shift = shifts.FirstOrDefault(x => new TimePeriod() { Start = x.Start, - End = x.End, + End = x.End }.IsPresent1(request.CostDateTime)); - //根据档案号 - var canteenUser = await _dbContext.Queryable() - .Where(x => x.UserNo == request.UserUniqueNo || x.UserCode == request.UserUniqueNo) - .FirstAsync(); - - canteenUser.ThrowIfNull($"{request.DeviceUniqueNo}不存在,无法就餐"); - - var gafinfo = await _dbContext.Queryable() - .Where(x => x.No == canteenUser.UserNo) - .FirstAsync(); - - var settings = await _canteenSettingService.GetUserSettingAsync(gafinfo.No); + var settings = await _canteenSettingService.GetUserSettingAsync(request.UserUniqueNo); var switchTime = GetSetting(settings, CanteenSettingItemEnumType.BusinessSwitchTime); - //金额处理 - request.Amount = request.Type == BalanceRecordTypeEnumType.In ? Math.Abs(request.Amount) : -Math.Abs(request.Amount); var shiftTimePeriod = new TimePeriod() { @@ -82,7 +117,7 @@ public class OrderService : IOrderService End = shift?.End }; - //成本日期 todo 优化 + //计算成本日期 todo 优化 var costDate = DateTime.Now; if (shift == null) { @@ -106,136 +141,141 @@ public class OrderService : IOrderService { RequestJson = request.ToJson(), ThirdNo = request.ThirdNo, - Type = request.Type.Value, - Name = gafinfo.Name, + Type = BalanceRecordTypeEnumType.Out, + Name = gainfo.Name, Source = request.Source, - UserNo = canteenUser.UserNo, - UserCode = canteenUser.UserCode, + UserNo = gainfo.No, + UserCode = gainfo.Code, Amount = request.Amount, BusinessPlaceCode = businessPlace.Code, BusinessShiftCode = shift?.Code, CostDate = costDate.Date, - CostId = canteenUser.CostId, - DepartmentNo = gafinfo.DepartmentNo, - DeviceInfo = device.Code.ToString(), + CostId = gainfo.CostId, + DepartmentNo = gainfo.DepartmentNo, + DeviceInfo = request.DeviceUniqueNo, No = "O" + SnowFlakeHelper.Instance.NextId(), SubsidiesFromMonth = DateTime.Now.ToString("yyyyMM"), IsOffline = request.IsOffline, IsCostCrossTenant = GetSetting(settings, CanteenSettingItemEnumType.AllowCrossTenant), - OriginTenantId = canteenUser.TenantId,//人员可能跨基地消费 + OriginTenantId = gainfo.Werks,//人员可能跨基地消费 //餐别异常处理 Status = shift == null ? BalanceRecordStatusEnumType.Doubtful : BalanceRecordStatusEnumType.Valid, - OrderStatus = CanteenOrderStatusEnumType.Wait, - //设备才能标识当前消费的基地 - TenantId = device.TenantId, - //默认为admin - CreatedBy = 1, - CreateTime = DateTime.Now, - ModifiedBy = 1, - ModifiedTime = DateTime.Now, + OrderStatus = CanteenOrderStatusEnumType.Wait, + + CostDateTime = request.CostDateTime, + + //标识当前消费的基地 + TenantId = businessPlace.TenantId, }; - var id = await _dbContext.InsertReturnIdentityAsync(order); - var result = await _dbContext.UseTransactionAsync(async () => + var orderId = await _dbContext.InsertReturnIdentityAsync(order); + //await _dbContext.Client.Ado.BeginTranAsync(); + try { - try - { - var r = await GenerateChargeRecordAsync(id, settings, shifts, request.CostDateTime); - return r; - } - catch (Exception) - { - throw; - } - finally - { - //订单消费成功 - order.OrderStatus = CanteenOrderStatusEnumType.Successful; - await _dbContext.UpdateAsync(order); - } - }); + await GenerateConsumeRecordAsync(orderId, settings, shifts, request.CostDateTime); + order.OrderStatus = CanteenOrderStatusEnumType.Successful; - if (!result.IsSuccess) - { - throw new UserFriendlyException(result.ErrorMessage); + //await _dbContext.Client.Ado.CommitTranAsync(); } - - return result.Data; - } - - public async Task GenerateChargeRecordAsync(long orderId, - Dictionary settings, - List shifts, - DateTime costDateTime) - { - var order = await _dbContext.Queryable().Where(x => x.Id == orderId).FirstAsync(); - - //校验 - await Verify(settings, shifts, costDateTime, order); - - - return await ContinueUpdateAsync(async () => + catch (Exception) { - var canteenUser = await _dbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); - //金额+- - canteenUser.Balance += order.Amount; - - if (order.Type == BalanceRecordTypeEnumType.Out) - { - if (canteenUser.Balance < 0) - { - throw new UserFriendlyException("余额不足,无法消费"); - } - } - - var effect = await _dbContext.Updateable() - .SetColumns(x => x.Balance == canteenUser.Balance) - .SetColumns(x => x.Version == x.Version + 1) - .Where(x => x.UserNo == order.UserNo && x.Version == canteenUser.Version)//必须与查询时的version一致 - .ExecuteCommandAsync(); - - if (effect <= 0) throw new VersionExceptions("更新版本冲突"); - - //写入消费记录 - var balanceRecord = order.Adapt(); - await _dbContext.InsertAsync(balanceRecord); - - return await _dbContext.Queryable() - .LeftJoin((ga, department) => ga.DepartmentNo == department.No) - .LeftJoin((ga, department, mealUser) => ga.No == mealUser.UserNo) - .Select((ga, department, mealUser) => new OrderConsumeResponse - { - UserCode = ga.Code, - UserName = ga.Name, - DepartmentNo = ga.DepartmentNo, - DepartmentName = department.Name, - Balance = mealUser.Balance - }) - .FirstAsync(); - }, 5); + order.OrderStatus = CanteenOrderStatusEnumType.Failure; + //await _dbContext.Client.Ado.RollbackTranAsync(); + throw; + } + finally + { + await _dbContext.UpdateAsync(order); + } } + /// + /// 充值 + /// + /// + /// + /// + private async Task RechargeAsync(RechargeRequest request, GainfoEntity gainfo) + { + var settings = await _canteenSettingService.GetUserSettingAsync(request.UserUniqueNo); + + var order = new CanteenOrderEntity + { + RequestJson = request.ToJson(), + ThirdNo = request.ThirdNo, + Type = BalanceRecordTypeEnumType.Out, + Name = gainfo.Name, + Source = request.Source, + UserNo = gainfo.No, + UserCode = gainfo.Code, + Amount = request.Amount, + BusinessPlaceCode = Guid.Empty, + BusinessShiftCode = null, + CostDate = DateTime.Now.Date, + CostId = gainfo.CostId, + DepartmentNo = gainfo.DepartmentNo, + DeviceInfo = string.Empty, + + No = "O" + SnowFlakeHelper.Instance.NextId(), + SubsidiesFromMonth = DateTime.Now.ToString("yyyyMM"), + IsOffline = false, + IsCostCrossTenant = GetSetting(settings, CanteenSettingItemEnumType.AllowCrossTenant), + + OriginTenantId = gainfo.Werks,//人员可能跨基地消费 + + Status = BalanceRecordStatusEnumType.Valid, + + OrderStatus = CanteenOrderStatusEnumType.Wait, + + CostDateTime = request.CostDateTime, + + //标识当前消费的基地 + TenantId = gainfo.Werks, + }; + + var orderId = await _dbContext.InsertReturnIdentityAsync(order); + try + { + await GenerateRechargeRecordAsync(orderId); + order.OrderStatus = CanteenOrderStatusEnumType.Successful; + } + catch (Exception) + { + order.OrderStatus = CanteenOrderStatusEnumType.Failure; + throw; + } + finally + { + await _dbContext.UpdateAsync(order); + } + } + + + + //******************************************************* help methods ************************************************************/ + /// - /// 设备并发更新 + /// 设备并发更新 需要抛出VersionExceptions /// /// /// - /// + /// 并发冲突处理次数 /// /// - private async Task ContinueUpdateAsync(Func> func, int retry) + private async Task ContinueUpdateAsync(Func func, int retry) { for (int i = 0; i < retry; i++) { try { - return await func(); + await func(); + return; } catch (Exception ex) when (ex is VersionExceptions) { @@ -247,7 +287,7 @@ public class OrderService : IOrderService } } - throw new UserFriendlyException("消费失败,请重试"); + throw new UserFriendlyException("操作失败,请重试"); } @@ -270,7 +310,6 @@ public class OrderService : IOrderService } - /// /// 验证参数 /// @@ -342,4 +381,80 @@ public class OrderService : IOrderService } } } + + + /// + /// 生成消费记录 + /// + /// + /// + /// + /// + private async Task GenerateRechargeRecordAsync(long orderId) + { + var order = await _dbContext.Queryable().Where(x => x.Id == orderId).FirstAsync(); + + await ContinueUpdateAsync(async () => + { + var canteenUser = await _dbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); + //金额+- + canteenUser.Balance += order.Amount; + + var effect = await _dbContext.Updateable() + .SetColumns(x => x.Balance == canteenUser.Balance) + .SetColumns(x => x.Version == x.Version + 1) + .Where(x => x.UserNo == order.UserNo && x.Version == canteenUser.Version)//必须与查询时的version一致 + .ExecuteCommandAsync(); + + if (effect <= 0) throw new VersionExceptions("更新版本冲突"); + + //写入记录 + var balanceRecord = order.Adapt(); + await _dbContext.InsertAsync(balanceRecord); + + }, 5); + } + + /// + /// 生成充值记录 + /// + /// + /// + /// + /// + /// + /// + /// + private async Task GenerateConsumeRecordAsync(long orderId, + Dictionary settings, + List shifts, + DateTime costDateTime) + { + var order = await _dbContext.Queryable().Where(x => x.Id == orderId).FirstAsync(); + //校验 + await Verify(settings, shifts, costDateTime, order); + + await ContinueUpdateAsync(async () => + { + var canteenUser = await _dbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); + //金额+- + canteenUser.Balance += order.Amount; + if (canteenUser.Balance < 0) + { + throw new UserFriendlyException("余额不足,无法消费"); + } + + var effect = await _dbContext.Updateable() + .SetColumns(x => x.Balance == canteenUser.Balance) + .SetColumns(x => x.Version == x.Version + 1) + .Where(x => x.UserNo == order.UserNo && x.Version == canteenUser.Version)//必须与查询时的version一致 + .ExecuteCommandAsync(); + + if (effect <= 0) throw new VersionExceptions("更新版本冲突"); + + //写入记录 + var balanceRecord = order.Adapt(); + await _dbContext.InsertAsync(balanceRecord); + }, 5); + } }