Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/PointsService.cs

288 lines
10 KiB
C#
Raw Normal View History

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;
using SqlSugar;
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);
RefAsync<int> 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
}