2026-06-08 16:57:44 +08:00
|
|
|
|
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)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("增加积分数量必须大于0", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("扣除积分数量必须大于0", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("增加积分数量必须大于0", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 查询用户当前积分
|
|
|
|
|
|
var user = await Context.Queryable<Users>()
|
|
|
|
|
|
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
|
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (user == null)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
var previousBalance = user.Points;
|
|
|
|
|
|
var newBalance = previousBalance + input.Amount;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户积分
|
|
|
|
|
|
await Context.Updateable<Users>()
|
|
|
|
|
|
.SetColumns(u => u.Points == newBalance)
|
|
|
|
|
|
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
|
|
|
|
|
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
|
|
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 插入积分流水记录
|
|
|
|
|
|
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,
|
2026-06-08 17:54:36 +08:00
|
|
|
|
Status = (int)PointsRecordStatusEnum.Success,
|
2026-06-08 16:57:44 +08:00
|
|
|
|
IsDeleted = false,
|
|
|
|
|
|
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
|
|
|
|
|
CreatedAt = DateTime.Now,
|
|
|
|
|
|
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
|
|
|
|
|
UpdatedAt = DateTime.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)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("扣除积分数量必须大于0", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 查询用户当前积分
|
|
|
|
|
|
var user = await Context.Queryable<Users>()
|
|
|
|
|
|
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
|
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (user == null)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
var previousBalance = user.Points;
|
|
|
|
|
|
|
|
|
|
|
|
// 余额不足校验
|
|
|
|
|
|
if (previousBalance < input.Amount)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {previousBalance}", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
var newBalance = previousBalance - input.Amount;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新用户积分
|
|
|
|
|
|
await Context.Updateable<Users>()
|
|
|
|
|
|
.SetColumns(u => u.Points == newBalance)
|
|
|
|
|
|
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
|
|
|
|
|
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
|
|
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 插入积分流水记录
|
|
|
|
|
|
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,
|
2026-06-08 17:54:36 +08:00
|
|
|
|
Status = (int)PointsRecordStatusEnum.Success,
|
2026-06-08 16:57:44 +08:00
|
|
|
|
IsDeleted = false,
|
|
|
|
|
|
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
|
|
|
|
|
CreatedAt = DateTime.Now,
|
|
|
|
|
|
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
|
|
|
|
|
|
UpdatedAt = DateTime.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)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 查询累计收入(Income 类型)
|
|
|
|
|
|
var totalIncome = await Context.Queryable<PointsRecord>()
|
2026-06-08 17:54:36 +08:00
|
|
|
|
.Where(r => r.UserId == userId && !r.IsDeleted && r.Type == PointsFlowTypeEnum.Income && r.Status == (int)PointsRecordStatusEnum.Success)
|
2026-06-08 16:57:44 +08:00
|
|
|
|
.SumAsync(r => r.ChangeAmount);
|
|
|
|
|
|
|
|
|
|
|
|
// 查询累计支出(Expense 类型,取绝对值)
|
|
|
|
|
|
var totalExpense = await Context.Queryable<PointsRecord>()
|
2026-06-08 17:54:36 +08:00
|
|
|
|
.Where(r => r.UserId == userId && !r.IsDeleted && r.Type == PointsFlowTypeEnum.Expense && r.Status == (int)PointsRecordStatusEnum.Success)
|
2026-06-08 16:57:44 +08:00
|
|
|
|
.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)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("页码必须大于0", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
if (input.PageSize <= 0 || input.PageSize > 100)
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("每页条数必须在1-100之间", ResultCode.BAD_REQUEST);
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|