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.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 Kurisu.Core.Result; using Org.BouncyCastle.Asn1.Ocsp; namespace Dlhis.Canteen.Order.Services.Implements; public class OrderService : BaseOrderService, IOrderService { private readonly RedisCache _redisCache; public OrderService(ICanteenSettingService canteenSettingService, IDbContext dbContext, ICurrentUser currentUser, IDevicePermissionApi devicePermissionApi, RedisCache redisCache, ILogger logger) : base(canteenSettingService, dbContext, currentUser, devicePermissionApi, logger) { _redisCache = redisCache; } 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(); var message = string.Empty; if (request.IsOffline) { if (request.ThirdNo.IsPresent()) { using (var handler = await _redisCache.LockAsync($"consume:device:{request.ThirdNo}")) { if (handler.Acquired) { if (await ToCheckAsync(request.ThirdNo)) { try { await HandleOfflineAsync(request.Adapt(), gainfo); } catch (Exception ex) { Logger.LogError(ex, "离线入库错误: 订单号:{no} .{message}", request.ThirdNo, ex.Message); } } } } } else { try { await HandleOfflineAsync(request.Adapt(), gainfo); } catch (Exception ex) { Logger.LogError(ex, "离线入库错误: 订单号:{no} .{message}", request.ThirdNo, ex.Message); } } } else { gainfo.ThrowIfNull($"无消费权限"); await CheckPermissionAsync(gainfo.No, request.DeviceUniqueNo); try { (await DbContext.Queryable().Where(x => x.UserNo == gainfo.No).AnyAsync()) .ThrowIfFalse("余额不足,无法消费"); await ConsumeAsync(request.Adapt(), gainfo); } catch (Exception ex) { message = ex.Message; } } //获取人员余额 var data = 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(); return new DefaultApiResult { Code = message.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success, Data = data, Msg = message, }; } private async Task ToCheckAsync(string thirdNo) { var order = await DbContext.Queryable() .Where(x => x.ThirdNo == thirdNo) .OrderBy(x => x.CreateTime, OrderByType.Desc) .FirstAsync(); if (order == null) { return true; } if (!order.IsOffline && order.OrderStatus != CanteenOrderStatusEnumType.Success) { return true; } return false; } private async Task HandleOfflineAsync(ConsumeRequest request, GainfoEntity gainfo) { var device = await DbContext.Queryable() .Where(x => x.UniqueNo == request.DeviceUniqueNo) .FirstAsync(); var registry = await DbContext.Queryable() .Where(x => x.DeviceUniqueNo == request.DeviceUniqueNo) .FirstAsync(); 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(); //获取当前消费餐次 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 switchTime = await GetSettingAsync(businessPlaceInfo.TenantId, CanteenSettingItemEnumType.BusinessSwitchTime); //计算成本日期 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 = await GetSettingAsync(businessPlaceInfo.TenantId, CanteenSettingItemEnumType.AllowCrossTenant), OriginTenantId = gainfo.Factory, Werks = gainfo.Werks, Status = BalanceRecordStatusEnumType.Valid, OrderStatus = CanteenOrderStatusEnumType.Success, CostDateTime = request.CostDateTime, //标识当前消费的基地 TenantId = businessPlaceInfo.TenantId, }; var orderId = await DbContext.InsertReturnIdentityAsync(order); var allowNoBalance = await GetSettingAsync(businessPlaceInfo.TenantId, CanteenSettingItemEnumType.AllowNoBalance); await CommonHelper.RetryAsync(async () => { if (!allowNoBalance) { GainfoMealEntity gainfoMeal = null; using (var handler = await _redisCache.LockAsync($"gainfo:meal:{order.UserNo}", retryInterval: TimeSpan.FromSeconds(1))) { if (handler.Acquired) { gainfoMeal = await DbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); if (gainfoMeal == null) { gainfoMeal = new GainfoMealEntity { Balance = 0, BalanceOfRecharge = 0, BalanceOfSubsidies = 0, Version = 0, UserNo = order.UserNo, UserCode = order.UserCode, CreatedBy = 1, CreateTime = DateTime.Now, ModifiedBy = 1, ModifiedTime = DateTime.Now, }; await DbContext.InsertAsync(gainfoMeal); } } } //金额+- gainfoMeal.Balance += order.Amount; await DbContext.Updateable() .SetColumns(x => x.Balance == gainfoMeal.Balance) .SetColumns(x => x.Version == x.Version + 1) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.UserNo == order.UserNo && x.Version == gainfoMeal.Version)//必须与查询时的version一致 .ExecuteCommandThrowVersionAsync(); } //写入记录 var balanceRecord = order.Adapt(); await DbContext.InsertAsync(balanceRecord); }, 5); } /// /// 消费 /// /// /// /// private async Task ConsumeAsync(ConsumeRequest request, GainfoEntity gainfo) { var registry = await DbContext.Queryable().Where(x => x.DeviceUniqueNo == request.DeviceUniqueNo).SingleAsync(); registry.ThrowIfNull("设备尚未登记"); var device = await DbContext.Queryable().Where(x => x.UniqueNo == request.DeviceUniqueNo).SingleAsync(); device.ThrowIfNull("设备尚未登记"); device.Enable.ThrowIfFalse("设备已被停用"); 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(); shifts.ThrowIfEmpty("营业点餐次尚未设置"); var shift = shifts.FirstOrDefault(x => new TimePeriod() { Start = x.Start, End = x.End }.IsPresent1(request.CostDateTime)); shift.ThrowIfNull("非就餐时间,无法消费"); var switchTime = await GetSettingAsync(businessPlaceInfo.TenantId, CanteenSettingItemEnumType.BusinessSwitchTime); //计算成本日期 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 = await GetSettingAsync(businessPlaceInfo.TenantId, CanteenSettingItemEnumType.AllowCrossTenant), OriginTenantId = gainfo.Factory, Werks = gainfo.Werks, //餐别异常处理 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, 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.Success) .SetColumns(x => x.ModifiedTime == DateTime.Now) .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) .OrderBy(x => x.CreateTime, OrderByType.Desc) .Select((x, bplace) => new OrderAggregationResponse { No = x.No, BusinessPlaceName = bplace.Name, CreateTime = x.CreateTime, Remark = x.Remark, OrderStatus = x.OrderStatus, }) .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; } public async Task UpdateOffline(UpdateOfflineRequest outRequest) { var orders = await DbContext.Queryable().Where(x => x.IsOffline) .Where(x => x.Status == BalanceRecordStatusEnumType.Valid) .Where(x => x.OrderStatus == CanteenOrderStatusEnumType.Success) .Where(x => x.ApplicationType == CanteenApplicaitonTypeEnumType.Device) .Where(x => SqlFunc.IsNullOrEmpty(x.BusinessShiftCode)) .WhereIF(outRequest.No.IsPresent(), x => x.No == outRequest.No) .ToListAsync(); foreach (var item in orders) { var request = item.RequestJson.ToObject(); var device = await DbContext.Queryable() .Where(x => x.UniqueNo == request.DeviceUniqueNo) .FirstAsync(); var registry = await DbContext.Queryable() .Where(x => x.DeviceUniqueNo == request.DeviceUniqueNo) .FirstAsync(); 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(); //获取当前消费餐次 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 switchTime = await GetSettingAsync(businessPlaceInfo.TenantId, CanteenSettingItemEnumType.BusinessSwitchTime); //计算成本日期 todo 优化 var costDate = request.CostDateTime.Date; if (shift == null) { costDate = request.CostDateTime.Date; } else { if (switchTime.HasValue) { var beginOfDay = request.CostDateTime.Date; var setTime = beginOfDay.Add(switchTime.Value.ToTimeSpan()); if (request.CostDateTime < setTime) costDate = request.CostDateTime.Date.AddDays(-1); } } var shiftType = shift?.ShiftType; await DbContext.Updateable() .SetColumns(x => x.BusinessShiftCode == businessPlaceInfo.ShiftCode) .SetColumns(x => x.CostDate == costDate) .SetColumns(x => x.CostDateTime == request.CostDateTime) .SetColumns(x => x.ShiftType == shiftType) .Where(x => x.No == item.No) .ExecuteCommandAsync(); await DbContext.Updateable() .SetColumns(x => x.BusinessShiftCode == businessPlaceInfo.ShiftCode) .SetColumns(x => x.CostDate == costDate) .SetColumns(x => x.CostDateTime == request.CostDateTime) .SetColumns(x => x.ShiftType == shiftType) .Where(x => x.No == item.No) .ExecuteCommandAsync(); } } //******************************************************* help methods ************************************************************/ /// /// 验证参数 /// /// /// /// /// /// private async Task Verify(List shifts, DateTime costDateTime, CanteenOrderEntity order) { shifts.FirstOrDefault(x => new TimePeriod() { Start = x.Start, End = x.End, }.IsPresent1(costDateTime)).ThrowIfNull("当前非就餐时间,无法消费"); var beginOfDay = DateTime.Today; var endOfDay = beginOfDay.AddDays(1).AddSeconds(-1); //今天所有消费记录 var todayAllBalanceRecords = await DbContext.Queryable() .Where(x => x.UserNo == order.UserNo) .Where(x => x.Type == BalanceRecordTypeEnumType.Out) .Where(x => x.Status == BalanceRecordStatusEnumType.Valid) .Where(x => x.CreateTime >= beginOfDay && x.CreateTime <= endOfDay) .ToListAsync(); //超市营业点 var supermarketCodes = await DbContext.Queryable() .Where(x => x.Catalog == CanteenBusinessPlaceCatalogEnumType.Supermarket) .Select(x => x.Code) .ToListAsync(); //一天消费金额 var dayCostMax = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.DayCostMax); if (dayCostMax != default) { //must has business place code, use .Value is safe var cost = todayAllBalanceRecords .Where(x => !supermarketCodes.Contains(x.BusinessPlaceCode.Value)) //filter supermarket .Sum(x => Math.Abs(x.Amount)); (cost + Math.Abs(order.Amount) > dayCostMax).ThrowIfTrue($"超出当天消费限制{dayCostMax}元,无法消费"); } //一次消费金额 var onceCostMax = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.OnceCostMax); if (onceCostMax != default) { (Math.Abs(order.Amount) > onceCostMax).ThrowIfTrue($"单次消费金额不能超出{onceCostMax}元"); } //超时消费限额 var dayMarketCostMax = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.DayMarketCostMax); if (dayMarketCostMax != default) { //current businessplace is supermarket if (order.BusinessPlaceCode.HasValue && supermarketCodes.Contains(order.BusinessPlaceCode.Value)) { var cost = todayAllBalanceRecords.Where(x => x.BusinessPlaceCode.HasValue) .Where(x => supermarketCodes.Contains(x.BusinessPlaceCode.Value)) .Sum(x => Math.Abs(x.Amount)); (cost + Math.Abs(order.Amount) > dayMarketCostMax).ThrowIfTrue($"超出当天超市消费限制{dayMarketCostMax},无法消费"); } } //重复餐次消费 var allowRepeatInOneShift = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.AllowRepeatInOneShift, true); if (!allowRepeatInOneShift) { var costCount = todayAllBalanceRecords.Where(x => x.ShiftType == order.ShiftType).Count(); (costCount > 1).ThrowIfTrue($"{order?.ShiftType?.GetDescription()}不允许多次消费"); } } /// /// 生成充值记录 /// /// /// /// /// /// /// private async Task GenerateConsumeRecordAsync(long orderId, List shifts, DateTime costDateTime) { var order = await DbContext.Queryable().Where(x => x.Id == orderId).FirstAsync(); //校验 await Verify(shifts, costDateTime, order); var allowNoBalance = await GetSettingAsync(order.TenantId, CanteenSettingItemEnumType.AllowNoBalance); await CommonHelper.RetryAsync(async () => { if (!allowNoBalance) { var gainfoMeal = await DbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); //金额+- gainfoMeal.Balance += order.Amount; if (gainfoMeal.Balance < 0) { throw new UserFriendlyException("余额不足,无法消费"); } await DbContext.Updateable() .SetColumns(x => x.Balance == gainfoMeal.Balance) .SetColumns(x => x.Version == x.Version + 1) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.UserNo == order.UserNo && x.Version == gainfoMeal.Version)//必须与查询时的version一致 .ExecuteCommandThrowVersionAsync(); } //写入记录 var balanceRecord = order.Adapt(); await DbContext.InsertAsync(balanceRecord); }, 5); } public async Task ReCalcOfflineAsync(ReCalcOfflineRequest request) { var gainfo = await DbContext.Queryable().Where(x => x.Code == request.UserCode).FirstAsync(); var nos = await DbContext.Queryable().Where(x => x.UserCode == request.UserCode).Select(x => x.No).ToListAsync(); var orders = await DbContext.Queryable() .Where(x => x.IsOffline) .Where(x => !nos.Contains(x.No)) .Where(x => x.UserCode == request.UserCode) .ToListAsync(); var gainfoMeal = await DbContext.Queryable().Where(x => x.UserNo == gainfo.No).FirstAsync(); if (gainfoMeal == null) { gainfoMeal = new GainfoMealEntity { Balance = 0, BalanceOfRecharge = 0, BalanceOfSubsidies = 0, Version = 0, UserNo = gainfo.No, UserCode = gainfo.Code, CreatedBy = 1, CreateTime = DateTime.Now, ModifiedBy = 1, ModifiedTime = DateTime.Now, }; await DbContext.InsertAsync(gainfoMeal); } foreach (var order in orders) { await CommonHelper.RetryAsync(async () => { var gainfoMeal = await DbContext.Queryable().Where(x => x.UserNo == order.UserNo).FirstAsync(); //金额+- gainfoMeal.Balance += order.Amount; await DbContext.Updateable() .SetColumns(x => x.Balance == gainfoMeal.Balance) .SetColumns(x => x.Version == x.Version + 1) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.UserNo == order.UserNo && x.Version == gainfoMeal.Version)//必须与查询时的version一致 .ExecuteCommandThrowVersionAsync(); //写入记录 var balanceRecord = order.Adapt(); balanceRecord.Id = 0; await DbContext.InsertAsync(balanceRecord); }, 5); } } }