1. 新增InteractiveMagazineApiDefaultsExtensions统一注册共享服务,简化WeChatApi和WebApi的Program.cs代码 2. 修复积分增减的并发问题,改用原子更新操作,添加余额校验避免超扣 3. 修复注释乱码问题,将简体中文注释替换为正确文本 4. 新增.editorconfig配置统一代码格式
287 lines
10 KiB
C#
287 lines
10 KiB
C#
using Microsoft.Extensions.Logging;
|
||
using QYZH.InteractiveMagazine.IService;
|
||
using QYZH.InteractiveMagazine.Models.Common;
|
||
using QYZH.InteractiveMagazine.Models.Dto;
|
||
using QYZH.InteractiveMagazine.Models.Dto.Points;
|
||
using QYZH.InteractiveMagazine.Models.Entity;
|
||
using QYZH.InteractiveMagazine.Models.Enum;
|
||
using QYZH.InteractiveMagazine.Repository;
|
||
|
||
namespace QYZH.InteractiveMagazine.Service;
|
||
|
||
/// <summary>
|
||
/// 积分服务实现
|
||
/// </summary>
|
||
public class PointsService(
|
||
BaseRepository<PointsRecord> pointsRecordRepository,
|
||
ILogger<PointsService> logger)
|
||
: BaseRepository<PointsRecord>, IPointsService
|
||
{
|
||
#region 带事务版本(独立调用)
|
||
|
||
/// <summary>
|
||
/// 增加积分(带事务)
|
||
/// </summary>
|
||
public async Task<AddPointsOutput> AddPointsAsync(AddPointsInput input)
|
||
{
|
||
logger.LogInformation("用户增加积分,UserId: {UserId}, Amount: {Amount}, Type: {Type}",
|
||
input.UserId, input.Amount, input.ChangeType);
|
||
|
||
if (input.Amount <= 0)
|
||
throw new BusinessException("增加积分数量必须大于0", ResultCode.BAD_REQUEST);
|
||
|
||
AddPointsOutput result = null!;
|
||
|
||
await UseTranAsync(async () =>
|
||
{
|
||
result = await AddPointsInTranAsync(input);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 扣除积分(带事务)
|
||
/// </summary>
|
||
public async Task<DeductPointsOutput> DeductPointsAsync(DeductPointsInput input)
|
||
{
|
||
logger.LogInformation("用户扣除积分,UserId: {UserId}, Amount: {Amount}, Type: {Type}",
|
||
input.UserId, input.Amount, input.ChangeType);
|
||
|
||
if (input.Amount <= 0)
|
||
throw new BusinessException("扣除积分数量必须大于0", ResultCode.BAD_REQUEST);
|
||
|
||
DeductPointsOutput result = null!;
|
||
|
||
await UseTranAsync(async () =>
|
||
{
|
||
result = await DeductPointsInTranAsync(input);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 无事务版本(供外部事务调用)
|
||
|
||
/// <summary>
|
||
/// 增加积分(无事务,需在外部事务中调用)
|
||
/// </summary>
|
||
public async Task<AddPointsOutput> AddPointsInTranAsync(AddPointsInput input)
|
||
{
|
||
if (input.Amount <= 0)
|
||
throw new BusinessException("增加积分数量必须大于0", ResultCode.BAD_REQUEST);
|
||
|
||
var now = DateTime.Now;
|
||
|
||
// 原子增加积分,避免并发写回旧余额覆盖新余额
|
||
var affectedRows = await Context.Updateable<Users>()
|
||
.SetColumns(u => u.Points == u.Points + input.Amount)
|
||
.SetColumns(u => u.UpdatedAt == now)
|
||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||
.ExecuteCommandAsync();
|
||
|
||
if (affectedRows <= 0)
|
||
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
||
|
||
var user = await Context.Queryable<Users>()
|
||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (user == null)
|
||
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
||
|
||
var newBalance = user.Points;
|
||
var previousBalance = newBalance - input.Amount;
|
||
|
||
// 插入积分流水记录
|
||
var record = new PointsRecord
|
||
{
|
||
UserId = input.UserId,
|
||
ChangeAmount = input.Amount,
|
||
BalanceAfter = newBalance,
|
||
ChangeType = input.ChangeType.ToString(),
|
||
RelatedId = input.RelatedId,
|
||
Description = input.Description,
|
||
Type = PointsFlowTypeEnum.Income,
|
||
Status = (int)PointsRecordStatusEnum.Success,
|
||
IsDeleted = false,
|
||
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
||
CreatedAt = now,
|
||
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
||
UpdatedAt = now
|
||
};
|
||
|
||
var recordEntity = await InsertReturnEntityAsync(record);
|
||
|
||
logger.LogInformation("增加积分成功,UserId: {UserId}, 积分: {Before} -> {After}, 变动: +{Amount}",
|
||
input.UserId, previousBalance, newBalance, input.Amount);
|
||
|
||
return new AddPointsOutput
|
||
{
|
||
RecordId = recordEntity.Id,
|
||
PreviousBalance = previousBalance,
|
||
NewBalance = newBalance,
|
||
AddedAmount = input.Amount
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 扣除积分(无事务,需在外部事务中调用)
|
||
/// </summary>
|
||
public async Task<DeductPointsOutput> DeductPointsInTranAsync(DeductPointsInput input)
|
||
{
|
||
if (input.Amount <= 0)
|
||
throw new BusinessException("扣除积分数量必须大于0", ResultCode.BAD_REQUEST);
|
||
|
||
var now = DateTime.Now;
|
||
|
||
// 带余额条件的原子扣减,避免并发扣减时超扣或覆盖余额
|
||
var affectedRows = await Context.Updateable<Users>()
|
||
.SetColumns(u => u.Points == u.Points - input.Amount)
|
||
.SetColumns(u => u.UpdatedAt == now)
|
||
.Where(u => u.Id == input.UserId && !u.IsDeleted && u.Points >= input.Amount)
|
||
.ExecuteCommandAsync();
|
||
|
||
if (affectedRows <= 0)
|
||
{
|
||
var currentUser = await Context.Queryable<Users>()
|
||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (currentUser == null)
|
||
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
||
|
||
throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {currentUser.Points}", ResultCode.BAD_REQUEST);
|
||
}
|
||
|
||
var user = await Context.Queryable<Users>()
|
||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (user == null)
|
||
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
||
|
||
var newBalance = user.Points;
|
||
var previousBalance = newBalance + input.Amount;
|
||
|
||
// 插入积分流水记录
|
||
var record = new PointsRecord
|
||
{
|
||
UserId = input.UserId,
|
||
ChangeAmount = -input.Amount,
|
||
BalanceAfter = newBalance,
|
||
ChangeType = input.ChangeType.ToString(),
|
||
RelatedId = input.RelatedId,
|
||
Description = input.Description,
|
||
Type = PointsFlowTypeEnum.Expense,
|
||
Status = (int)PointsRecordStatusEnum.Success,
|
||
IsDeleted = false,
|
||
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
||
CreatedAt = now,
|
||
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
||
UpdatedAt = now
|
||
};
|
||
|
||
var recordEntity = await InsertReturnEntityAsync(record);
|
||
|
||
logger.LogInformation("扣除积分成功,UserId: {UserId}, 积分: {Before} -> {After}, 变动: -{Amount}",
|
||
input.UserId, previousBalance, newBalance, input.Amount);
|
||
|
||
return new DeductPointsOutput
|
||
{
|
||
RecordId = recordEntity.Id,
|
||
PreviousBalance = previousBalance,
|
||
NewBalance = newBalance,
|
||
DeductedAmount = input.Amount
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 查询
|
||
|
||
/// <summary>
|
||
/// 查询用户当前积分余额
|
||
/// </summary>
|
||
public async Task<int> GetUserPointsAsync(long userId)
|
||
{
|
||
var user = await Context.Queryable<Users>()
|
||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||
.Select(u => u.Points)
|
||
.FirstAsync();
|
||
|
||
return user;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取用户积分概览
|
||
/// </summary>
|
||
public async Task<PointsSummaryOutput> GetPointsSummaryAsync(long userId)
|
||
{
|
||
// 查询用户
|
||
var user = await Context.Queryable<Users>()
|
||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (user == null)
|
||
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
||
|
||
// 查询累计收入(Income 类型)
|
||
var totalIncome = await Context.Queryable<PointsRecord>()
|
||
.Where(r => r.UserId == userId && !r.IsDeleted && r.Type == PointsFlowTypeEnum.Income && r.Status == (int)PointsRecordStatusEnum.Success)
|
||
.SumAsync(r => r.ChangeAmount);
|
||
|
||
// 查询累计支出(Expense 类型,取绝对值)
|
||
var totalExpense = await Context.Queryable<PointsRecord>()
|
||
.Where(r => r.UserId == userId && !r.IsDeleted && r.Type == PointsFlowTypeEnum.Expense && r.Status == (int)PointsRecordStatusEnum.Success)
|
||
.SumAsync(r => r.ChangeAmount);
|
||
|
||
return new PointsSummaryOutput
|
||
{
|
||
UserId = userId,
|
||
CurrentBalance = user.Points,
|
||
TotalIncome = totalIncome,
|
||
TotalExpense = Math.Abs(totalExpense)
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询积分流水
|
||
/// </summary>
|
||
public async Task<PageListModel<PointsRecordOutput>> GetPointsRecordsAsync(PointsRecordQueryInput input)
|
||
{
|
||
if (input.PageIndex <= 0)
|
||
throw new BusinessException("页码必须大于0", ResultCode.BAD_REQUEST);
|
||
|
||
if (input.PageSize <= 0 || input.PageSize > 100)
|
||
throw new BusinessException("每页条数必须在1-100之间", ResultCode.BAD_REQUEST);
|
||
|
||
var query = Context.Queryable<PointsRecord>()
|
||
.Where(r => r.UserId == input.UserId && !r.IsDeleted)
|
||
.WhereIF(input.ChangeType.HasValue, r => r.ChangeType == input.ChangeType.Value.ToString())
|
||
.WhereIF(input.Type.HasValue, r => r.Type == input.Type.Value)
|
||
.WhereIF(!string.IsNullOrEmpty(input.Status), r => r.Status.ToString() == input.Status)
|
||
.OrderByDescending(r => r.CreatedAt);
|
||
|
||
var total = 0;
|
||
var records = await query
|
||
.Select(r => new PointsRecordOutput
|
||
{
|
||
Id = (long)r.Id,
|
||
ChangeAmount = r.ChangeAmount,
|
||
BalanceAfter = r.BalanceAfter,
|
||
ChangeType = r.ChangeType,
|
||
Description = r.Description,
|
||
Type = r.Type.ToString(),
|
||
CreatedAt = r.CreatedAt
|
||
})
|
||
.ToPageListAsync(input.PageIndex, input.PageSize, total);
|
||
|
||
return new PageListModel<PointsRecordOutput>(records, input.PageIndex, input.PageSize, total);
|
||
}
|
||
|
||
#endregion
|
||
}
|