feat 优化消费过程

This commit is contained in:
2025-02-13 17:37:47 +08:00
parent f7b5f7169d
commit a06dcbef61
13 changed files with 579 additions and 304 deletions

View File

@@ -0,0 +1,31 @@
using Dlhis.Entity.Canteen.Enums;
namespace Dlhis.Canteen.Order.Dto;
public class OrderCheckResultDto
{
/// <summary>
/// 消费厂区
/// </summary>
public string Factory { get; set; }
/// <summary>
/// 消费日期
/// </summary>
public DateTime CostDate { get; set; }
/// <summary>
/// 消费餐次
/// </summary>
public ShiftEnumType ShiftType { get; set; }
/// <summary>
/// 消费营业点
/// </summary>
public Guid BusinessPlaceCode { get; set; }
/// <summary>
/// 消费餐别
/// </summary>
public Guid BusinessShiftCode { get; set; }
}

View File

@@ -1,16 +0,0 @@
using Dlhis.Entity.Canteen.Enums;
namespace Dlhis.Canteen.Order.Dto;
public class OrderPreCheckDto
{
public string Factory { get; set; }
public DateTime CostDate { get; set; }
public ShiftEnumType ShiftType { get; set; }
public Guid BusinessPlaceCode { get; set; }
public Guid BusinessShiftCode { get; set; }
}

View File

@@ -7,5 +7,5 @@ namespace Dlhis.Canteen.Order.Repository;
public interface IOrderRepository : IScopeDependency public interface IOrderRepository : IScopeDependency
{ {
Task<CanteenOrderEntity> CreateDeviceConsumeOrderAsync(OrderPreCheckDto checkData, ConsumeRequest request, GainfoEntity gainfo); Task<CanteenOrderEntity> CreateDeviceConsumeOrderAsync(OrderCheckResultDto checkData, ConsumeRequest request, GainfoEntity gainfo);
} }

View File

@@ -19,7 +19,7 @@ public class OrderRepository : IOrderRepository
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<CanteenOrderEntity> CreateDeviceConsumeOrderAsync(OrderPreCheckDto checkData, ConsumeRequest request, GainfoEntity gainfo) public async Task<CanteenOrderEntity> CreateDeviceConsumeOrderAsync(OrderCheckResultDto checkData, ConsumeRequest request, GainfoEntity gainfo)
{ {
var order = new CanteenOrderEntity var order = new CanteenOrderEntity
{ {

View File

@@ -0,0 +1,110 @@
using Dlhis.Canteen.Order.Repository;
using Dlhis.Canteen.Order.Request;
using Dlhis.Canteen.Order.Response;
using Dlhis.Entity.Basic.Entity;
using Dlhis.Entity.Canteen.Entities;
using Dlhis.Entity.Canteen.Enums;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.UnifyResultAndValidation;
using Kurisu.AspNetCore.Utils;
using Kurisu.AspNetCore.Utils.Extensions;
using Mapster;
namespace Dlhis.Canteen.Order.Services.ConsumeHandlers;
/// <summary>
/// 生日券消费处理
/// </summary>
[Service(nameof(BalanceRecordSourceEnumType.Birthday))]
public class BirthdayConsumeHandler : IConsumeHandler
{
private readonly IDbContext _dbContext;
private readonly IOrderCommonHandler _orderCommonHandler;
private readonly IOrderRepository _orderRepository;
public BirthdayConsumeHandler(IDbContext dbContext,
IOrderCommonHandler orderCommonHandler,
IOrderRepository orderRepository)
{
_dbContext = dbContext;
_orderCommonHandler = orderCommonHandler;
_orderRepository = orderRepository;
}
public async Task<DefaultApiResult<OrderResultResponse>> ConsumeAsync(OrderAddRequest request, GainfoEntity gainfo)
{
var msg = string.Empty;
var ticketNo = request.UserUniqueNo;
var exists = await _dbContext.Queryable<TicketEntity>()
.Where(x => x.Type == TicketEnumType.Birthday)
.Where(x => x.QrCode == ticketNo)
.AnyAsync();
exists.ThrowIfFalse("生日券无效");
try
{
var checkData = await _orderCommonHandler.PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime!.Value);
var isSupermarket = await _dbContext.Queryable<BusinessPlaceEntity>()
.Where(x => x.Code == checkData.BusinessPlaceCode)
.Select(x => x.Catalog == CanteenBusinessPlaceCatalogEnumType.Supermarket)
.SingleAsync();
isSupermarket.ThrowIfFalse("请在超市消费");
var order = await _orderRepository.CreateDeviceConsumeOrderAsync(checkData, request.Adapt<ConsumeRequest>(), gainfo);
await _orderCommonHandler.TryConsumeAsync(order.Id, async () =>
{
await CommonHelper.RetryAsync(async () =>
{
var ticket = await _dbContext.Queryable<TicketEntity>()
.Where(x => x.Type == TicketEnumType.Birthday)
.Where(x => x.QrCode == ticketNo)
.SingleAsync();
//金额+-
ticket.Balance += order.Amount;
(ticket.Balance < 0).ThrowIfTrue("余额不足,无法消费");
await _dbContext.Updateable<TicketEntity>()
.SetColumns(x => x.Balance == ticket.Balance)
.SetColumns(x => x.Version == x.Version + 1)
.SetColumns(x => x.ModifiedTime == DateTime.Now)
.Where(x => x.QrCode == ticketNo && x.Version == ticket.Version) //必须与查询时的version一致
.ExecuteCommandThrowVersionAsync();
//写入记录
var balanceRecord = order.Adapt<BalanceRecordEntity>();
balanceRecord.Remark = $"{ticket.QrCode}(生日券消费)";
await _dbContext.InsertAsync(balanceRecord);
}, 5);
});
}
catch (Exception ex)
{
msg = ex.Message;
}
//获取餐券余额
var data = await _dbContext.Queryable<TicketEntity>()
.LeftJoin<GainfoEntity>((x, ga) => x.UserCode == ga.Code)
.Where(x => x.Type == TicketEnumType.Birthday && x.QrCode == ticketNo)
.Select((x, ga) => new OrderResultResponse
{
UserCode = x.UserCode,
UserName = ga.Name,
CurrentBalance = x.Balance
})
.SingleAsync();
return new DefaultApiResult<OrderResultResponse>
{
Code = msg.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success,
Data = data,
Msg = msg
};
}
}

View File

@@ -0,0 +1,166 @@
using Dlhis.Canteen.Order.Repository;
using Dlhis.Canteen.Order.Request;
using Dlhis.Canteen.Order.Response;
using Dlhis.Entity.Basic.Entity;
using Dlhis.Entity.Canteen.Entities;
using Dlhis.Entity.Canteen.Enums;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.UnifyResultAndValidation;
using Kurisu.AspNetCore.Utils;
using Kurisu.AspNetCore.Utils.Extensions;
using Mapster;
namespace Dlhis.Canteen.Order.Services.ConsumeHandlers;
/// <summary>
/// 电子餐券处理
/// </summary>
[Service(nameof(BalanceRecordSourceEnumType.EMealVoucher))]
public class EMealVoucherHandler : IConsumeHandler
{
private readonly IDbContext _dbContext;
private readonly IOrderRepository _orderRepository;
private readonly IOrderCommonHandler _orderCommonHandler;
public EMealVoucherHandler(IDbContext dbContext,
IOrderRepository orderRepository,
IOrderCommonHandler orderCommonHandler)
{
_dbContext = dbContext;
_orderRepository = orderRepository;
_orderCommonHandler = orderCommonHandler;
}
public async Task<DefaultApiResult<OrderResultResponse>> ConsumeAsync(OrderAddRequest request, GainfoEntity gainfo)
{
var msg = string.Empty;
var originData = await _dbContext.Queryable<EMealVoucherDetailQrCodeEntity>()
.Where(x => x.QrCode == request.UserUniqueNo)
.Select(x => x.OriginData)
.SingleAsync();
if (originData.IsEmpty())
{
msg = "无法识别";
return new DefaultApiResult<OrderResultResponse>
{
Code = ApiStateCode.Error,
Data = null,
Msg = msg
};
}
var originDataItems = originData.Split(":");
var no = originDataItems[0];
var idNo = originDataItems[3];
try
{
await EMealVoucherConsumeAsync(request.Adapt<ConsumeRequest>(), gainfo, originData, no, idNo);
}
catch (Exception ex)
{
msg = ex.Message;
}
//获取餐券余额
var data = await _dbContext.Queryable<EMealVoucherEntity>()
.LeftJoin<EMealVoucherDetailEntity>((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<OrderResultResponse>
{
Code = msg.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success,
Data = data,
Msg = msg
};
}
/// <summary>
/// 电子餐券消费
/// </summary>
/// <param name="request"></param>
/// <param name="gainfo"></param>
/// <param name="originData"></param>
/// <param name="no"></param>
/// <param name="idNo"></param>
private async Task EMealVoucherConsumeAsync(ConsumeRequest request, GainfoEntity gainfo, string originData, string no, string idNo)
{
var now = DateTime.Now;
var current = await _dbContext.Queryable<EMealVoucherDetailEntity>()
.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<EMealVoucherEntity>().Where(x => x.No == no).SingleAsync();
var checkData = await _orderCommonHandler.PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime);
var isSupermarket = await _dbContext.Queryable<BusinessPlaceEntity>()
.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 _orderCommonHandler.TryConsumeAsync(order.Id, async () =>
{
await CommonHelper.RetryAsync(async () =>
{
var detail = await _dbContext.Queryable<EMealVoucherDetailEntity>().Where(x => x.No == no && x.IdNo == idNo).SingleAsync();
//金额+-
detail.Balance += order.Amount;
(detail.Balance < 0).ThrowIfTrue("余额不足,无法消费");
await _dbContext.Updateable<EMealVoucherDetailEntity>()
.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<BalanceRecordEntity>();
balanceRecord.Remark = $"({no})" + current.Name;
await _dbContext.InsertAsync(balanceRecord);
}, 5);
});
}
}

View File

@@ -0,0 +1,144 @@
using Dlhis.Canteen.Order.Dto;
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 Dlhis.Entity.Device.Enums;
using Kurisu.AspNetCore.CustomClass;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.Utils.Extensions;
namespace Dlhis.Canteen.Order.Services.ConsumeHandlers;
/// <summary>
/// 订单通用处理
/// </summary>
public class OrderCommonHandler : IOrderCommonHandler
{
private readonly IDbContext _dbContext;
private readonly ICanteenSettingService _canteenSettingService;
private readonly ILogger<OrderCommonHandler> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
public OrderCommonHandler(IDbContext dbContext,
IHttpContextAccessor httpContextAccessor,
ICanteenSettingService canteenSettingService,
ILogger<OrderCommonHandler> logger)
{
_dbContext = dbContext;
_canteenSettingService = canteenSettingService;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
/// <summary>
/// 设置设备ip
/// </summary>
/// <param name="deviceNo"></param>
public async Task SetDeviceIpInfoAsync(string deviceNo)
{
try
{
if (_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue("X-Requested-DeviceIp", out var ip))
{
var device = await _dbContext.Queryable<DeviceEntity>().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
}
}
/// <summary>
/// 消费前预检
/// </summary>
/// <param name="deviceUniqueNo">设备号</param>
/// <param name="costDateTime">消费时间</param>
/// <returns></returns>
public async Task<OrderCheckResultDto> PreCheckAsync(string deviceUniqueNo, DateTime costDateTime)
{
var device = await _dbContext.Queryable<DeviceEntity>().Where(x => x.UniqueNo == deviceUniqueNo).SingleAsync();
device.ThrowIfNull("设备尚未登记");
device.Enable.ThrowIfFalse("设备已被停用");
(device.Type != DeviceTypeEnumType.Meal).ThrowIfTrue("设备类型有误");
var bindingInfo = await _dbContext.Queryable<DeviceRegistryEntity>()
.LeftJoin<BusinessPlaceEntity>((x, b) => x.BusinessPlaceCode == b.Code)
.LeftJoin<BusinessPlaceShiftRelationshipEntity>((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<BusinessShiftMealRelationshipEntity>()
.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 OrderCheckResultDto
{
Factory = bindingInfo.Factory,
CostDate = costDate,
ShiftType = shift!.ShiftType,
BusinessPlaceCode = bindingInfo.Code,
BusinessShiftCode = bindingInfo.ShiftCode
};
}
/// <summary>
/// 消费更新处理
/// </summary>
/// <param name="orderId"></param>
/// <param name="action"></param>
public async Task TryConsumeAsync(long orderId, Func<Task> action)
{
try
{
await action();
await _dbContext.Updateable<CanteenOrderEntity>()
.SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Success)
.SetColumns(x => x.ModifiedTime == DateTime.Now)
.Where(x => x.Id == orderId)
.ExecuteCommandAsync();
}
catch (Exception ex)
{
await _dbContext.Updateable<CanteenOrderEntity>()
.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;
}
}
}

View File

@@ -0,0 +1,20 @@
using Dlhis.Canteen.Order.Request;
using Dlhis.Canteen.Order.Response;
using Dlhis.Entity.Basic.Entity;
using Kurisu.AspNetCore.UnifyResultAndValidation;
namespace Dlhis.Canteen.Order.Services;
/// <summary>
/// 消费处理器
/// </summary>
public interface IConsumeHandler : IScopeDependency
{
/// <summary>
/// 消费
/// </summary>
/// <param name="request"></param>
/// <param name="gainfo"></param>
/// <returns></returns>
Task<DefaultApiResult<OrderResultResponse>> ConsumeAsync(OrderAddRequest request, GainfoEntity gainfo);
}

View File

@@ -0,0 +1,29 @@
using Dlhis.Canteen.Order.Dto;
namespace Dlhis.Canteen.Order.Services;
public interface IOrderCommonHandler : IScopeDependency
{
/// <summary>
/// 消费更新处理
/// </summary>
/// <param name="orderId"></param>
/// <param name="action"></param>
/// <returns></returns>
Task TryConsumeAsync(long orderId, Func<Task> action);
/// <summary>
/// 设置设备ip
/// </summary>
/// <param name="deviceNo"></param>
/// <returns></returns>
Task SetDeviceIpInfoAsync(string deviceNo);
/// <summary>
/// 消费前预检
/// </summary>
/// <param name="deviceUniqueNo">设备号</param>
/// <param name="costDateTime">消费时间</param>
/// <returns></returns>
Task<OrderCheckResultDto> PreCheckAsync(string deviceUniqueNo, DateTime costDateTime);
}

View File

@@ -1,19 +1,14 @@
using Dlhis.Basic.Api; using Dlhis.Basic.Api;
using Dlhis.Canteen.Order.Dto;
using Dlhis.Canteen.Order.Repository; using Dlhis.Canteen.Order.Repository;
using Dlhis.Canteen.Order.Request;
using Dlhis.Canteen.Order.Response;
using Dlhis.Canteen.Setting.Services; using Dlhis.Canteen.Setting.Services;
using Dlhis.Constants;
using Dlhis.Entity.Basic.Entity; using Dlhis.Entity.Basic.Entity;
using Dlhis.Entity.Canteen.Entities; using Dlhis.Entity.Canteen.Entities;
using Dlhis.Entity.Canteen.Enums; 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;
using Dlhis.Entity.Device.Enums; using Dlhis.Entity.Device.Entities;
using Kurisu.AspNetCore.Authentication.Abstractions; using Kurisu.AspNetCore.Authentication.Abstractions;
using Kurisu.AspNetCore.Cache; using Kurisu.AspNetCore.Cache;
using Kurisu.AspNetCore.CustomClass; using Kurisu.AspNetCore.CustomClass;
@@ -21,7 +16,10 @@ using Kurisu.AspNetCore.DataAccess.Dto;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions; using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
using Kurisu.AspNetCore.DataAccess.SqlSugar.Services; using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
using Kurisu.AspNetCore.UnifyResultAndValidation; using Kurisu.AspNetCore.UnifyResultAndValidation;
using Dlhis.Constants; using Kurisu.AspNetCore.Utils;
using Kurisu.AspNetCore.Utils.Extensions;
using Mapster;
using SqlSugar;
namespace Dlhis.Canteen.Order.Services.Implements; namespace Dlhis.Canteen.Order.Services.Implements;
@@ -30,31 +28,34 @@ public class OrderService : IOrderService
private readonly ICanteenSettingService _canteenSettingService; private readonly ICanteenSettingService _canteenSettingService;
private readonly IDbContext _dbContext; private readonly IDbContext _dbContext;
private readonly ICurrentUser _currentUser; private readonly ICurrentUser _currentUser;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IBalanceRepository _balanceRepository; private readonly IBalanceRepository _balanceRepository;
private readonly IOrderRepository _orderRepository; private readonly IOrderRepository _orderRepository;
private readonly RedisCache _redisCache; private readonly RedisCache _redisCache;
private readonly IGainfoPermissionApi _gainfoPermissionApi; private readonly IGainfoPermissionApi _gainfoPermissionApi;
private readonly IOrderCommonHandler _orderCommonHandler;
private readonly INamedResolver _namedResolver;
private readonly ILogger<OrderService> _logger; private readonly ILogger<OrderService> _logger;
public OrderService(ICanteenSettingService canteenSettingService, public OrderService(ICanteenSettingService canteenSettingService,
IDbContext dbContext, IDbContext dbContext,
ICurrentUser currentUser, ICurrentUser currentUser,
IHttpContextAccessor httpContextAccessor,
IBalanceRepository balanceRepository, IBalanceRepository balanceRepository,
IOrderRepository orderRepository, IOrderRepository orderRepository,
RedisCache redisCache, RedisCache redisCache,
IGainfoPermissionApi gainfoPermissionApi, IGainfoPermissionApi gainfoPermissionApi,
IOrderCommonHandler orderCommonHandler,
INamedResolver namedResolver,
ILogger<OrderService> logger) ILogger<OrderService> logger)
{ {
_canteenSettingService = canteenSettingService; _canteenSettingService = canteenSettingService;
_dbContext = dbContext; _dbContext = dbContext;
_currentUser = currentUser; _currentUser = currentUser;
_httpContextAccessor = httpContextAccessor;
_balanceRepository = balanceRepository; _balanceRepository = balanceRepository;
_orderRepository = orderRepository; _orderRepository = orderRepository;
_redisCache = redisCache; _redisCache = redisCache;
_gainfoPermissionApi = gainfoPermissionApi; _gainfoPermissionApi = gainfoPermissionApi;
_orderCommonHandler = orderCommonHandler;
_namedResolver = namedResolver;
_logger = logger; _logger = logger;
} }
@@ -75,7 +76,7 @@ public class OrderService : IOrderService
/// <returns></returns> /// <returns></returns>
public async Task<DefaultApiResult<OrderResultResponse>> AddAsync(OrderAddRequest request) public async Task<DefaultApiResult<OrderResultResponse>> AddAsync(OrderAddRequest request)
{ {
await SetDeviceIpInfoAsync(request.DeviceUniqueNo); await _orderCommonHandler.SetDeviceIpInfoAsync(request.DeviceUniqueNo);
await using var handler = await _redisCache.LockAsync($"consume:device:{request.UserUniqueNo}"); await using var handler = await _redisCache.LockAsync($"consume:device:{request.UserUniqueNo}");
handler.Acquired.ThrowIfFalse("请勿频繁消费"); handler.Acquired.ThrowIfFalse("请勿频繁消费");
@@ -152,39 +153,47 @@ public class OrderService : IOrderService
var s = long.TryParse(request.UserUniqueNo, out var userId); var s = long.TryParse(request.UserUniqueNo, out var userId);
GainfoEntity gainfo; GainfoEntity gainfo;
//条码消费
if (request.Source == BalanceRecordSourceEnumType.QrCode) if (request.Source == BalanceRecordSourceEnumType.QrCode)
{ {
if (request.UserUniqueNo.StartsWith(QrCodeConstants.User)) var flag = request.UserUniqueNo.Substring(0, 2);
switch (flag)
{ {
gainfo = await GetUserByQrCodeAsync(request.UserUniqueNo); case QrCodeConstants.User:
await CheckPermissionAsync(gainfo.Code, request.DeviceUniqueNo); {
} gainfo = await GetUserByQrCodeAsync(request.UserUniqueNo);
else if (request.UserUniqueNo.StartsWith(QrCodeConstants.EMealVoucher)) await CheckPermissionAsync(gainfo.Code, request.DeviceUniqueNo);
{ }
request.Source = BalanceRecordSourceEnumType.EMealVoucher; break;
var eMealVoucherQrCode = await _dbContext.Queryable<EMealVoucherDetailQrCodeEntity>().Where(x => x.QrCode == request.UserUniqueNo).SingleAsync(); case QrCodeConstants.EMealVoucher:
eMealVoucherQrCode.ThrowIfNull("无法识别"); {
var code = eMealVoucherQrCode.OriginData.Split(':')[2]; request.Source = BalanceRecordSourceEnumType.EMealVoucher;
gainfo = await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == code).SingleAsync(); var eMealVoucherQrCode = await _dbContext.Queryable<EMealVoucherDetailQrCodeEntity>().Where(x => x.QrCode == request.UserUniqueNo).SingleAsync();
} eMealVoucherQrCode.ThrowIfNull("无法识别");
else if (request.UserUniqueNo.StartsWith(QrCodeConstants.Birthday)) var code = eMealVoucherQrCode.OriginData.Split(':')[2];
{ gainfo = await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == code).SingleAsync();
var ticket = await _dbContext.Queryable<TicketEntity>().Where(x => x.Type == TicketEnumType.Birthday).Where(x => x.QrCode == request.UserUniqueNo).SingleAsync(); }
ticket.ThrowIfNull("无法识别"); break;
gainfo = await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == ticket.UserCode).SingleAsync(); case QrCodeConstants.Birthday:
} {
else request.Source = BalanceRecordSourceEnumType.Birthday;
{ var ticket = await _dbContext.Queryable<TicketEntity>().Where(x => x.Type == TicketEnumType.Birthday).Where(x => x.QrCode == request.UserUniqueNo).SingleAsync();
throw new UserFriendlyException($"无法识别{request.UserUniqueNo}"); ticket.ThrowIfNull("无法识别");
gainfo = await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == ticket.UserCode).SingleAsync();
}
break;
default: throw new UserFriendlyException($"无法识别{request.UserUniqueNo}");
} }
} }
else else
{ {
//根据档案号 //根据档案号
//区分userId和code: 工号纯数字避免跟userId相同导致查询到的消费人错误
gainfo = await _dbContext.Queryable<GainfoEntity>() gainfo = await _dbContext.Queryable<GainfoEntity>()
.WhereIF(request.Source == BalanceRecordSourceEnumType.Card, x => x.CardNo == request.UserUniqueNo) .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, x => x.Code == request.UserUniqueNo) //工号
.WhereIF(request.Source != BalanceRecordSourceEnumType.Card && request.IsUserId && s, x => x.Id == userId) .WhereIF(request.Source != BalanceRecordSourceEnumType.Card && request.IsUserId && s, x => x.Id == userId) //user id
.SingleAsync(); .SingleAsync();
} }
@@ -230,7 +239,7 @@ public class OrderService : IOrderService
//一天消费金额 //一天消费金额
var dayCostMax = await _canteenSettingService.GetSettingAsync<decimal>(CanteenSettingItemEnumType.DayCostMax, order.TenantId, order.UserNo); var dayCostMax = await _canteenSettingService.GetSettingAsync<decimal>(CanteenSettingItemEnumType.DayCostMax, order.TenantId, order.UserNo);
if (dayCostMax != default) if (dayCostMax != 0)
{ {
//消费记录一定会有营业点代码!.Value是安全用法 //消费记录一定会有营业点代码!.Value是安全用法
var cost = todayAllBalanceRecords var cost = todayAllBalanceRecords
@@ -242,7 +251,7 @@ public class OrderService : IOrderService
//一次消费金额 //一次消费金额
var onceCostMax = await _canteenSettingService.GetSettingAsync<decimal>(CanteenSettingItemEnumType.OnceCostMax, order.TenantId, order.UserNo); var onceCostMax = await _canteenSettingService.GetSettingAsync<decimal>(CanteenSettingItemEnumType.OnceCostMax, order.TenantId, order.UserNo);
if (onceCostMax != default) if (onceCostMax != 0)
{ {
(Math.Abs(order.Amount) > onceCostMax).ThrowIfTrue($"单次消费金额不能超出{onceCostMax}元"); (Math.Abs(order.Amount) > onceCostMax).ThrowIfTrue($"单次消费金额不能超出{onceCostMax}元");
} }
@@ -251,7 +260,7 @@ public class OrderService : IOrderService
if (order.BusinessPlaceCode.HasValue && supermarketCodes.Contains(order.BusinessPlaceCode.Value)) if (order.BusinessPlaceCode.HasValue && supermarketCodes.Contains(order.BusinessPlaceCode.Value))
{ {
var dayMarketCostMax = await _canteenSettingService.GetSettingAsync<decimal>(CanteenSettingItemEnumType.DayMarketCostMax, order.TenantId, order.UserNo); var dayMarketCostMax = await _canteenSettingService.GetSettingAsync<decimal>(CanteenSettingItemEnumType.DayMarketCostMax, order.TenantId, order.UserNo);
if (dayMarketCostMax != default) if (dayMarketCostMax != 0)
{ {
var cost = todayAllBalanceRecords.Where(x => x.BusinessPlaceCode.HasValue) var cost = todayAllBalanceRecords.Where(x => x.BusinessPlaceCode.HasValue)
.Where(x => supermarketCodes.Contains(x.BusinessPlaceCode.Value)) .Where(x => supermarketCodes.Contains(x.BusinessPlaceCode.Value))
@@ -354,7 +363,7 @@ public class OrderService : IOrderService
/// <returns></returns> /// <returns></returns>
private async Task ConsumeAsync(ConsumeRequest request, GainfoEntity gainfo) private async Task ConsumeAsync(ConsumeRequest request, GainfoEntity gainfo)
{ {
var checkData = await PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime); var checkData = await _orderCommonHandler.PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime);
var order = await _orderRepository.CreateDeviceConsumeOrderAsync(checkData, request, gainfo); var order = await _orderRepository.CreateDeviceConsumeOrderAsync(checkData, request, gainfo);
if (request.Image.IsPresent()) if (request.Image.IsPresent())
@@ -367,7 +376,7 @@ public class OrderService : IOrderService
}); });
} }
await TryConsumeAsync(order.Id, async () => await _orderCommonHandler.TryConsumeAsync(order.Id, async () =>
{ {
//校验 //校验
await Verify(order); await Verify(order);
@@ -380,78 +389,11 @@ public class OrderService : IOrderService
}); });
} }
private async Task<DefaultApiResult<OrderResultResponse>> EMealVoucherConsumeAsync(OrderAddRequest request, GainfoEntity gainfo)
{
var msg = string.Empty;
var originData = await _dbContext.Queryable<EMealVoucherDetailQrCodeEntity>()
.Where(x => x.QrCode == request.UserUniqueNo)
.Select(x => x.OriginData)
.SingleAsync();
if (originData.IsEmpty()) // private async Task<DefaultApiResult<OrderResultResponse>> BirthdayConsumeAsync(OrderAddRequest request, GainfoEntity gainfo)
{ // {
msg = "无法识别"; //
return new DefaultApiResult<OrderResultResponse> // }
{
Code = ApiStateCode.Error,
Data = null,
Msg = msg
};
}
var originDataItems = originData.Split(":");
var no = originDataItems[0];
var idNo = originDataItems[3];
try
{
await EMealVoucherConsumeAsync(request.Adapt<ConsumeRequest>(), gainfo, originData, no, idNo);
}
catch (Exception ex)
{
msg = ex.Message;
}
//获取餐券余额
var data = await _dbContext.Queryable<EMealVoucherEntity>()
.LeftJoin<EMealVoucherDetailEntity>((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<OrderResultResponse>
{
Code = msg.IsPresent() ? ApiStateCode.Error : ApiStateCode.Success,
Data = data,
Msg = msg
};
}
private async Task OfflineConsumeAsync(OrderAddRequest request, GainfoEntity gainfo) private async Task OfflineConsumeAsync(OrderAddRequest request, GainfoEntity gainfo)
@@ -490,8 +432,9 @@ public class OrderService : IOrderService
{ {
var message = string.Empty; var message = string.Empty;
if (request.Source == BalanceRecordSourceEnumType.EMealVoucher) var otherTypes = new[] { BalanceRecordSourceEnumType.EMealVoucher, BalanceRecordSourceEnumType.Birthday };
return await EMealVoucherConsumeAsync(request, gainfo); if (otherTypes.Contains(request.Source))
return await _namedResolver.GetService<IConsumeHandler>(request.Source.ToString()).ConsumeAsync(request, gainfo);
if (request.IsOffline) if (request.IsOffline)
await OfflineConsumeAsync(request, gainfo); await OfflineConsumeAsync(request, gainfo);
@@ -547,144 +490,6 @@ public class OrderService : IOrderService
return await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == gainfoQrCode.UserCode).SingleAsync(); return await _dbContext.Queryable<GainfoEntity>().Where(x => x.Code == gainfoQrCode.UserCode).SingleAsync();
} }
/// <summary>
/// 电子餐券消费
/// </summary>
/// <param name="request"></param>
/// <param name="gainfo"></param>
/// <param name="originData"></param>
/// <param name="no"></param>
/// <param name="idNo"></param>
private async Task EMealVoucherConsumeAsync(ConsumeRequest request, GainfoEntity gainfo, string originData, string no, string idNo)
{
var now = DateTime.Now;
var current = await _dbContext.Queryable<EMealVoucherDetailEntity>()
.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<EMealVoucherEntity>().Where(x => x.No == no).SingleAsync();
var checkData = await PreCheckAsync(request.DeviceUniqueNo, request.CostDateTime);
var isSupermarket = await _dbContext.Queryable<BusinessPlaceEntity>().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<EMealVoucherDetailEntity>().Where(x => x.No == no && x.IdNo == idNo).SingleAsync();
//金额+-
detail.Balance += order.Amount;
(detail.Balance < 0).ThrowIfTrue("余额不足,无法消费");
await _dbContext.Updateable<EMealVoucherDetailEntity>()
.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<BalanceRecordEntity>();
balanceRecord.Remark = $"({no})" + current.Name;
await _dbContext.InsertAsync(balanceRecord);
}, 5);
});
}
/// <summary>
/// 消费前预检
/// </summary>
/// <param name="deviceUniqueNo"></param>
/// <param name="costDateTime"></param>
/// <returns></returns>
private async Task<OrderPreCheckDto> PreCheckAsync(string deviceUniqueNo, DateTime costDateTime)
{
var device = await _dbContext.Queryable<DeviceEntity>().Where(x => x.UniqueNo == deviceUniqueNo).SingleAsync();
device.ThrowIfNull("设备尚未登记");
device.Enable.ThrowIfFalse("设备已被停用");
(device.Type != DeviceTypeEnumType.Meal).ThrowIfTrue("设备类型有误");
var bindingInfo = await _dbContext.Queryable<DeviceRegistryEntity>()
.LeftJoin<BusinessPlaceEntity>((x, b) => x.BusinessPlaceCode == b.Code)
.LeftJoin<BusinessPlaceShiftRelationshipEntity>((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<BusinessShiftMealRelationshipEntity>()
.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
};
}
/// <summary>
/// 设置设备ip
/// </summary>
/// <param name="deviceNo"></param>
private async Task SetDeviceIpInfoAsync(string deviceNo)
{
try
{
if (_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue("X-Requested-DeviceIp", out var ip))
{
var device = await _dbContext.Queryable<DeviceEntity>().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
}
}
/// <summary> /// <summary>
/// 检查是否能够进行离线消费,处理hpt重复提交 /// 检查是否能够进行离线消费,处理hpt重复提交
/// </summary> /// </summary>
@@ -700,34 +505,9 @@ public class OrderService : IOrderService
if (orders.IsEmpty()) if (orders.IsEmpty())
return true; return true;
var order = orders.FirstOrDefault(); var order = orders.FirstOrDefault()!;
//存在在线order数据,但是状态未成功 //存在在线order数据,但是状态未成功
return !order.IsOffline && order.OrderStatus != CanteenOrderStatusEnumType.Success; return !order.IsOffline && order.OrderStatus != CanteenOrderStatusEnumType.Success;
} }
private async Task TryConsumeAsync(long orderId, Func<Task> action)
{
try
{
await action();
await _dbContext.Updateable<CanteenOrderEntity>()
.SetColumns(x => x.OrderStatus == CanteenOrderStatusEnumType.Success)
.SetColumns(x => x.ModifiedTime == DateTime.Now)
.Where(x => x.Id == orderId)
.ExecuteCommandAsync();
}
catch (Exception ex)
{
await _dbContext.Updateable<CanteenOrderEntity>()
.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;
}
}
} }

View File

@@ -40,8 +40,8 @@ public class BirthdayTicketService : BaseTicketService, IBirthdayTicketService
{ {
Type = TicketEnumType.Birthday, Type = TicketEnumType.Birthday,
UserCode = item.Code, UserCode = item.Code,
OriginalAmount = amount, OriginalBalance = amount,
Amount = amount, Balance = amount,
QrCode = qrCode, QrCode = qrCode,
Remark = todayStr + "生日券", Remark = todayStr + "生日券",
TenantId = item.Factory TenantId = item.Factory

View File

@@ -10,7 +10,7 @@ namespace Dlhis.Entity.Canteen.Entities;
/// 券 /// 券
/// </summary> /// </summary>
[SugarTable("Tickets", TableDescription = "券")] [SugarTable("Tickets", TableDescription = "券")]
public class TicketEntity : SugarBaseEntity, ITenantId public class TicketEntity : SugarBaseEntity, ITenantId, IVersion
{ {
/// <summary> /// <summary>
/// 类型 /// 类型
@@ -28,13 +28,13 @@ public class TicketEntity : SugarBaseEntity, ITenantId
/// 初始余额 /// 初始余额
/// </summary> /// </summary>
[SugarColumn(IsNullable = false, ColumnDescription = "初始余额")] [SugarColumn(IsNullable = false, ColumnDescription = "初始余额")]
public decimal OriginalAmount { get; set; } public decimal OriginalBalance { get; set; }
/// <summary> /// <summary>
/// 当前余额 /// 当前余额
/// </summary> /// </summary>
[SugarColumn(IsNullable = false, ColumnDescription = "当前余额")] [SugarColumn(IsNullable = false, ColumnDescription = "当前余额")]
public decimal Amount { get; set; } public decimal Balance { get; set; }
/// <summary> /// <summary>
/// 消费码 /// 消费码
@@ -49,4 +49,7 @@ public class TicketEntity : SugarBaseEntity, ITenantId
public string Remark { get; set; } public string Remark { get; set; }
public string TenantId { get; set; } public string TenantId { get; set; }
/// <inheritdoc />
public int Version { get; set; }
} }

View File

@@ -37,6 +37,14 @@ public enum BalanceRecordSourceEnumType
/// </summary> /// </summary>
[Description("电子餐券")] [Description("电子餐券")]
EMealVoucher, EMealVoucher,
/// <summary>
/// 生日券
/// </summary>
[Description("生日券")]
Birthday,
//********************************************************************************************************* //*********************************************************************************************************
/// <summary> /// <summary>
@@ -70,7 +78,7 @@ public enum BalanceRecordSourceEnumType
/// </summary> /// </summary>
[Description("其他基地餐补")] [Description("其他基地餐补")]
OtherTenantSubsidies, OtherTenantSubsidies,
/// <summary> /// <summary>
/// 余额写入 /// 余额写入
/// </summary> /// </summary>