using Dlhis.Basic.Api; using Dlhis.Canteen.Order.Dto; using Dlhis.Canteen.Order.Repository; 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 Mapster; using SqlSugar; using Dlhis.Canteen.Order.Response; using Dlhis.Canteen.Order.Request; using Dlhis.Entity.Device; using Dlhis.Entity.Device.Enums; using Kurisu.AspNetCore.Authentication.Abstractions; using Kurisu.AspNetCore.Cache; using Kurisu.AspNetCore.CustomClass; using Kurisu.AspNetCore.DataAccess.Dto; using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions; using Kurisu.AspNetCore.DataAccess.SqlSugar.Services; using Kurisu.AspNetCore.UnifyResultAndValidation; namespace Dlhis.Canteen.Order.Services.Implements; public class OrderService : IOrderService { private readonly ICanteenSettingService _canteenSettingService; private readonly IDbContext _dbContext; private readonly ICurrentUser _currentUser; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBalanceRepository _balanceRepository; private readonly IOrderRepository _orderRepository; private readonly RedisCache _redisCache; private readonly IGainfoPermissionApi _gainfoPermissionApi; private readonly ILogger _logger; public OrderService(ICanteenSettingService canteenSettingService, IDbContext dbContext, ICurrentUser currentUser, IHttpContextAccessor httpContextAccessor, IBalanceRepository balanceRepository, IOrderRepository orderRepository, RedisCache redisCache, IGainfoPermissionApi gainfoPermissionApi, ILogger logger) { _canteenSettingService = canteenSettingService; _dbContext = dbContext; _currentUser = currentUser; _httpContextAccessor = httpContextAccessor; _balanceRepository = balanceRepository; _orderRepository = orderRepository; _redisCache = redisCache; _gainfoPermissionApi = gainfoPermissionApi; _logger = logger; } /// /// 设备消费 /// /// /// public async Task> DeviceConsume(OrderAddRequest request) { return await AddAsync(request); } /// /// 设备消费 /// /// /// public async Task> AddAsync(OrderAddRequest request) { await SetDeviceIpInfoAsync(request.DeviceUniqueNo); await using var handler = await _redisCache.LockAsync($"consume:device:{request.UserUniqueNo}"); handler.Acquired.ThrowIfFalse("请勿频繁消费"); var gainfo = await GetUserAsync(request); return await ToDoAsync(request, gainfo); } 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).SingleAsync(); } var pageResult = await _dbContext.Queryable().Where(x => x.UserNo == gainfo.No) .LeftJoin((x, bPlace) => x.BusinessPlaceCode == bPlace.Code) .Where(x => x.ApplicationType == CanteenApplicationTypeEnumType.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); 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 ************************************************************/ private async Task CheckPermissionAsync(string userCode, string deviceUniqueNo) { var data = await _gainfoPermissionApi.GetPermissionsDeviceCodesAsync(userCode); var code = await _dbContext.Queryable().Where(x => x.UniqueNo == deviceUniqueNo).Select(x => x.Code).SingleAsync(); data.Contains(code).ThrowIfFalse("无权消费"); } /// /// 获取当前消费人员 /// /// /// private async Task GetUserAsync(OrderAddRequest request) { //尝试匹配userId var s = long.TryParse(request.UserUniqueNo, out var userId); GainfoEntity gainfo; if (request.Source == BalanceRecordSourceEnumType.QrCode) { if (request.UserUniqueNo.StartsWith("01")) { gainfo = await GetUserByQrCodeAsync(request.UserUniqueNo); await CheckPermissionAsync(gainfo.Code, request.DeviceUniqueNo); } else if (request.UserUniqueNo.StartsWith("02")) { request.Source = BalanceRecordSourceEnumType.EMealVoucher; var eMealVoucherQrCode = await _dbContext.Queryable().Where(x => x.QrCode == request.UserUniqueNo).SingleAsync(); eMealVoucherQrCode.ThrowIfNull("无法识别"); var code = eMealVoucherQrCode.OriginData.Split(':')[2]; gainfo = await _dbContext.Queryable().Where(x => x.Code == code).SingleAsync(); } else { throw new UserFriendlyException($"无法识别{request.UserUniqueNo}"); } } else { //根据档案号 gainfo = await _dbContext.Queryable() .WhereIF(request.Source == BalanceRecordSourceEnumType.Card, x => x.CardNo == request.UserUniqueNo) .WhereIF(request.Source != BalanceRecordSourceEnumType.Card && !request.IsUserId, x => x.Code == request.UserUniqueNo) .WhereIF(request.Source != BalanceRecordSourceEnumType.Card && request.IsUserId && s, x => x.Id == userId) .SingleAsync(); } //在线 if (!request.IsOffline) { gainfo.ThrowIfNull("无法识别"); } else { //兼容hpt尚未删除人员后产生的消费数据 gainfo ??= await _dbContext.Queryable().AS("GainfoLeave") .WhereIF(request.Source == BalanceRecordSourceEnumType.Card, x => x.CardNo == request.UserUniqueNo) .WhereIF(request.Source != BalanceRecordSourceEnumType.Card && !request.IsUserId, x => x.Code == request.UserUniqueNo) .WhereIF(request.Source != BalanceRecordSourceEnumType.Card && request.IsUserId && s, x => x.IdentityUserId == userId) .FirstAsync(); } return gainfo; } /// /// 验证参数 /// /// /// /// private async Task Verify(CanteenOrderEntity order) { var beginOfDay = DateTime.Today; var endOfDay = beginOfDay.AddDays(1).AddSeconds(-1); //今天所有消费记录 var todayAllBalanceRecords = await _balanceRepository.GetBalanceRecordsAsync(order.UserCode, order.TenantId, beginOfDay, endOfDay); //超市营业点 var supermarketCodes = await _dbContext.Queryable() .Where(x => x.Catalog == CanteenBusinessPlaceCatalogEnumType.Supermarket) .Select(x => x.Code) .ToListAsync(); //一天消费金额 var dayCostMax = await _canteenSettingService.GetSettingAsync(CanteenSettingItemEnumType.DayCostMax, order.TenantId, order.UserNo); if (dayCostMax != default) { //消费记录一定会有营业点代码!.Value是安全用法 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 _canteenSettingService.GetSettingAsync(CanteenSettingItemEnumType.OnceCostMax, order.TenantId, order.UserNo); if (onceCostMax != default) { (Math.Abs(order.Amount) > onceCostMax).ThrowIfTrue($"单次消费金额不能超出{onceCostMax}元"); } //超市消费限额 if (order.BusinessPlaceCode.HasValue && supermarketCodes.Contains(order.BusinessPlaceCode.Value)) { var dayMarketCostMax = await _canteenSettingService.GetSettingAsync(CanteenSettingItemEnumType.DayMarketCostMax, order.TenantId, order.UserNo); if (dayMarketCostMax != default) { 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 _canteenSettingService.GetSettingAsync(CanteenSettingItemEnumType.AllowRepeatInOneShift, order.TenantId, order.UserNo, true); if (!allowRepeatInOneShift) { var costCount = todayAllBalanceRecords.Count(x => x.ShiftType == order.ShiftType); (costCount > 1).ThrowIfTrue($"{order.ShiftType?.GetDescription()}不允许多次消费"); } } /// /// 离线消费处理 /// /// /// private async Task OfflineHandleAsync(ConsumeRequest request, GainfoEntity gainfo) { var bindingInfo = await _dbContext.Queryable() .LeftJoin((x, b) => x.BusinessPlaceCode == b.Code) .LeftJoin((x, b, bs) => bs.PlaceCode == b.Code) .Where(x => x.DeviceUniqueNo == request.DeviceUniqueNo && x.BusinessPlaceCode.HasValue) .Select((x, b, bs) => new { b.Code, bs.ShiftCode, Factory = x.TenantId }) .SingleAsync(); //获取当前消费餐次 var shifts = await _dbContext.Queryable() .Where(x => x.ShiftCode == bindingInfo.ShiftCode).ToListAsync(); var shift = shifts.FirstOrDefault(x => new TimePeriod { Start = x.Start, End = x.End }.IsPresent(request.CostDateTime) ); var costDate = await _canteenSettingService.GetCostDateAsync(bindingInfo.Factory, request.CostDateTime); var order = new CanteenOrderEntity { RequestJson = request.ToJson(), No = SnowFlakeHelper.Instance.NextId().ToString(), ThirdNo = request.ThirdNo, IsOffline = request.IsOffline, Type = BalanceRecordTypeEnumType.Out, ApplicationType = CanteenApplicationTypeEnumType.Device, Name = gainfo.Name, Source = request.Source, UserNo = gainfo.No, UserCode = gainfo.Code, Amount = request.Amount, BusinessPlaceCode = bindingInfo.Code, BusinessShiftCode = shift?.ShiftCode, ShiftType = shift?.ShiftType, CostDate = costDate.Date, CostId = gainfo.CostId, DepartmentNo = gainfo.DepartmentNo, DeviceUniqueNo = request.DeviceUniqueNo, IsCostCrossTenant = bindingInfo.Factory != gainfo.Factory, OriginTenantId = gainfo.Factory, Werks = gainfo.Werks, Status = BalanceRecordStatusEnumType.Valid, OrderStatus = CanteenOrderStatusEnumType.Success, CostDateTime = request.CostDateTime, //标识当前消费的基地 TenantId = bindingInfo.Factory, }; await _dbContext.InsertAsync(order); var balanceRecord = order.Adapt(); await _dbContext.InsertAsync(balanceRecord); await _balanceRepository.CalculateBalanceAsync(balanceRecord.UserCode); } /// /// 消费 /// /// /// /// private async Task ConsumeAsync(ConsumeRequest request, GainfoEntity gainfo) { var checkData = await PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime); var order = await _orderRepository.CreateDeviceConsumeOrderAsync(checkData, request, gainfo); if (request.Image.IsPresent()) { await _dbContext.InsertAsync(new ImageRecordEntity { No = order.No, TableName = "B", Image = request.Image }); } await TryConsumeAsync(order.Id, async () => { //校验 await Verify(order); await _balanceRepository.ConsumeBalanceAsync(order.UserCode, order.Amount); //写入记录 var balanceRecord = order.Adapt(); await _dbContext.InsertAsync(balanceRecord); }); } private async Task> EMealVoucherConsumeAsync(OrderAddRequest request, GainfoEntity gainfo) { var msg = string.Empty; var originData = await _dbContext.Queryable() .Where(x => x.QrCode == request.UserUniqueNo) .Select(x => x.OriginData) .SingleAsync(); if (originData.IsEmpty()) { msg = "无法识别"; return new DefaultApiResult { Code = ApiStateCode.Error, Data = null, Msg = msg }; } var originDataItems = originData.Split(":"); var no = originDataItems[0]; var idNo = originDataItems[3]; try { await EMealVoucherConsumeAsync(request.Adapt(), gainfo, originData, no, idNo); } catch (Exception ex) { msg = ex.Message; } //获取餐券余额 var data = await _dbContext.Queryable() .LeftJoin((x, d) => x.No == d.No) .Where((x, d) => x.No == no && d.IdNo == idNo) .Select((x, d) => new OrderResultResponse { UserCode = d.IdNo, UserName = d.Name, CurrentBalance = d.Balance }) .SingleAsync(); try { var v = data.UserCode ?? string.Empty; var vc = v.ToCharArray().Reverse().ToList(); for (int i = 0; i < vc.Count; i++) { if (i + 1 > 4) { vc[i] = '*'; } } vc.Reverse(); data.UserCode = string.Join("", vc); } catch (Exception) { //ignore } return new DefaultApiResult { Code = msg.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success, Data = data, Msg = msg }; } private async Task OfflineConsumeAsync(OrderAddRequest request, GainfoEntity gainfo) { try { if (request.ThirdNo.IsPresent() && await ToCheckAbleOfflineConsumeAsync(request.ThirdNo)) { await using var handler = await _redisCache.LockAsync($"consume:device-offline:{request.ThirdNo}"); if (handler.Acquired) { await OfflineHandleAsync(request.Adapt(), gainfo); } } else { await OfflineHandleAsync(request.Adapt(), gainfo); } } catch (Exception ex) { _logger.LogError(ex, "离线入库错误: 订单号:{no} .{message}", request.ThirdNo, ex.Message); } } /// /// 分支处理在线/离线消费 /// /// /// /// private async Task> ToDoAsync(OrderAddRequest request, GainfoEntity gainfo) { var message = string.Empty; if (request.Source == BalanceRecordSourceEnumType.EMealVoucher) return await EMealVoucherConsumeAsync(request, gainfo); if (request.IsOffline) await OfflineConsumeAsync(request, gainfo); else { try { await ConsumeAsync(request.Adapt(), gainfo); } catch (Exception ex) { message = ex.Message; } } var balance = await _balanceRepository.GetBalanceAsync(gainfo.Code); var data = await _dbContext.Queryable() .LeftJoin((x, department) => x.DepartmentNo == department.No) .Where(x => x.Code == gainfo.Code) .Select((x, department) => new OrderResultResponse { UserCode = x.Code, UserName = x.Name, DepartmentNo = x.DepartmentNo, DepartmentName = department.Name, CurrentBalance = balance, CurrentTotalBalance = balance }) .SingleAsync(); return new DefaultApiResult { Code = message.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success, Data = data, Msg = message }; } /// /// 根据二维码获取消费用户 /// /// /// private async Task GetUserByQrCodeAsync(string qrCode) { var gainfoQrCode = await _dbContext.Queryable().Where(x => x.QrCode == qrCode).SingleAsync(); gainfoQrCode.ThrowIfNull("消费码已过期"); (gainfoQrCode.ModifiedTime.AddMinutes(60) < DateTime.Now).ThrowIfTrue("消费码已过期"); return await _dbContext.Queryable().Where(x => x.Code == gainfoQrCode.UserCode).SingleAsync(); } /// /// 电子餐券消费 /// /// /// /// /// /// private async Task EMealVoucherConsumeAsync(ConsumeRequest request, GainfoEntity gainfo, string originData, string no, string idNo) { var now = DateTime.Now; var current = await _dbContext.Queryable() .Where(x => x.No == no && x.IdNo == idNo) .Select(x => new { x.Name, x.Start, x.End }) .SingleAsync(); (now >= current.Start && now <= current.End).ThrowIfFalse("餐券已过期"); var eMealVoucher = await _dbContext.Queryable().Where(x => x.No == no).SingleAsync(); var checkData = await PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime); var isSupermarket = await _dbContext.Queryable().Where(x => x.Code == checkData.BusinessPlaceCode) .Select(x => x.Catalog == CanteenBusinessPlaceCatalogEnumType.Supermarket) .SingleAsync(); isSupermarket.ThrowIfTrue("禁止在超市消费"); eMealVoucher.Factories.Contains(checkData.Factory).ThrowIfFalse("禁止在当前厂区消费"); gainfo.No = originData; var order = await _orderRepository.CreateDeviceConsumeOrderAsync(checkData, request, gainfo); await TryConsumeAsync(order.Id, async () => { await CommonHelper.RetryAsync(async () => { var detail = await _dbContext.Queryable().Where(x => x.No == no && x.IdNo == idNo).SingleAsync(); //金额+- detail.Balance += order.Amount; (detail.Balance < 0).ThrowIfTrue("余额不足,无法消费"); await _dbContext.Updateable() .SetColumns(x => x.Balance == detail.Balance) .SetColumns(x => x.Version == x.Version + 1) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.No == no && x.IdNo == idNo && x.Version == detail.Version) //必须与查询时的version一致 .ExecuteCommandThrowVersionAsync(); //写入记录 var balanceRecord = order.Adapt(); balanceRecord.Remark = $"({no})"+current.Name; await _dbContext.InsertAsync(balanceRecord); }, 5); }); } /// /// 消费前预检 /// /// /// /// private async Task PreCheckAsync(string deviceUniqueNo, DateTime costDateTime) { var device = await _dbContext.Queryable().Where(x => x.UniqueNo == deviceUniqueNo).SingleAsync(); device.ThrowIfNull("设备尚未登记"); device.Enable.ThrowIfFalse("设备已被停用"); (device.Type != DeviceTypeEnumType.Meal).ThrowIfTrue("设备类型有误"); var bindingInfo = await _dbContext.Queryable() .LeftJoin((x, b) => x.BusinessPlaceCode == b.Code) .LeftJoin((x, b, bs) => bs.PlaceCode == b.Code) .Where(x => x.DeviceUniqueNo == deviceUniqueNo && x.BusinessPlaceCode.HasValue) .Select((x, b, bs) => new { b.Code, bs.ShiftCode, Factory = x.TenantId }) .SingleAsync(); bindingInfo.ThrowIfNull("设备尚未登记营业点"); //获取当前消费餐次 var shifts = await _dbContext.Queryable() .Where(x => x.ShiftCode == bindingInfo.ShiftCode) .Select(x => new { x.ShiftType, x.Start, x.End }) .ToListAsync(); shifts.ThrowIfEmpty("营业点餐次尚未设置"); var shift = shifts.FirstOrDefault(x => new TimePeriod { Start = x.Start, End = x.End }.IsPresent(costDateTime)); shift.ThrowIfNull("非就餐时间"); var costDate = await _canteenSettingService.GetCostDateAsync(bindingInfo.Factory, costDateTime); return new OrderPreCheckDto { Factory = bindingInfo.Factory, CostDate = costDate, ShiftType = shift!.ShiftType, BusinessPlaceCode = bindingInfo.Code, BusinessShiftCode = bindingInfo.ShiftCode }; } /// /// 设置设备ip /// /// private async Task SetDeviceIpInfoAsync(string deviceNo) { try { if (_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue("X-Requested-DeviceIp", out var ip)) { var device = await _dbContext.Queryable().Where(x => x.UniqueNo == deviceNo).SingleAsync(); device.ConnectionInfo = new { ip = ip.ToString() }.ToJson(); await _dbContext.UpdateAsync(device); } _logger.LogInformation("设备ip获取:{deviceNo} {ip}", deviceNo, ip); } catch (Exception) { //ignore } } /// /// 检查是否能够进行离线消费,处理hpt重复提交 /// /// /// private async Task ToCheckAbleOfflineConsumeAsync(string thirdNo) { var orders = await _dbContext.Queryable() .Where(x => x.ThirdNo == thirdNo) .OrderBy(x => x.CreateTime, OrderByType.Desc) .ToListAsync(); if (orders.IsEmpty()) return true; var order = orders.FirstOrDefault(); //存在在线order数据,但是状态未成功 return !order.IsOffline && order.OrderStatus != CanteenOrderStatusEnumType.Success; } private async Task TryConsumeAsync(long orderId, Func action) { try { await action(); await _dbContext.Updateable() .SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Success) .SetColumns(x => x.ModifiedTime == DateTime.Now) .Where(x => x.Id == orderId) .ExecuteCommandAsync(); } catch (Exception ex) { await _dbContext.Updateable() .SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Failure) .SetColumns(x => x.ModifiedTime == DateTime.Now) .SetColumns(x => x.Result == ex.Message) .Where(x => x.Id == orderId) .ExecuteCommandAsync(); throw; } } }