using Dlhis.Basic.Api; using Dlhis.Canteen.Dishes.Request; using Dlhis.Canteen.Setting.Services; using Dlhis.Entity.Basic.Entity; using Dlhis.Entity.Canteen.Entities; using Dlhis.Entity.Canteen.Enums; using Dlhis.Entity.Device.Entities; using Kurisu.AspNetCore.UnifyResultAndValidation; using Kurisu.AspNetCore.Utils; using Kurisu.AspNetCore.Utils.Extensions; using Kurisu.Core.CustomClass; using Kurisu.Core.DataAccess.Dto; using Kurisu.Core.User.Abstractions; using Kurisu.SqlSugar.Extensions; using Kurisu.SqlSugar.Services; using Mapster; using SqlSugar; using Dlhis.Canteen.Order.Response; using Dlhis.Canteen.Dishes; using Dlhis.Canteen.Order.Request; using System.Numerics; using MySqlX.XDevAPI.Relational; namespace Dlhis.Canteen.Order.Services.Implements; public class OrderService : IOrderService { private readonly IDbContext _dbContext; private readonly ICanteenSettingService _canteenSettingService; private readonly IDevicePermissionApi _devicePermissionApi; private readonly ICurrentUser _currentUser; private readonly IDisheMenuApi _disheMenuApi; private readonly ILogger _logger; public OrderService(IDbContext dbContext, ICanteenSettingService canteenSettingService, IDevicePermissionApi devicePermissionApi, ICurrentUser currentUser, IDisheMenuApi disheMenuApi, ILogger logger) { _dbContext = dbContext; _canteenSettingService = canteenSettingService; _devicePermissionApi = devicePermissionApi; _currentUser = currentUser; _disheMenuApi = disheMenuApi; _logger = logger; } public async Task> GetPageAsync(OrderPageRequest request) { var page = await _dbContext.Queryable() .Select() .ToPageListAsync(request); return page; } private async Task CheckPAsync(string userNo, string deviceUniqueNo) { var uniqueNosResult = await _devicePermissionApi.GetCanteenDeviceUniqueNosByUserNoAsync(userNo); var uniqueNos = uniqueNosResult.Data; uniqueNos.Contains(deviceUniqueNo).ThrowIfFalse("无消费权限"); } public async Task BookingAsync(BookingMealRequest request) { var gainfo = await CheckExistsMyBookingsAsync(request.BookingDate, request.ShiftType); var businessPlace = await _dbContext.Queryable().Where(x => x.Code == request.BusinessPlaceCode).FirstAsync(); //预定时间 var now = DateTime.Now; var settings = await _canteenSettingService.GetUserSettingAsync(gainfo.Code); var bookingDeadline = GetSetting(settings, CanteenSettingItemEnumType.BookingDeadline) ?? new TimeOnly(20, 30, 00); var dealLine = DateTime.Today.Add(bookingDeadline.ToTimeSpan()); (now >= dealLine).ThrowIfTrue("超出订餐时间"); //菜单存在 var menuDetailResult = await _disheMenuApi.GetBookingDishesDetailAsync(request.BookingDate, request.BusinessPlaceCode, (int)request.ShiftType, request.DishesCode); var menuDetail = menuDetailResult.Data; menuDetail.ThrowIfNull("预定失败,菜品已经下架"); var order = new CanteenOrderEntity { RequestJson = request.ToJson(), ThirdNo = string.Empty, Type = BalanceRecordTypeEnumType.Out, ApplicationType = CanteenApplicaitonTypeEnumType.Booking, Name = gainfo.Name, Source = request.Source, UserNo = gainfo.No, UserCode = gainfo.Code, Amount = -Math.Abs(menuDetail.Price), BusinessPlaceCode = request.BusinessPlaceCode, BusinessShiftCode = null, ShiftType = request.ShiftType, CostDate = now.Date, CostId = gainfo.CostId, DepartmentNo = gainfo.DepartmentNo, DeviceUniqueNo = string.Empty, DeliveryAddress = request.DeliveryAddress, No = SnowFlakeHelper.Instance.NextId().ToString(), SubsidiesFromMonth = now.ToString("yyyyMM"), IsOffline = false, IsCostCrossTenant = GetSetting(settings, CanteenSettingItemEnumType.AllowCrossTenant), OriginTenantId = gainfo.Factory,//人员可能跨基地消费 Status = BalanceRecordStatusEnumType.Valid, OrderStatus = CanteenOrderStatusEnumType.Successful, CostDateTime = now, //标识当前消费的基地 TenantId = businessPlace.TenantId, }; var orderId = await _dbContext.InsertReturnIdentityAsync(order); await ContinueUpdateAsync(async () => { await _disheMenuApi.GetBookingDishesDetailAsync(request.BookingDate, request.BusinessPlaceCode, (int)request.ShiftType, request.DishesCode); var menuDetail = menuDetailResult.Data; menuDetail.ThrowIfNull("预定失败,菜品已经下架"); menuDetail.Number -= 1; (menuDetail.Number < 0).ThrowIfTrue($"{menuDetail.DishesName}已售罄"); var mealInfo = await _dbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); if (mealInfo == null) { await _dbContext.InsertAsync(new GainfoMealEntity { Balance = 0, BalanceOfRecharge = 0, BalanceOfSubsidies = 0, UserCode = gainfo.Code, UserNo = gainfo.No, Version = 0, }); (mealInfo == null).ThrowIfTrue("余额不足,无法消费"); } //金额+- mealInfo.Balance += order.Amount; (mealInfo.Balance < 0).ThrowIfTrue("余额不足,无法消费"); var effect = await _dbContext.Updateable() .SetColumns(x => x.Balance == mealInfo.Balance) .SetColumns(x => x.Version == x.Version + 1) .Where(x => x.UserNo == order.UserNo && x.Version == mealInfo.Version)//必须与查询时的version一致 .ExecuteCommandAsync(); if (effect <= 0) throw new VersionExceptions("更新版本冲突"); effect = await _dbContext.Updateable() .SetColumns(x => x.Number == menuDetail.Number) .SetColumns(x => x.Version == x.Version + 1) .SetColumns(x => x.ModifiedTime == now) .Where(x => x.Date == menuDetail.Date && x.ShiftType == request.ShiftType && x.BusinessPlaceCode == request.BusinessPlaceCode && x.DishesCode == request.DishesCode && x.Version == menuDetail.Version ) .ExecuteCommandAsync(); if (effect <= 0) throw new VersionExceptions("更新版本冲突"); //写入记录 var balanceRecord = order.Adapt(); await _dbContext.InsertAsync(balanceRecord); await _dbContext.InsertAsync(new CanteenOrderDetailEntity { OrderNo = order.No, DishesCode = request.DishesCode, Price = menuDetail.Price, //Unit Number = 1, }); }, 10); } //public async Task> GetDetailsAsync(string no) //{ // return await _dbContext.Queryable() // .LeftJoin((x, d) => x.DishesCode == d.CustomCode) // .Where(x => x.OrderNo == no) // .Select((x, d) => new OrderDetailResponse // { // DishesCode = x.DishesCode, // Price = x.Price, // Name = d.Name, // Number = x.Number, // Unit = x.Unit, // ImageUrl = d.ImageUrl, // }) // .ToListAsync(); //} public async Task DeviceConsume(OrderAddRequest request) { return await AddAsync(request); } public async Task AddAsync(OrderAddRequest request) { //尝试匹配userId _ = long.TryParse(request.UserUniqueNo, out var userId); //根据档案号 var gainfo = await _dbContext.Queryable() .Where(x => x.No == request.UserUniqueNo || x.Code == request.UserUniqueNo || x.Id == userId || x.CardNo == request.UserUniqueNo) .FirstAsync(); gainfo.ThrowIfNull($"无消费权限"); //消费权限控制 await CheckPAsync(gainfo.No, request.DeviceUniqueNo); var canteenUserExists = await _dbContext.Queryable() .Where(x => x.UserNo == gainfo.No) .AnyAsync(); if (!canteenUserExists) { await _dbContext.InsertAsync(new GainfoMealEntity { UserNo = gainfo.No, Balance = 0, BalanceOfRecharge = 0, BalanceOfSubsidies = 0, TenantId = gainfo.Factory, Version = 0, CreatedBy = 1, CreateTime = DateTime.Now, ModifiedBy = 1, ModifiedTime = DateTime.Now, }); } //消费 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, CurrentBalance = 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 businessPlaceInfo = await _dbContext.Queryable() .LeftJoin((x, sr) => x.Code == sr.PlaceCode) .Where(x => x.Code == registry.BusinessPlaceCode) .Select((x, sr) => new { x.Code, sr.ShiftCode, x.TenantId }) .FirstAsync(); businessPlaceInfo.ThrowIfNull("营业点尚未设置"); //获取当前消费餐次 var shifts = await _dbContext.Queryable().Where(x => x.ShiftCode == businessPlaceInfo.ShiftCode).ToListAsync(); var shift = shifts.FirstOrDefault(x => new TimePeriod() { Start = x.Start, End = x.End }.IsPresent1(request.CostDateTime)); var settings = await _canteenSettingService.GetUserSettingAsync(request.UserUniqueNo); var switchTime = GetSetting(settings, CanteenSettingItemEnumType.BusinessSwitchTime); var shiftTimePeriod = new TimePeriod() { Start = shift?.Start, End = shift?.End }; //计算成本日期 todo 优化 var costDate = DateTime.Now; if (shift == null) { costDate = DateTime.Now; } else { if (switchTime.HasValue) { var beginOfDay = DateTime.Today; var setTime = beginOfDay.Add(switchTime.Value.ToTimeSpan()); if (DateTime.Now < setTime) costDate = DateTime.Now.AddDays(-1); } } var order = new CanteenOrderEntity { RequestJson = request.ToJson(), ThirdNo = request.ThirdNo, Type = BalanceRecordTypeEnumType.Out, ApplicationType = CanteenApplicaitonTypeEnumType.Device, Name = gainfo.Name, Source = request.Source, UserNo = gainfo.No, UserCode = gainfo.Code, Amount = request.Amount, BusinessPlaceCode = businessPlaceInfo.Code, BusinessShiftCode = shift?.ShiftCode, ShiftType = shift?.ShiftType, CostDate = costDate.Date, CostId = gainfo.CostId, DepartmentNo = gainfo.DepartmentNo, DeviceUniqueNo = request.DeviceUniqueNo, No = SnowFlakeHelper.Instance.NextId().ToString(), SubsidiesFromMonth = request.CostDateTime.ToString("yyyyMM"), IsOffline = request.IsOffline, IsCostCrossTenant = GetSetting(settings, CanteenSettingItemEnumType.AllowCrossTenant), OriginTenantId = gainfo.Factory,//人员可能跨基地消费 //餐别异常处理 Status = shift == null ? BalanceRecordStatusEnumType.Doubtful : BalanceRecordStatusEnumType.Valid, OrderStatus = CanteenOrderStatusEnumType.Wait, CostDateTime = request.CostDateTime, //标识当前消费的基地 TenantId = businessPlaceInfo.TenantId, }; var orderId = await _dbContext.InsertReturnIdentityAsync(order); try { await GenerateConsumeRecordAsync(orderId, settings, shifts, request.CostDateTime); } catch (Exception ex) { await _dbContext.Updateable() .SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Failure) .SetColumns(x => x.Result == ex.Message) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.Id == orderId) .ExecuteCommandAsync(); throw; } await _dbContext.Updateable() .SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Successful) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.Id == orderId) .ExecuteCommandAsync(); } /// /// 充值 /// /// /// /// private async Task RechargeAsync(RechargeRequest request, GainfoEntity gainfo) { var settings = await _canteenSettingService.GetUserSettingAsync(request.UserUniqueNo); var order = new CanteenOrderEntity { RequestJson = request.ToJson(), IsOffline = false, ThirdNo = request.ThirdNo, Type = BalanceRecordTypeEnumType.In, Name = gainfo.Name, Source = request.Source, UserNo = gainfo.No, UserCode = gainfo.Code, Amount = request.Amount, BusinessPlaceCode = null, BusinessShiftCode = null, CostDate = DateTime.Now.Date, CostId = gainfo.CostId, DepartmentNo = gainfo.DepartmentNo, DeviceUniqueNo = string.Empty, No = SnowFlakeHelper.Instance.NextId().ToString(), SubsidiesFromMonth = request.CostDateTime.ToString("yyyyMM"), IsCostCrossTenant = GetSetting(settings, CanteenSettingItemEnumType.AllowCrossTenant), OriginTenantId = gainfo.Factory,//人员可能跨基地消费 Status = BalanceRecordStatusEnumType.Valid, OrderStatus = CanteenOrderStatusEnumType.Wait, CostDateTime = request.CostDateTime, //标识当前消费的基地 TenantId = gainfo.Factory, }; var orderId = await _dbContext.InsertReturnIdentityAsync(order); try { await GenerateRechargeRecordAsync(orderId); } catch (Exception ex) { await _dbContext.Updateable() .SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Failure) .SetColumns(x => x.Result == ex.Message) .Where(x => x.Id == orderId) .ExecuteCommandAsync(); throw; } await _dbContext.Updateable() .SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Successful) .Where(x => x.Id == orderId) .ExecuteCommandAsync(); } public async Task> GetOrdersAggregationAsync(PageDto page) { var subjectId = _currentUser.GetIntSubjectId(); var gainfo = await _dbContext.Queryable().Where(x => x.IdentityUserId == subjectId).FirstAsync(); var jinkoCode = _currentUser.GetUserClaim("jinko-code") ?? string.Empty; if (jinkoCode.IsPresent()) { gainfo = await _dbContext.Queryable().Where(x => x.Code == jinkoCode).FirstAsync(); } var pageResult = await _dbContext.Queryable().Where(x => x.UserNo == gainfo.No) .LeftJoin((x, bplace) => x.BusinessPlaceCode == bplace.Code) .Where(x => x.ApplicationType == CanteenApplicaitonTypeEnumType.Booking) .Select((x, bplace) => new OrderAggregationResponse { No = x.No, BusinessPlaceName = bplace.Name, }) .ToPageListAsync(page); //.LeftJoin((x, bplace, detail) => x.No == detail.OrderNo) var nos = pageResult.Data.Select(x => x.No).ToList(); var dtos = await _dbContext.Queryable() .LeftJoin((x, d) => x.DishesCode == d.CustomCode) .Where(x => nos.Contains(x.OrderNo)) .Select((x, d) => new DishesDto { No = x.OrderNo, DishesCode = x.DishesCode, Name = d.Name, ImageUrl = d.ImageUrl, Number = x.Number, Price = x.Price }) .ToListAsync(); pageResult.Data.ForEach(item => { item.Dishes = dtos.Where(x => x.No == item.No).ToList(); }); return pageResult; } //******************************************************* help methods ************************************************************/ /// /// 设备并发更新 需要抛出VersionExceptions /// /// /// /// 并发冲突处理次数 /// /// private async Task ContinueUpdateAsync(Func func, int retry) { for (int i = 0; i < retry; i++) { try { await func(); return; } catch (Exception ex) when (ex is VersionExceptions) { _logger.LogWarning(ex, ex.Message); } catch (Exception) { throw; } } throw new UserFriendlyException("操作失败,请重试"); } /// /// 获取参数配置 /// /// /// /// /// /// private static T GetSetting(Dictionary settings, CanteenSettingItemEnumType item, T defaultValue = default) { if (settings.TryGetValue(item, out var v)) { try { if (int.TryParse(v, out var n)) { return n.Adapt(); } return v.Adapt(); } catch (Exception) { return defaultValue; } } return defaultValue; } /// /// 验证参数 /// /// /// /// /// /// /// private async Task Verify(Dictionary settings, List shifts, DateTime costDateTime, CanteenOrderEntity order) { //在线 if (!order.IsOffline) { shifts.FirstOrDefault(x => new TimePeriod() { Start = x.Start, End = x.End, }.IsPresent1(costDateTime)).ThrowIfNull("当前非就餐时间,无法消费"); } var beginOfDay = DateTime.Today; var endOfDay = beginOfDay.AddHours(23).AddMinutes(59).AddSeconds(59); var balanceRecords = await _dbContext.Queryable() .Where(x => x.UserNo == order.UserNo) .Where(x => x.CreateTime >= beginOfDay && x.CreateTime <= endOfDay) //今天消费记录 .ToListAsync(); var dayMarketCostMax = GetSetting(settings, CanteenSettingItemEnumType.DayMarketCostMax); if (dayMarketCostMax != default) { var businessPlaceCodes = await _dbContext.Queryable() .Where(x => x.Catalog == CanteenBusinessPlaceCatalogEnumType.Supermarket) .Select(x => x.Code).ToListAsync(); var cost = balanceRecords.Where(x => businessPlaceCodes.Contains(x.BusinessPlaceCode.Value)).Sum(x => x.Amount); if (cost + Math.Abs(order.Amount) > dayMarketCostMax) { throw new UserFriendlyException($"超出当天超市消费限制({dayMarketCostMax}),无法消费"); } } var allowRepeatInOneShift = GetSetting(settings, CanteenSettingItemEnumType.AllowRepeatInOneShift, true); if (!allowRepeatInOneShift) { //todo 餐次问题 var costCount = balanceRecords.Where(x => x.BusinessShiftCode == order.BusinessShiftCode).Count(); if (costCount > 1) { throw new UserFriendlyException($"当前餐次不允许多次消费"); } } var onceCostMax = GetSetting(settings, CanteenSettingItemEnumType.OnceCostMax); if (onceCostMax != default) { if (Math.Abs(order.Amount) > onceCostMax) { throw new UserFriendlyException($"一次消费金额不能超出{onceCostMax}"); } } var dayCostMax = GetSetting(settings, CanteenSettingItemEnumType.DayCostMax); if (dayCostMax != default) { var cost = balanceRecords.Where(x => x.Type == BalanceRecordTypeEnumType.Out).Sum(x => Math.Abs(x.Amount)); if (cost + Math.Abs(order.Amount) > dayCostMax) { throw new UserFriendlyException($"超出当天消费限制({dayCostMax}),无法消费"); } } } /// /// 生成消费记录 /// /// /// /// /// 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) .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); var allowNoBalance = GetSetting(settings, CanteenSettingItemEnumType.AllowNoBalance); await ContinueUpdateAsync(async () => { if (!allowNoBalance) { 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); } /// /// 检查我的订餐是否存在,存在抛出异常 /// /// /// /// private async Task CheckExistsMyBookingsAsync(DateTime bookingDate, ShiftEnumType shiftType) { var subjectId = _currentUser.GetIntSubjectId(); var gainfo = await _dbContext.Queryable().Where(x => x.IdentityUserId == subjectId).FirstAsync(); var jinkoCode = _currentUser.GetUserClaim("jinko-code") ?? string.Empty; if (jinkoCode.IsPresent()) { gainfo = await _dbContext.Queryable().Where(x => x.Code == jinkoCode).FirstAsync(); } var start = bookingDate.Date.AddDays(-1); var end = start.AddDays(1).AddSeconds(-1); var exists = await _dbContext.Queryable() .Where(x => x.ApplicationType == CanteenApplicaitonTypeEnumType.Booking) .Where(x => x.ShiftType == shiftType) .Where(x => x.UserNo == gainfo.No) .Where(x => x.CreateTime >= start && x.CreateTime <= end) .AnyAsync(); exists.ThrowIfTrue("不可重复订餐"); return gainfo; } }