refactor: 批量新增枚举类型并完成实体、DTO、服务层枚举替换
- 新增30+业务枚举类型覆盖用户、宠物、商城、社区、积分等模块 - 完成实体类、DTO、服务层的字符串枚举替换为强类型枚举 - 修复用户状态枚举名称变更,将Frozen改为Disabled - 新增批量发布社区消息接口与控制器实现 - 新增操作日志、积分管理、补偿任务相关服务与DTO
This commit is contained in:
@ -66,7 +66,7 @@ public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, ICo
|
||||
Token = token,
|
||||
UserId = (long)adminUser.Id,
|
||||
UserName = adminUser.UserName,
|
||||
Type = adminUser.Type
|
||||
Type = adminUser.Type.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, ICo
|
||||
{
|
||||
UserId = adminUser.Id,
|
||||
UserName = adminUser.UserName,
|
||||
Type = adminUser.Type,
|
||||
Type = adminUser.Type.ToString(),
|
||||
Status = adminUser.Status
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -64,7 +65,7 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
|
||||
|
||||
logger.LogInformation("管理员创建成功,用户名: {UserName}, ID: {Id}", input.UserName, adminUser.Id);
|
||||
|
||||
return new AdminUserOutput { Id = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type, Status = adminUser.Status, CreatedBy = adminUser.CreatedBy, CreatedAt = adminUser.CreatedAt, UpdatedBy = adminUser.UpdatedBy, UpdatedAt = adminUser.UpdatedAt };
|
||||
return new AdminUserOutput { Id = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type.ToString(), Status = adminUser.Status, CreatedBy = adminUser.CreatedBy, CreatedAt = adminUser.CreatedAt, UpdatedBy = adminUser.UpdatedBy, UpdatedAt = adminUser.UpdatedAt };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -100,10 +101,7 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
|
||||
adminUser.PasswordHash = BCrypt.Net.BCrypt.HashPassword(input.Password);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(input.Type))
|
||||
{
|
||||
adminUser.Type = input.Type;
|
||||
}
|
||||
adminUser.Type = input.Type;
|
||||
|
||||
|
||||
adminUser.UpdatedBy = "System";
|
||||
@ -118,7 +116,7 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
|
||||
|
||||
logger.LogInformation("管理员更新成功,ID: {Id}", id);
|
||||
|
||||
return new AdminUserOutput { Id = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type, Status = adminUser.Status, CreatedBy = adminUser.CreatedBy, CreatedAt = adminUser.CreatedAt, UpdatedBy = adminUser.UpdatedBy, UpdatedAt = adminUser.UpdatedAt };
|
||||
return new AdminUserOutput { Id = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type.ToString(), Status = adminUser.Status, CreatedBy = adminUser.CreatedBy, CreatedAt = adminUser.CreatedAt, UpdatedBy = adminUser.UpdatedBy, UpdatedAt = adminUser.UpdatedAt };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -162,7 +160,7 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
|
||||
{
|
||||
Id = adminUser.Id,
|
||||
UserName = adminUser.UserName,
|
||||
Type = adminUser.Type,
|
||||
Type = adminUser.Type.ToString(),
|
||||
Status = adminUser.Status,
|
||||
CreatedBy = adminUser.CreatedBy,
|
||||
CreatedAt = adminUser.CreatedAt,
|
||||
@ -195,7 +193,7 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
|
||||
{
|
||||
Id = a.Id,
|
||||
UserName = a.UserName,
|
||||
Type = a.Type,
|
||||
Type = a.Type.ToString(),
|
||||
Status = a.Status,
|
||||
CreatedBy = a.CreatedBy,
|
||||
CreatedAt = a.CreatedAt,
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Points;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
@ -16,6 +19,7 @@ public class CheckInService(
|
||||
BaseRepository<CheckInRecord> checkInRecordRepository,
|
||||
IPetService petService,
|
||||
ICompensationTaskService compensationTaskService,
|
||||
IPointsService pointsService,
|
||||
ILogger<CheckInService> logger)
|
||||
: BaseRepository<CheckInRecord>, ICheckInService
|
||||
{
|
||||
@ -77,35 +81,29 @@ public class CheckInService(
|
||||
PointsAwarded = pointsReward,
|
||||
GrowthPointsAwarded = growthReward,
|
||||
ConsecutiveDays = consecutiveDays,
|
||||
Type = "Normal",
|
||||
Status = "Success"
|
||||
Type = CheckInRecordTypeEnum.Normal,
|
||||
Status = (int)CheckInRecordStatusEnum.Success
|
||||
};
|
||||
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
|
||||
checkInRecord.Id = recordId;
|
||||
|
||||
// 6b. 更新用户积分余额
|
||||
var newPointsBalance = user.Points + pointsReward;
|
||||
// 6b. 更新用户成长值
|
||||
var newGrowthBalance = user.GrowthPoints + growthReward;
|
||||
|
||||
await checkInRecordRepository.Context.Updateable<Users>()
|
||||
.SetColumns(u => u.Points == newPointsBalance)
|
||||
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 6c. 创建积分变动记录
|
||||
var pointsRecord = new PointsRecord
|
||||
// 6c. 通过积分服务增加积分
|
||||
var pointsResult = await pointsService.AddPointsInTranAsync(new AddPointsInput
|
||||
{
|
||||
UserId = userId,
|
||||
ChangeAmount = pointsReward,
|
||||
BalanceAfter = newPointsBalance,
|
||||
ChangeType = "SignIn",
|
||||
Amount = pointsReward,
|
||||
ChangeType = PointsChangeTypeEnum.SignIn,
|
||||
RelatedId = recordId,
|
||||
Description = $"签到奖励(连续{consecutiveDays}天)",
|
||||
Type = "Income",
|
||||
Status = "Success"
|
||||
};
|
||||
await checkInRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
||||
Description = $"签到奖励(连续{consecutiveDays}天)"
|
||||
});
|
||||
|
||||
// 构建返回结果
|
||||
result.RecordId = (long)recordId;
|
||||
@ -113,13 +111,13 @@ public class CheckInService(
|
||||
result.ConsecutiveDays = consecutiveDays;
|
||||
result.PointsAwarded = pointsReward;
|
||||
result.GrowthPointsAwarded = growthReward;
|
||||
result.PointsBalance = newPointsBalance;
|
||||
result.PointsBalance = pointsResult.NewBalance;
|
||||
result.GrowthPointsBalance = newGrowthBalance;
|
||||
result.HasPet = pet != null;
|
||||
});
|
||||
|
||||
// 7. 如果用户有活跃宠物,调用 PetService 喂养(含进化检查),独立事务
|
||||
if (pet != null && pet.Status == "Active" && growthReward > 0)
|
||||
if (pet != null && pet.Status == UserPetStatusEnum.Active && growthReward > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -141,7 +139,7 @@ public class CheckInService(
|
||||
|
||||
await compensationTaskService.CreateTaskAsync(new CreateCompensationTaskInput
|
||||
{
|
||||
TaskType = CompensationTaskType.PetFeeding,
|
||||
TaskType = CompensationTaskTypeEnum.PetFeeding,
|
||||
BusinessSource = "CheckIn",
|
||||
BusinessId = result.RecordId.ToString(),
|
||||
UserId = userId,
|
||||
@ -212,8 +210,8 @@ public class CheckInService(
|
||||
ConsecutiveDays = r.ConsecutiveDays,
|
||||
PointsAwarded = r.PointsAwarded,
|
||||
GrowthPointsAwarded = r.GrowthPointsAwarded,
|
||||
Type = r.Type,
|
||||
Status = r.Status
|
||||
Type = r.Type.ToString(),
|
||||
Status = r.Status.ToString()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
@ -277,8 +275,8 @@ public class CheckInService(
|
||||
PointsAwarded = pointsReward,
|
||||
GrowthPointsAwarded = growthReward,
|
||||
ConsecutiveDays = 0, // 补签不纳入连续天数
|
||||
Type = "MakeUp",
|
||||
Status = "Success",
|
||||
Type = CheckInRecordTypeEnum.MakeUp,
|
||||
Status = (int)CheckInRecordStatusEnum.Success,
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -288,47 +286,36 @@ public class CheckInService(
|
||||
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
|
||||
checkInRecord.Id = recordId;
|
||||
|
||||
// 更新用户积分和成长值
|
||||
var newPointsBalance = user.Points + pointsReward;
|
||||
// 更新用户成长值
|
||||
var newGrowthBalance = user.GrowthPoints + growthReward;
|
||||
|
||||
await checkInRecordRepository.Context.Updateable<Users>()
|
||||
.SetColumns(u => u.Points == newPointsBalance)
|
||||
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 创建积分变动记录
|
||||
var pointsRecord = new PointsRecord
|
||||
// 通过积分服务增加积分
|
||||
var pointsResult = await pointsService.AddPointsInTranAsync(new AddPointsInput
|
||||
{
|
||||
UserId = userId,
|
||||
ChangeAmount = pointsReward,
|
||||
BalanceAfter = newPointsBalance,
|
||||
ChangeType = "MakeUpSign",
|
||||
Amount = pointsReward,
|
||||
ChangeType = PointsChangeTypeEnum.MakeUpSign,
|
||||
RelatedId = recordId,
|
||||
Description = $"补签奖励({targetDate:yyyy-MM-dd})",
|
||||
Type = "Income",
|
||||
Status = "Success",
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = userId.ToString(),
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
await checkInRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
||||
Description = $"补签奖励({targetDate:yyyy-MM-dd})"
|
||||
});
|
||||
|
||||
result.RecordId = (long)recordId;
|
||||
result.CheckInDate = targetDate;
|
||||
result.ConsecutiveDays = 0;
|
||||
result.PointsAwarded = pointsReward;
|
||||
result.GrowthPointsAwarded = growthReward;
|
||||
result.PointsBalance = newPointsBalance;
|
||||
result.PointsBalance = pointsResult.NewBalance;
|
||||
result.GrowthPointsBalance = newGrowthBalance;
|
||||
result.HasPet = pet != null;
|
||||
});
|
||||
|
||||
// 如果有活跃宠物,喂养成长值
|
||||
if (pet != null && pet.Status == "Active" && growthReward > 0)
|
||||
if (pet != null && pet.Status == UserPetStatusEnum.Active && growthReward > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -404,7 +391,7 @@ public class CheckInService(
|
||||
{
|
||||
// 查询签到配置(按 DayNumber 升序)
|
||||
var configs = await checkInRecordRepository.Context.Queryable<CheckInConfig>()
|
||||
.Where(c => c.Status == "Active" && !c.IsDeleted)
|
||||
.Where(c => c.Status == CheckInConfigStatusEnum.Active && !c.IsDeleted)
|
||||
.OrderBy(c => c.DayNumber)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -28,11 +30,11 @@ public class CommunityMessageService(BaseRepository<CommunityMessage> messageRep
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var pageResult = await messageRepository.Queryable()
|
||||
.WhereIF(input.JournalId.HasValue, m => m.JournalId == input.JournalId.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), m => m.Type == input.Type)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), m => m.Type.ToString() == input.Type)
|
||||
.WhereIF(input.Status.HasValue, m => m.Status == input.Status.Value)
|
||||
.WhereIF(input.IsFeatured.HasValue, m => m.IsFeatured == input.IsFeatured.Value)
|
||||
.WhereIF(input.IsActive.HasValue, m => m.IsActive == input.IsActive.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), m => m.Content.Contains(input.Keyword))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.KeyWord), m => m.Content.Contains(input.KeyWord))
|
||||
.WhereIF(input.UserId.HasValue, m => m.UserId == input.UserId.Value)
|
||||
.OrderByDescending(m => m.IsFeatured)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
@ -48,7 +50,7 @@ public class CommunityMessageService(BaseRepository<CommunityMessage> messageRep
|
||||
ImageUrl = m.ImageUrl,
|
||||
SortOrder = m.SortOrder,
|
||||
IsActive = m.IsActive,
|
||||
Type = m.Type,
|
||||
Type = m.Type.ToString(),
|
||||
LikeCount = m.LikeCount,
|
||||
IsFeatured = m.IsFeatured,
|
||||
Status = m.Status,
|
||||
@ -88,7 +90,7 @@ public class CommunityMessageService(BaseRepository<CommunityMessage> messageRep
|
||||
ImageUrl = message.ImageUrl,
|
||||
SortOrder = message.SortOrder,
|
||||
IsActive = message.IsActive,
|
||||
Type = message.Type,
|
||||
Type = message.Type.ToString(),
|
||||
LikeCount = message.LikeCount,
|
||||
IsFeatured = message.IsFeatured,
|
||||
Status = message.Status,
|
||||
@ -216,4 +218,24 @@ public class CommunityMessageService(BaseRepository<CommunityMessage> messageRep
|
||||
|
||||
logger.LogInformation("社区消息排序权重设置成功,ID: {Id}, SortOrder: {SortOrder}", id, sortOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量发布消息
|
||||
/// </summary>
|
||||
public async Task BatchPublishAsync(List<long> ids)
|
||||
{
|
||||
logger.LogInformation("正在批量发布社区消息,数量: {Count}", ids.Count);
|
||||
|
||||
if (ids == null || ids.Count == 0)
|
||||
{
|
||||
throw new BusinessException("消息ID列表不能为空", 400);
|
||||
}
|
||||
|
||||
var result = await Context.Updateable<CommunityMessage>()
|
||||
.SetColumns(m => m.IsActive == true)
|
||||
.Where(m => ids.Contains(m.Id))
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
logger.LogInformation("批量发布社区消息完成,影响行数: {Result}", result);
|
||||
}
|
||||
}
|
||||
|
||||
191
QYZH.InteractiveMagazine.Service/CompensationManageService.cs
Normal file
191
QYZH.InteractiveMagazine.Service/CompensationManageService.cs
Normal file
@ -0,0 +1,191 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务管理服务实现
|
||||
/// </summary>
|
||||
public class CompensationManageService(
|
||||
BaseRepository<CompensationTask> compensationTaskRepository,
|
||||
IOperationLogService operationLogService,
|
||||
ILogger<CompensationManageService> logger)
|
||||
: BaseRepository<CompensationTask>, ICompensationManageService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询补偿任务(含用户昵称)
|
||||
/// </summary>
|
||||
public async Task<PageListModel<CompensationManageOutput>> GetListAsync(CompensationTaskQueryInput input)
|
||||
{
|
||||
if (input.PageIndex <= 0)
|
||||
input.PageIndex = 1;
|
||||
|
||||
if (input.PageSize <= 0 || input.PageSize > 100)
|
||||
input.PageSize = 10;
|
||||
|
||||
RefAsync<int> totalNumber = 0;
|
||||
|
||||
var pageResult = await compensationTaskRepository.Queryable()
|
||||
.LeftJoin<Users>((t, u) => t.UserId == u.Id)
|
||||
.WhereIF(input.UserId.HasValue, (t, u) => t.UserId == input.UserId.Value)
|
||||
.WhereIF(input.TaskType.HasValue, (t, u) => t.TaskType == (int)input.TaskType.Value)
|
||||
.WhereIF(input.Status.HasValue, (t, u) => t.Status == input.Status.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.BusinessSource), (t, u) => t.BusinessSource == input.BusinessSource)
|
||||
.OrderByDescending((t, u) => t.CreatedAt)
|
||||
.Select((t, u) => new CompensationManageOutput
|
||||
{
|
||||
Id = t.Id,
|
||||
TaskType = (CompensationTaskTypeEnum)t.TaskType,
|
||||
BusinessSource = t.BusinessSource,
|
||||
BusinessId = t.BusinessId,
|
||||
UserId = t.UserId,
|
||||
UserName = u.Name,
|
||||
Payload = t.Payload,
|
||||
ErrorMessage = t.ErrorMessage,
|
||||
ErrorSource = t.ErrorSource,
|
||||
RetryCount = t.RetryCount,
|
||||
MaxRetries = t.MaxRetries,
|
||||
Status = t.Status,
|
||||
ProcessedAt = t.ProcessedAt,
|
||||
ScheduledAt = t.ScheduledAt,
|
||||
ResultMessage = t.ResultMessage,
|
||||
CreatedAt = t.CreatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
return new PageListModel<CompensationManageOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动重试补偿任务
|
||||
/// </summary>
|
||||
public async Task RetryAsync(long taskId, long operatorId, string operatorName, CompensationRetryInput input, string? ipAddress = null)
|
||||
{
|
||||
logger.LogInformation("手动重试补偿任务,TaskId: {TaskId}, Operator: {Operator}", taskId, operatorName);
|
||||
|
||||
var task = await compensationTaskRepository.GetByIdAsync(taskId);
|
||||
if (task == null)
|
||||
throw new BusinessException("补偿任务不存在", 404);
|
||||
|
||||
if (task.Status != CompensationTaskStatusEnum.Failed && task.Status != CompensationTaskStatusEnum.Cancelled)
|
||||
throw new BusinessException($"只有失败或已取消的任务才能重试,当前状态: {task.Status}", 400);
|
||||
|
||||
// 重置任务状态为 Pending,清零重试次数,设置立即执行
|
||||
await compensationTaskRepository.Context.Updateable<CompensationTask>()
|
||||
.SetColumns(t => t.Status == CompensationTaskStatusEnum.Pending)
|
||||
.SetColumns(t => t.RetryCount == 0)
|
||||
.SetColumns(t => t.ScheduledAt == DateTime.Now)
|
||||
.SetColumns(t => t.ResultMessage == $"管理员手动重试: {input.Reason}")
|
||||
.SetColumns(t => t.UpdatedBy == operatorName)
|
||||
.SetColumns(t => t.UpdatedAt == DateTime.Now)
|
||||
.Where(t => t.Id == taskId && !t.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 记录操作日志
|
||||
var detail = JsonConvert.SerializeObject(new
|
||||
{
|
||||
OriginalStatus = task.Status,
|
||||
Reason = input.Reason,
|
||||
TaskType = task.TaskType,
|
||||
UserId = task.UserId
|
||||
});
|
||||
|
||||
await operationLogService.LogAsync(
|
||||
operatorId, operatorName,
|
||||
OperationLogActionType.CompensationRetry,
|
||||
OperationLogTargetType.CompensationTask,
|
||||
taskId, null, detail, ipAddress);
|
||||
|
||||
logger.LogInformation("补偿任务手动重试成功,TaskId: {TaskId}", taskId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记补偿任务为已解决
|
||||
/// </summary>
|
||||
public async Task ResolveAsync(long taskId, long operatorId, string operatorName, CompensationResolveInput input, string? ipAddress = null)
|
||||
{
|
||||
logger.LogInformation("标记补偿任务已解决,TaskId: {TaskId}, Operator: {Operator}", taskId, operatorName);
|
||||
|
||||
var task = await compensationTaskRepository.GetByIdAsync(taskId);
|
||||
if (task == null)
|
||||
throw new BusinessException("补偿任务不存在", 404);
|
||||
|
||||
if (task.Status == CompensationTaskStatusEnum.Success)
|
||||
throw new BusinessException("该任务已经是成功状态,无需标记", 400);
|
||||
|
||||
// 标记为 Success
|
||||
await compensationTaskRepository.Context.Updateable<CompensationTask>()
|
||||
.SetColumns(t => t.Status == CompensationTaskStatusEnum.Success)
|
||||
.SetColumns(t => t.ResultMessage == $"管理员手动标记已解决: {input.ResolveNote}")
|
||||
.SetColumns(t => t.ProcessedAt == DateTime.Now)
|
||||
.SetColumns(t => t.UpdatedBy == operatorName)
|
||||
.SetColumns(t => t.UpdatedAt == DateTime.Now)
|
||||
.Where(t => t.Id == taskId && !t.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 记录操作日志
|
||||
var detail = JsonConvert.SerializeObject(new
|
||||
{
|
||||
OriginalStatus = task.Status,
|
||||
ResolveNote = input.ResolveNote,
|
||||
TaskType = task.TaskType,
|
||||
UserId = task.UserId
|
||||
});
|
||||
|
||||
await operationLogService.LogAsync(
|
||||
operatorId, operatorName,
|
||||
OperationLogActionType.CompensationResolve,
|
||||
OperationLogTargetType.CompensationTask,
|
||||
taskId, null, detail, ipAddress);
|
||||
|
||||
logger.LogInformation("补偿任务标记已解决成功,TaskId: {TaskId}", taskId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取补偿任务详情
|
||||
/// </summary>
|
||||
public async Task<CompensationManageDetailOutput> GetDetailAsync(long taskId)
|
||||
{
|
||||
logger.LogInformation("获取补偿任务详情,TaskId: {TaskId}", taskId);
|
||||
|
||||
var result = await compensationTaskRepository.Queryable()
|
||||
.LeftJoin<Users>((t, u) => t.UserId == u.Id)
|
||||
.Where((t, u) => t.Id == taskId && !t.IsDeleted)
|
||||
.Select((t, u) => new CompensationManageDetailOutput
|
||||
{
|
||||
Id = t.Id,
|
||||
TaskType = (CompensationTaskTypeEnum)t.TaskType,
|
||||
BusinessSource = t.BusinessSource,
|
||||
BusinessId = t.BusinessId,
|
||||
UserId = t.UserId,
|
||||
UserName = u.Name,
|
||||
Payload = t.Payload,
|
||||
ErrorMessage = t.ErrorMessage,
|
||||
ErrorSource = t.ErrorSource,
|
||||
RetryCount = t.RetryCount,
|
||||
MaxRetries = t.MaxRetries,
|
||||
Status = t.Status,
|
||||
ProcessedAt = t.ProcessedAt,
|
||||
ScheduledAt = t.ScheduledAt,
|
||||
ResultMessage = t.ResultMessage,
|
||||
CreatedAt = t.CreatedAt,
|
||||
CreatedBy = t.CreatedBy,
|
||||
UpdatedBy = t.UpdatedBy,
|
||||
UpdatedAt = t.UpdatedAt
|
||||
})
|
||||
.FirstAsync();
|
||||
|
||||
if (result == null)
|
||||
throw new BusinessException("补偿任务不存在", 404);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ using Newtonsoft.Json;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
@ -28,7 +29,7 @@ public class CompensationTaskService(
|
||||
|
||||
var task = new CompensationTask
|
||||
{
|
||||
TaskType = input.TaskType,
|
||||
TaskType = (int)input.TaskType,
|
||||
BusinessSource = input.BusinessSource,
|
||||
BusinessId = input.BusinessId,
|
||||
UserId = input.UserId,
|
||||
@ -37,7 +38,7 @@ public class CompensationTaskService(
|
||||
ErrorSource = input.ErrorSource,
|
||||
RetryCount = 0,
|
||||
MaxRetries = input.MaxRetries > 0 ? input.MaxRetries : 3,
|
||||
Status = CompensationTaskStatus.Pending,
|
||||
Status = CompensationTaskStatusEnum.Pending,
|
||||
ScheduledAt = DateTime.Now,
|
||||
IsDeleted = false,
|
||||
CreatedBy = "System",
|
||||
@ -61,7 +62,7 @@ public class CompensationTaskService(
|
||||
var now = DateTime.Now;
|
||||
|
||||
var tasks = await taskRepository.Queryable()
|
||||
.Where(t => (t.Status == CompensationTaskStatus.Pending || t.Status == CompensationTaskStatus.Processing)
|
||||
.Where(t => (t.Status == CompensationTaskStatusEnum.Pending || t.Status == CompensationTaskStatusEnum.Processing)
|
||||
&& !t.IsDeleted
|
||||
&& (t.ScheduledAt == null || t.ScheduledAt <= now))
|
||||
.OrderBy(t => t.CreatedAt)
|
||||
@ -69,7 +70,7 @@ public class CompensationTaskService(
|
||||
.Select(t => new CompensationTaskOutput
|
||||
{
|
||||
Id = t.Id,
|
||||
TaskType = t.TaskType,
|
||||
TaskType = (CompensationTaskTypeEnum)t.TaskType,
|
||||
BusinessSource = t.BusinessSource,
|
||||
BusinessId = t.BusinessId,
|
||||
UserId = t.UserId,
|
||||
@ -97,11 +98,11 @@ public class CompensationTaskService(
|
||||
var query = taskRepository.Queryable()
|
||||
.Where(t => !t.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrEmpty(input.Status))
|
||||
query = query.Where(t => t.Status == input.Status);
|
||||
if (input.Status.HasValue)
|
||||
query = query.Where(t => t.Status == input.Status.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(input.TaskType))
|
||||
query = query.Where(t => t.TaskType == input.TaskType);
|
||||
if (input.TaskType.HasValue)
|
||||
query = query.Where(t => t.TaskType == (int)input.TaskType.Value);
|
||||
|
||||
if (!string.IsNullOrEmpty(input.BusinessSource))
|
||||
query = query.Where(t => t.BusinessSource == input.BusinessSource);
|
||||
@ -112,7 +113,7 @@ public class CompensationTaskService(
|
||||
.Select(t => new CompensationTaskOutput
|
||||
{
|
||||
Id = t.Id,
|
||||
TaskType = t.TaskType,
|
||||
TaskType = (CompensationTaskTypeEnum)t.TaskType,
|
||||
BusinessSource = t.BusinessSource,
|
||||
BusinessId = t.BusinessId,
|
||||
UserId = t.UserId,
|
||||
@ -162,7 +163,7 @@ public class CompensationTaskService(
|
||||
public async Task CancelTaskAsync(long taskId, string reason)
|
||||
{
|
||||
await taskRepository.Context.Updateable<CompensationTask>()
|
||||
.SetColumns(t => t.Status == CompensationTaskStatus.Cancelled)
|
||||
.SetColumns(t => t.Status == CompensationTaskStatusEnum.Cancelled)
|
||||
.SetColumns(t => t.ResultMessage == reason)
|
||||
.SetColumns(t => t.UpdatedAt == DateTime.Now)
|
||||
.Where(t => t.Id == taskId && !t.IsDeleted)
|
||||
|
||||
@ -4,6 +4,7 @@ using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -38,7 +39,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
Description = input.Description,
|
||||
ImageUrl = input.ImageUrl,
|
||||
SortOrder = input.SortOrder,
|
||||
Type = input.Type,
|
||||
Type = Enum.Parse<MedalTypeEnum>(input.Type, true),
|
||||
JournalId = input.JournalId,
|
||||
CreatedBy = "System",
|
||||
UpdatedBy = "System",
|
||||
@ -107,7 +108,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
medal.Description = input.Description;
|
||||
medal.ImageUrl = input.ImageUrl;
|
||||
medal.SortOrder = input.SortOrder;
|
||||
medal.Type = input.Type;
|
||||
medal.Type = Enum.Parse<MedalTypeEnum>(input.Type, true);
|
||||
medal.JournalId = input.JournalId;
|
||||
medal.UpdatedBy = "System";
|
||||
medal.UpdatedAt = DateTime.Now;
|
||||
@ -232,7 +233,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var medals = await medalRepository.Queryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), m => m.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), m => m.Type == input.Type)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), m => m.Type.ToString() == input.Type)
|
||||
.OrderBy(m => m.SortOrder)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
@ -313,7 +314,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
.ToListAsync();
|
||||
|
||||
var userMedals = await Context.Queryable<UserMedal>()
|
||||
.Where(um => um.UserId == userId && um.Status == "Awarded")
|
||||
.Where(um => um.UserId == userId && um.Status == UserMedalStatusEnum.Awarded)
|
||||
.ToListAsync();
|
||||
|
||||
var userMedalDict = userMedals.ToDictionary(um => um.MedalId, um => um.AwardedAt);
|
||||
@ -325,7 +326,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
Description = m.Description,
|
||||
ImageUrl = m.ImageUrl,
|
||||
SortOrder = m.SortOrder,
|
||||
Type = m.Type,
|
||||
Type = m.Type.ToString(),
|
||||
IsOwned = userMedalDict.ContainsKey((int)m.Id),
|
||||
AwardedAt = userMedalDict.TryGetValue((int)m.Id, out var awardedAt) ? awardedAt : null
|
||||
}).ToList();
|
||||
@ -340,7 +341,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
|
||||
var result = await Context.Queryable<UserMedal>()
|
||||
.InnerJoin<Medal>((um, m) => um.MedalId == m.Id)
|
||||
.Where((um, m) => um.UserId == userId && um.Status == "Awarded")
|
||||
.Where((um, m) => um.UserId == userId && um.Status == UserMedalStatusEnum.Awarded)
|
||||
.OrderByDescending((um, m) => um.AwardedAt)
|
||||
.Select((um, m) => new WxUserMedalOutput
|
||||
{
|
||||
@ -348,9 +349,9 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
Name = m.Name,
|
||||
Description = m.Description,
|
||||
ImageUrl = m.ImageUrl,
|
||||
Type = m.Type,
|
||||
Type = m.Type.ToString(),
|
||||
AwardedAt = um.AwardedAt,
|
||||
Status = um.Status
|
||||
Status = um.Status.ToString()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
@ -390,7 +391,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
}
|
||||
|
||||
var existingUserMedal = await Context.Queryable<UserMedal>()
|
||||
.Where(um => um.UserId == userId && um.MedalId == (int)input.MedalId && um.Status == "Awarded")
|
||||
.Where(um => um.UserId == userId && um.MedalId == (int)input.MedalId && um.Status == UserMedalStatusEnum.Awarded)
|
||||
.FirstAsync();
|
||||
|
||||
if (existingUserMedal != null)
|
||||
@ -403,8 +404,8 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
UserId = userId,
|
||||
MedalId = (int)input.MedalId,
|
||||
AwardedAt = DateTime.Now,
|
||||
Type = medal.Type,
|
||||
Status = "Awarded",
|
||||
Type = (UserMedalTypeEnum)medal.Type,
|
||||
Status = UserMedalStatusEnum.Awarded,
|
||||
CreatedBy = "System",
|
||||
UpdatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -472,7 +473,7 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
|
||||
Description = medal.Description,
|
||||
ImageUrl = medal.ImageUrl,
|
||||
SortOrder = medal.SortOrder,
|
||||
Type = medal.Type,
|
||||
Type = medal.Type.ToString(),
|
||||
JournalId = medal.JournalId,
|
||||
Rules = rules,
|
||||
CreatedBy = medal.CreatedBy,
|
||||
|
||||
145
QYZH.InteractiveMagazine.Service/OperationLogService.cs
Normal file
145
QYZH.InteractiveMagazine.Service/OperationLogService.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using Serilog.Core;
|
||||
using SqlSugar;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志服务实现
|
||||
/// </summary>
|
||||
public class OperationLogService(
|
||||
BaseRepository<OperationLog> operationLogRepository,
|
||||
BaseRepository<AdminUser> adminUserRepository,
|
||||
BaseRepository<Users> usersRepository,
|
||||
ILogger<OperationLogService> logger) : BaseRepository<OperationLog>, IOperationLogService
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 记录操作日志
|
||||
/// </summary>
|
||||
public async Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var log = new OperationLog
|
||||
{
|
||||
OperatorId = operatorId,
|
||||
OperatorName = operatorName,
|
||||
ActionType = actionType,
|
||||
TargetType = targetType,
|
||||
TargetId = targetId,
|
||||
TargetName = targetName,
|
||||
Detail = detail,
|
||||
IpAddress = ipAddress,
|
||||
IsDeleted = false,
|
||||
CreatedBy = operatorName,
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = operatorName,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
await operationLogRepository.InsertAsync(log);
|
||||
|
||||
logger.LogInformation(
|
||||
"记录操作日志,Operator: {Operator}, Action: {Action}, Target: {TargetType}/{TargetId}",
|
||||
operatorName, actionType, targetType, targetId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 日志记录不应影响主业务流程
|
||||
logger.LogError(ex, "记录操作日志失败,Operator: {Operator}, Action: {Action}", operatorName, actionType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询操作日志
|
||||
/// </summary>
|
||||
public async Task<PageListModel<OperationLogOutput>> GetListAsync(OperationLogQueryInput input)
|
||||
{
|
||||
if (input.PageIndex <= 0)
|
||||
input.PageIndex = 1;
|
||||
|
||||
if (input.PageSize <= 0 || input.PageSize > 100)
|
||||
input.PageSize = 10;
|
||||
|
||||
RefAsync<int> totalNumber = 0;
|
||||
|
||||
var pageResult = await operationLogRepository.Queryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.OperatorName), l => l.OperatorName.Contains(input.OperatorName))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.ActionType), l => l.ActionType == input.ActionType)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.TargetType), l => l.TargetType == input.TargetType)
|
||||
.WhereIF(input.TargetId.HasValue, l => l.TargetId == input.TargetId.Value)
|
||||
.OrderByDescending(l => l.CreatedAt)
|
||||
.Select(l => new OperationLogOutput
|
||||
{
|
||||
Id = l.Id,
|
||||
OperatorId = l.OperatorId,
|
||||
OperatorName = l.OperatorName,
|
||||
ActionType = l.ActionType,
|
||||
TargetType = l.TargetType,
|
||||
TargetId = l.TargetId,
|
||||
TargetName = l.TargetName,
|
||||
Detail = l.Detail,
|
||||
IpAddress = l.IpAddress,
|
||||
CreatedAt = l.CreatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
return new PageListModel<OperationLogOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取操作日志详情
|
||||
/// </summary>
|
||||
public async Task<OperationLogDetailOutput> GetDetailAsync(long id)
|
||||
{
|
||||
var log = await operationLogRepository.Queryable()
|
||||
.Where(l => l.Id == id && !l.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (log == null)
|
||||
{
|
||||
throw new BusinessException("操作日志记录不存在");
|
||||
}
|
||||
|
||||
var result = new OperationLogDetailOutput
|
||||
{
|
||||
Id = log.Id,
|
||||
ActionType = log.ActionType,
|
||||
TargetType = log.TargetType,
|
||||
TargetId = log.TargetId,
|
||||
TargetName = log.TargetName,
|
||||
Detail = log.Detail,
|
||||
IpAddress = log.IpAddress,
|
||||
CreatedAt = log.CreatedAt,
|
||||
OperatorName = log.OperatorName
|
||||
};
|
||||
|
||||
// 查询操作人详细信息
|
||||
var adminUser = await adminUserRepository.GetByIdAsync(log.OperatorId);
|
||||
if (adminUser != null)
|
||||
{
|
||||
result.OperatorRole = adminUser.Type.ToString();
|
||||
}
|
||||
|
||||
// 当目标类型为用户时,查询被操作人信息
|
||||
if (log.TargetType == OperationLogTargetType.User)
|
||||
{
|
||||
var targetUser = await usersRepository.GetByIdAsync(log.TargetId);
|
||||
if (targetUser != null)
|
||||
{
|
||||
result.TargetUserName = targetUser.Name;
|
||||
result.TargetUserPhone = targetUser.Phone;
|
||||
result.TargetUserAvatar = targetUser.AvatarUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -84,8 +85,8 @@ public class PetService(
|
||||
CurrentSkinId = pet.CurrentSkinId,
|
||||
CurrentSkinName = skinName,
|
||||
CurrentImages = images,
|
||||
Type = pet.Type,
|
||||
Status = pet.Status,
|
||||
Type = pet.Type.ToString(),
|
||||
Status = pet.Status.ToString(),
|
||||
CreatedAt = pet.CreatedAt
|
||||
};
|
||||
}
|
||||
@ -108,7 +109,7 @@ public class PetService(
|
||||
|
||||
// 查询默认宠物模板(取排序权重最低且状态为Active的模板)
|
||||
var defaultTemplate = await petTemplateRepository.Queryable()
|
||||
.Where(t => t.Status == "Active" && !t.IsDeleted)
|
||||
.Where(t => t.Status == PetTemplateStatusEnum.Active && !t.IsDeleted)
|
||||
.OrderBy(t => t.SortOrder)
|
||||
.FirstAsync();
|
||||
|
||||
@ -122,7 +123,7 @@ public class PetService(
|
||||
var initialEvolution = await petEvolutionRepository.Queryable()
|
||||
.Where(e => e.TemplateId == defaultTemplate.Id
|
||||
&& e.PreviousEvolutionId == null
|
||||
&& e.Status == "Active")
|
||||
&& e.Status == PetEvolutionStatusEnum.Active)
|
||||
.OrderBy(e => e.StageLevel)
|
||||
.FirstAsync();
|
||||
|
||||
@ -135,8 +136,8 @@ public class PetService(
|
||||
GrowthPoints = 0,
|
||||
FeedingCount = 0,
|
||||
CurrentSkinId = 0,
|
||||
Type = "Normal",
|
||||
Status = "Inactive",
|
||||
Type = UserPetTypeEnum.Normal,
|
||||
Status = UserPetStatusEnum.Inactive,
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -172,14 +173,14 @@ public class PetService(
|
||||
return;
|
||||
}
|
||||
|
||||
if (pet.Status != "Inactive")
|
||||
if (pet.Status != UserPetStatusEnum.Inactive)
|
||||
{
|
||||
logger.LogInformation("用户宠物已非未激活状态,跳过激活,UserId: {UserId}, Status: {Status}", userId, pet.Status);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await petRepository.UpdateAsync(
|
||||
p => new UserPet { Status = "Active" },
|
||||
p => new UserPet { Status = UserPetStatusEnum.Active },
|
||||
p => p.UserId == userId);
|
||||
|
||||
if (!result)
|
||||
@ -225,7 +226,7 @@ public class PetService(
|
||||
}
|
||||
|
||||
// 校验宠物状态
|
||||
if (pet.Status != "Active")
|
||||
if (pet.Status != UserPetStatusEnum.Active)
|
||||
{
|
||||
logger.LogWarning("喂养失败,宠物未激活,PetId: {PetId}, Status: {Status}", input.PetId, pet.Status);
|
||||
throw new BusinessException("宠物未激活,无法喂养", 400);
|
||||
@ -257,7 +258,7 @@ public class PetService(
|
||||
var nextEvolution = await petEvolutionRepository.Queryable()
|
||||
.Where(e => e.PreviousEvolutionId == pet.CurrentEvolutionId
|
||||
&& e.RequiredGrowth <= growthAfter
|
||||
&& e.Status == "Active")
|
||||
&& e.Status == PetEvolutionStatusEnum.Active)
|
||||
.OrderBy(e => e.RequiredGrowth, OrderByType.Desc)
|
||||
.FirstAsync();
|
||||
|
||||
@ -289,8 +290,8 @@ public class PetService(
|
||||
GrowthChange = input.GrowthPoints,
|
||||
GrowthBefore = growthBefore,
|
||||
GrowthAfter = growthAfter,
|
||||
Type = "Normal",
|
||||
Status = "Success",
|
||||
Type = PetFeedingRecordTypeEnum.Normal,
|
||||
Status = PetFeedingRecordStatusEnum.Success,
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -339,12 +340,663 @@ public class PetService(
|
||||
GrowthChange = r.GrowthChange,
|
||||
GrowthBefore = r.GrowthBefore,
|
||||
GrowthAfter = r.GrowthAfter,
|
||||
Type = r.Type,
|
||||
Status = r.Status,
|
||||
Type = r.Type.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
CreatedAt = r.CreatedAt
|
||||
}, true)
|
||||
.ToPageListAsync(pageQuery.PageIndex, pageQuery.PageSize, totalNumber);
|
||||
|
||||
return new PageListModel<FeedingRecordOutput>(records, pageQuery.PageIndex, pageQuery.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
// ==================== 后台管理:宠物模板 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建宠物模板
|
||||
/// </summary>
|
||||
public async Task<PetTemplateOutput> CreateTemplateAsync(PetTemplateInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Name))
|
||||
throw new BusinessException("模板名称不能为空", 400);
|
||||
|
||||
var template = new PetTemplate
|
||||
{
|
||||
Name = input.Name.Trim(),
|
||||
Description = input.Description,
|
||||
DefaultEvolutionId = input.DefaultEvolutionId,
|
||||
IconUrl = input.IconUrl,
|
||||
SortOrder = input.SortOrder,
|
||||
Type = Enum.Parse<PetTemplateTypeEnum>(input.Type, true),
|
||||
Status = PetTemplateStatusEnum.Active,
|
||||
CreatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = "System",
|
||||
UpdatedAt = DateTime.Now,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
var result = await petTemplateRepository.InsertAsync(template);
|
||||
if (!result)
|
||||
throw new BusinessException("创建宠物模板失败", 500);
|
||||
|
||||
logger.LogInformation("创建宠物模板成功,Id: {Id}, Name: {Name}", template.Id, template.Name);
|
||||
return BuildTemplateOutput(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新宠物模板
|
||||
/// </summary>
|
||||
public async Task<PetTemplateOutput> UpdateTemplateAsync(long id, PetTemplateInput input)
|
||||
{
|
||||
var template = await petTemplateRepository.GetByIdAsync(id);
|
||||
if (template == null || template.IsDeleted)
|
||||
throw new BusinessException("宠物模板不存在", 404);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.Name))
|
||||
throw new BusinessException("模板名称不能为空", 400);
|
||||
|
||||
template.Name = input.Name.Trim();
|
||||
template.Description = input.Description;
|
||||
template.DefaultEvolutionId = input.DefaultEvolutionId;
|
||||
template.IconUrl = input.IconUrl;
|
||||
template.SortOrder = input.SortOrder;
|
||||
template.Type = Enum.Parse<PetTemplateTypeEnum>(input.Type, true);
|
||||
template.UpdatedBy = "System";
|
||||
template.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await petTemplateRepository.UpdateAsync(template);
|
||||
if (!result)
|
||||
throw new BusinessException("更新宠物模板失败", 500);
|
||||
|
||||
logger.LogInformation("更新宠物模板成功,Id: {Id}", id);
|
||||
return BuildTemplateOutput(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除宠物模板(软删除,校验是否有用户宠物关联)
|
||||
/// </summary>
|
||||
public async Task DeleteTemplateAsync(long id)
|
||||
{
|
||||
var template = await petTemplateRepository.GetByIdAsync(id);
|
||||
if (template == null || template.IsDeleted)
|
||||
throw new BusinessException("宠物模板不存在", 404);
|
||||
|
||||
// 校验是否有用户宠物实例关联
|
||||
var hasUserPet = petRepository.Context.Queryable<UserPet>()
|
||||
.Any(p => p.TemplateId == id && !p.IsDeleted);
|
||||
if (hasUserPet)
|
||||
throw new BusinessException("该模板下存在用户宠物实例,无法删除", 400);
|
||||
|
||||
template.IsDeleted = true;
|
||||
template.UpdatedBy = "System";
|
||||
template.UpdatedAt = DateTime.Now;
|
||||
await petTemplateRepository.UpdateAsync(template);
|
||||
|
||||
logger.LogInformation("删除宠物模板成功,Id: {Id}", id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个宠物模板
|
||||
/// </summary>
|
||||
public async Task<PetTemplateOutput> GetTemplateByIdAsync(long id)
|
||||
{
|
||||
var template = await petTemplateRepository.GetByIdAsync(id);
|
||||
if (template == null || template.IsDeleted)
|
||||
throw new BusinessException("宠物模板不存在", 404);
|
||||
|
||||
return BuildTemplateOutput(template);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询宠物模板列表
|
||||
/// </summary>
|
||||
public async Task<PageListModel<PetTemplateOutput>> GetTemplatesAsync(PetTemplateQueryInput input)
|
||||
{
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var query = petTemplateRepository.Queryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), t => t.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), t => t.Type.ToString() == input.Type)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Status), t => t.Status.ToString() == input.Status)
|
||||
.OrderBy(t => t.SortOrder)
|
||||
.OrderByDescending(t => t.CreatedAt);
|
||||
|
||||
var list = await query
|
||||
.Select(t => new PetTemplateOutput
|
||||
{
|
||||
Id = t.Id,
|
||||
Name = t.Name,
|
||||
Description = t.Description,
|
||||
DefaultEvolutionId = t.DefaultEvolutionId,
|
||||
IconUrl = t.IconUrl,
|
||||
SortOrder = t.SortOrder,
|
||||
Type = t.Type.ToString(),
|
||||
Status = t.Status.ToString(),
|
||||
CreatedBy = t.CreatedBy,
|
||||
CreatedAt = t.CreatedAt,
|
||||
UpdatedBy = t.UpdatedBy,
|
||||
UpdatedAt = t.UpdatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
return new PageListModel<PetTemplateOutput>(list, input.PageIndex, input.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新模板状态(启用/禁用)
|
||||
/// </summary>
|
||||
public async Task UpdateTemplateStatusAsync(long id, string status)
|
||||
{
|
||||
var template = await petTemplateRepository.GetByIdAsync(id);
|
||||
if (template == null || template.IsDeleted)
|
||||
throw new BusinessException("宠物模板不存在", 404);
|
||||
|
||||
template.Status = Enum.Parse<PetTemplateStatusEnum>(status, true);
|
||||
template.UpdatedBy = "System";
|
||||
template.UpdatedAt = DateTime.Now;
|
||||
await petTemplateRepository.UpdateAsync(template);
|
||||
|
||||
logger.LogInformation("更新模板状态成功,Id: {Id}, Status: {Status}", id, status);
|
||||
}
|
||||
|
||||
private static PetTemplateOutput BuildTemplateOutput(PetTemplate t)
|
||||
{
|
||||
return new PetTemplateOutput
|
||||
{
|
||||
Id = t.Id,
|
||||
Name = t.Name,
|
||||
Description = t.Description,
|
||||
DefaultEvolutionId = t.DefaultEvolutionId,
|
||||
IconUrl = t.IconUrl,
|
||||
SortOrder = t.SortOrder,
|
||||
Type = t.Type.ToString(),
|
||||
Status = t.Status.ToString(),
|
||||
CreatedBy = t.CreatedBy,
|
||||
CreatedAt = t.CreatedAt,
|
||||
UpdatedBy = t.UpdatedBy,
|
||||
UpdatedAt = t.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 后台管理:进化链 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建进化阶段
|
||||
/// </summary>
|
||||
public async Task<PetEvolutionOutput> CreateEvolutionAsync(PetEvolutionInput input)
|
||||
{
|
||||
if (input.TemplateId <= 0)
|
||||
throw new BusinessException("模板Id不能为空", 400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.StageName))
|
||||
throw new BusinessException("阶段名称不能为空", 400);
|
||||
|
||||
// 校验模板是否存在
|
||||
var templateExists = petTemplateRepository.Context.Queryable<PetTemplate>()
|
||||
.Any(t => t.Id == input.TemplateId && !t.IsDeleted);
|
||||
if (!templateExists)
|
||||
throw new BusinessException("宠物模板不存在", 404);
|
||||
|
||||
var evolution = new PetEvolution
|
||||
{
|
||||
TemplateId = input.TemplateId,
|
||||
StageName = input.StageName.Trim(),
|
||||
StageLevel = input.StageLevel,
|
||||
RequiredGrowth = input.RequiredGrowth,
|
||||
PreviousEvolutionId = input.PreviousEvolutionId,
|
||||
BaseStrength = input.BaseStrength,
|
||||
BaseAgility = input.BaseAgility,
|
||||
BaseIntelligence = input.BaseIntelligence,
|
||||
BaseCharm = input.BaseCharm,
|
||||
Type = Enum.Parse<PetEvolutionTypeEnum>(input.Type, true),
|
||||
Status = PetEvolutionStatusEnum.Active,
|
||||
CreatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = "System",
|
||||
UpdatedAt = DateTime.Now,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
var result = await petEvolutionRepository.InsertAsync(evolution);
|
||||
if (!result)
|
||||
throw new BusinessException("创建进化阶段失败", 500);
|
||||
|
||||
logger.LogInformation("创建进化阶段成功,Id: {Id}, StageName: {StageName}", evolution.Id, evolution.StageName);
|
||||
return BuildEvolutionOutput(evolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新进化阶段
|
||||
/// </summary>
|
||||
public async Task<PetEvolutionOutput> UpdateEvolutionAsync(long id, PetEvolutionInput input)
|
||||
{
|
||||
var evolution = await petEvolutionRepository.GetByIdAsync(id);
|
||||
if (evolution == null || evolution.IsDeleted)
|
||||
throw new BusinessException("进化阶段不存在", 404);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.StageName))
|
||||
throw new BusinessException("阶段名称不能为空", 400);
|
||||
|
||||
evolution.TemplateId = input.TemplateId;
|
||||
evolution.StageName = input.StageName.Trim();
|
||||
evolution.StageLevel = input.StageLevel;
|
||||
evolution.RequiredGrowth = input.RequiredGrowth;
|
||||
evolution.PreviousEvolutionId = input.PreviousEvolutionId;
|
||||
evolution.BaseStrength = input.BaseStrength;
|
||||
evolution.BaseAgility = input.BaseAgility;
|
||||
evolution.BaseIntelligence = input.BaseIntelligence;
|
||||
evolution.BaseCharm = input.BaseCharm;
|
||||
evolution.Type = Enum.Parse<PetEvolutionTypeEnum>(input.Type, true);
|
||||
evolution.UpdatedBy = "System";
|
||||
evolution.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await petEvolutionRepository.UpdateAsync(evolution);
|
||||
if (!result)
|
||||
throw new BusinessException("更新进化阶段失败", 500);
|
||||
|
||||
logger.LogInformation("更新进化阶段成功,Id: {Id}", id);
|
||||
return BuildEvolutionOutput(evolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除进化阶段(软删除,校验是否有用户宠物处于该形态)
|
||||
/// </summary>
|
||||
public async Task DeleteEvolutionAsync(long id)
|
||||
{
|
||||
var evolution = await petEvolutionRepository.GetByIdAsync(id);
|
||||
if (evolution == null || evolution.IsDeleted)
|
||||
throw new BusinessException("进化阶段不存在", 404);
|
||||
|
||||
// 校验是否有用户宠物处于该形态
|
||||
var hasUserPet = petRepository.Context.Queryable<UserPet>()
|
||||
.Any(p => p.CurrentEvolutionId == id && !p.IsDeleted);
|
||||
if (hasUserPet)
|
||||
throw new BusinessException("有用户宠物正处于该形态,无法删除", 400);
|
||||
|
||||
evolution.IsDeleted = true;
|
||||
evolution.UpdatedBy = "System";
|
||||
evolution.UpdatedAt = DateTime.Now;
|
||||
await petEvolutionRepository.UpdateAsync(evolution);
|
||||
|
||||
logger.LogInformation("删除进化阶段成功,Id: {Id}", id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个进化阶段
|
||||
/// </summary>
|
||||
public async Task<PetEvolutionOutput> GetEvolutionByIdAsync(long id)
|
||||
{
|
||||
var evolution = await petEvolutionRepository.GetByIdAsync(id);
|
||||
if (evolution == null || evolution.IsDeleted)
|
||||
throw new BusinessException("进化阶段不存在", 404);
|
||||
|
||||
return BuildEvolutionOutput(evolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询进化阶段列表
|
||||
/// </summary>
|
||||
public async Task<PageListModel<PetEvolutionOutput>> GetEvolutionsAsync(PetEvolutionQueryInput input)
|
||||
{
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var query = petEvolutionRepository.Queryable()
|
||||
.Where(e => e.TemplateId == input.TemplateId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.StageName), e => e.StageName.Contains(input.StageName))
|
||||
.OrderBy(e => e.StageLevel);
|
||||
|
||||
var list = await query
|
||||
.Select(e => new PetEvolutionOutput
|
||||
{
|
||||
Id = e.Id,
|
||||
TemplateId = e.TemplateId,
|
||||
StageName = e.StageName,
|
||||
StageLevel = e.StageLevel,
|
||||
RequiredGrowth = e.RequiredGrowth,
|
||||
PreviousEvolutionId = e.PreviousEvolutionId,
|
||||
BaseStrength = e.BaseStrength,
|
||||
BaseAgility = e.BaseAgility,
|
||||
BaseIntelligence = e.BaseIntelligence,
|
||||
BaseCharm = e.BaseCharm,
|
||||
Type = e.Type.ToString(),
|
||||
Status = e.Status.ToString(),
|
||||
CreatedBy = e.CreatedBy,
|
||||
CreatedAt = e.CreatedAt,
|
||||
UpdatedBy = e.UpdatedBy,
|
||||
UpdatedAt = e.UpdatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
return new PageListModel<PetEvolutionOutput>(list, input.PageIndex, input.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
private static PetEvolutionOutput BuildEvolutionOutput(PetEvolution e)
|
||||
{
|
||||
return new PetEvolutionOutput
|
||||
{
|
||||
Id = e.Id,
|
||||
TemplateId = e.TemplateId,
|
||||
StageName = e.StageName,
|
||||
StageLevel = e.StageLevel,
|
||||
RequiredGrowth = e.RequiredGrowth,
|
||||
PreviousEvolutionId = e.PreviousEvolutionId,
|
||||
BaseStrength = e.BaseStrength,
|
||||
BaseAgility = e.BaseAgility,
|
||||
BaseIntelligence = e.BaseIntelligence,
|
||||
BaseCharm = e.BaseCharm,
|
||||
Type = e.Type.ToString(),
|
||||
Status = e.Status.ToString(),
|
||||
CreatedBy = e.CreatedBy,
|
||||
CreatedAt = e.CreatedAt,
|
||||
UpdatedBy = e.UpdatedBy,
|
||||
UpdatedAt = e.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 后台管理:皮肤 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建皮肤
|
||||
/// </summary>
|
||||
public async Task<PetSkinOutput> CreateSkinAsync(PetSkinInput input)
|
||||
{
|
||||
if (input.TemplateId <= 0)
|
||||
throw new BusinessException("模板Id不能为空", 400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.Name))
|
||||
throw new BusinessException("皮肤名称不能为空", 400);
|
||||
|
||||
// 校验模板是否存在
|
||||
var templateExists = petTemplateRepository.Context.Queryable<PetTemplate>()
|
||||
.Any(t => t.Id == input.TemplateId && !t.IsDeleted);
|
||||
if (!templateExists)
|
||||
throw new BusinessException("宠物模板不存在", 404);
|
||||
|
||||
var skin = new PetSkin
|
||||
{
|
||||
TemplateId = input.TemplateId,
|
||||
Name = input.Name.Trim(),
|
||||
Description = input.Description,
|
||||
Rarity = input.Rarity,
|
||||
SortOrder = input.SortOrder,
|
||||
Type = Enum.Parse<PetSkinTypeEnum>(input.Type, true),
|
||||
CreatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = "System",
|
||||
UpdatedAt = DateTime.Now,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
var result = await petSkinRepository.InsertAsync(skin);
|
||||
if (!result)
|
||||
throw new BusinessException("创建皮肤失败", 500);
|
||||
|
||||
logger.LogInformation("创建皮肤成功,Id: {Id}, Name: {Name}", skin.Id, skin.Name);
|
||||
return BuildSkinOutput(skin, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新皮肤
|
||||
/// </summary>
|
||||
public async Task<PetSkinOutput> UpdateSkinAsync(long id, PetSkinInput input)
|
||||
{
|
||||
var skin = await petSkinRepository.GetByIdAsync(id);
|
||||
if (skin == null || skin.IsDeleted)
|
||||
throw new BusinessException("皮肤不存在", 404);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.Name))
|
||||
throw new BusinessException("皮肤名称不能为空", 400);
|
||||
|
||||
skin.TemplateId = input.TemplateId;
|
||||
skin.Name = input.Name.Trim();
|
||||
skin.Description = input.Description;
|
||||
skin.Rarity = input.Rarity;
|
||||
skin.SortOrder = input.SortOrder;
|
||||
skin.Type = Enum.Parse<PetSkinTypeEnum>(input.Type, true);
|
||||
skin.UpdatedBy = "System";
|
||||
skin.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await petSkinRepository.UpdateAsync(skin);
|
||||
if (!result)
|
||||
throw new BusinessException("更新皮肤失败", 500);
|
||||
|
||||
logger.LogInformation("更新皮肤成功,Id: {Id}", id);
|
||||
|
||||
// 查询关联图片
|
||||
var images = await petSkinImageRepository.Queryable()
|
||||
.Where(i => i.SkinId == id && !i.IsDeleted)
|
||||
.OrderBy(i => i.SortOrder)
|
||||
.ToListAsync();
|
||||
|
||||
return BuildSkinOutput(skin, images);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除皮肤(软删除,校验是否有用户宠物装备中)
|
||||
/// </summary>
|
||||
public async Task DeleteSkinAsync(long id)
|
||||
{
|
||||
var skin = await petSkinRepository.GetByIdAsync(id);
|
||||
if (skin == null || skin.IsDeleted)
|
||||
throw new BusinessException("皮肤不存在", 404);
|
||||
|
||||
// 校验是否有用户宠物正在使用该皮肤
|
||||
var inUse = petRepository.Context.Queryable<UserPet>()
|
||||
.Any(p => p.CurrentSkinId == id && !p.IsDeleted);
|
||||
if (inUse)
|
||||
throw new BusinessException("有用户宠物正在使用该皮肤,无法删除", 400);
|
||||
|
||||
skin.IsDeleted = true;
|
||||
skin.UpdatedBy = "System";
|
||||
skin.UpdatedAt = DateTime.Now;
|
||||
await petSkinRepository.UpdateAsync(skin);
|
||||
|
||||
logger.LogInformation("删除皮肤成功,Id: {Id}", id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个皮肤(含图片列表)
|
||||
/// </summary>
|
||||
public async Task<PetSkinOutput> GetSkinByIdAsync(long id)
|
||||
{
|
||||
var skin = await petSkinRepository.GetByIdAsync(id);
|
||||
if (skin == null || skin.IsDeleted)
|
||||
throw new BusinessException("皮肤不存在", 404);
|
||||
|
||||
var images = await petSkinImageRepository.Queryable()
|
||||
.Where(i => i.SkinId == id && !i.IsDeleted)
|
||||
.OrderBy(i => i.SortOrder)
|
||||
.ToListAsync();
|
||||
|
||||
return BuildSkinOutput(skin, images);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询皮肤列表
|
||||
/// </summary>
|
||||
public async Task<PageListModel<PetSkinOutput>> GetSkinsAsync(PetSkinQueryInput input)
|
||||
{
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var query = petSkinRepository.Queryable()
|
||||
.Where(s => s.TemplateId == input.TemplateId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), s => s.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Rarity), s => s.Rarity == input.Rarity)
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.OrderByDescending(s => s.CreatedAt);
|
||||
|
||||
var list = await query
|
||||
.Select(s => new PetSkinOutput
|
||||
{
|
||||
Id = s.Id,
|
||||
TemplateId = s.TemplateId,
|
||||
Name = s.Name,
|
||||
Description = s.Description,
|
||||
Rarity = s.Rarity,
|
||||
SortOrder = s.SortOrder,
|
||||
Type = s.Type.ToString(),
|
||||
CreatedBy = s.CreatedBy,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedBy = s.UpdatedBy,
|
||||
UpdatedAt = s.UpdatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
// 批量查询预览图片
|
||||
var skinIds = list.Select(s => s.Id).ToList();
|
||||
if (skinIds.Count > 0)
|
||||
{
|
||||
var allImages = await petSkinImageRepository.Queryable()
|
||||
.Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted)
|
||||
.OrderBy(i => i.SortOrder)
|
||||
.ToListAsync();
|
||||
|
||||
var imageMap = allImages
|
||||
.GroupBy(i => i.SkinId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(i => BuildSkinImageOutput(i)).ToList());
|
||||
|
||||
foreach (var skin in list)
|
||||
{
|
||||
if (imageMap.ContainsKey(skin.Id))
|
||||
skin.Images = imageMap[skin.Id];
|
||||
}
|
||||
}
|
||||
|
||||
return new PageListModel<PetSkinOutput>(list, input.PageIndex, input.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
private static PetSkinOutput BuildSkinOutput(PetSkin s, List<PetSkinImage>? images)
|
||||
{
|
||||
return new PetSkinOutput
|
||||
{
|
||||
Id = s.Id,
|
||||
TemplateId = s.TemplateId,
|
||||
Name = s.Name,
|
||||
Description = s.Description,
|
||||
Rarity = s.Rarity,
|
||||
SortOrder = s.SortOrder,
|
||||
Type = s.Type.ToString(),
|
||||
CreatedBy = s.CreatedBy,
|
||||
CreatedAt = s.CreatedAt,
|
||||
UpdatedBy = s.UpdatedBy,
|
||||
UpdatedAt = s.UpdatedAt,
|
||||
Images = images?.Select(BuildSkinImageOutput).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 后台管理:皮肤图片 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建皮肤图片
|
||||
/// </summary>
|
||||
public async Task<PetSkinImageOutput> CreateSkinImageAsync(PetSkinImageInput input)
|
||||
{
|
||||
if (input.SkinId <= 0)
|
||||
throw new BusinessException("皮肤Id不能为空", 400);
|
||||
|
||||
if (input.EvolutionStageId <= 0)
|
||||
throw new BusinessException("进化阶段Id不能为空", 400);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.ImageUrl))
|
||||
throw new BusinessException("图片地址不能为空", 400);
|
||||
|
||||
// 校验皮肤是否存在
|
||||
var skinExists = petSkinRepository.Context.Queryable<PetSkin>()
|
||||
.Any(s => s.Id == input.SkinId && !s.IsDeleted);
|
||||
if (!skinExists)
|
||||
throw new BusinessException("皮肤不存在", 404);
|
||||
|
||||
var image = new PetSkinImage
|
||||
{
|
||||
SkinId = input.SkinId,
|
||||
EvolutionStageId = input.EvolutionStageId,
|
||||
ImageUrl = input.ImageUrl.Trim(),
|
||||
SortOrder = input.SortOrder,
|
||||
Type = Enum.Parse<PetSkinImageTypeEnum>(input.Type, true),
|
||||
CreatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = "System",
|
||||
UpdatedAt = DateTime.Now,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
var result = await petSkinImageRepository.InsertAsync(image);
|
||||
if (!result)
|
||||
throw new BusinessException("创建皮肤图片失败", 500);
|
||||
|
||||
logger.LogInformation("创建皮肤图片成功,Id: {Id}, SkinId: {SkinId}", image.Id, image.SkinId);
|
||||
return BuildSkinImageOutput(image);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新皮肤图片
|
||||
/// </summary>
|
||||
public async Task<PetSkinImageOutput> UpdateSkinImageAsync(long id, PetSkinImageInput input)
|
||||
{
|
||||
var image = await petSkinImageRepository.GetByIdAsync(id);
|
||||
if (image == null || image.IsDeleted)
|
||||
throw new BusinessException("皮肤图片不存在", 404);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.ImageUrl))
|
||||
throw new BusinessException("图片地址不能为空", 400);
|
||||
|
||||
image.SkinId = input.SkinId;
|
||||
image.EvolutionStageId = input.EvolutionStageId;
|
||||
image.ImageUrl = input.ImageUrl.Trim();
|
||||
image.SortOrder = input.SortOrder;
|
||||
image.Type = Enum.Parse<PetSkinImageTypeEnum>(input.Type, true);
|
||||
image.UpdatedBy = "System";
|
||||
image.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await petSkinImageRepository.UpdateAsync(image);
|
||||
if (!result)
|
||||
throw new BusinessException("更新皮肤图片失败", 500);
|
||||
|
||||
logger.LogInformation("更新皮肤图片成功,Id: {Id}", id);
|
||||
return BuildSkinImageOutput(image);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除皮肤图片(软删除)
|
||||
/// </summary>
|
||||
public async Task DeleteSkinImageAsync(long id)
|
||||
{
|
||||
var image = await petSkinImageRepository.GetByIdAsync(id);
|
||||
if (image == null || image.IsDeleted)
|
||||
throw new BusinessException("皮肤图片不存在", 404);
|
||||
|
||||
image.IsDeleted = true;
|
||||
image.UpdatedBy = "System";
|
||||
image.UpdatedAt = DateTime.Now;
|
||||
await petSkinImageRepository.UpdateAsync(image);
|
||||
|
||||
logger.LogInformation("删除皮肤图片成功,Id: {Id}", id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定皮肤的所有图片
|
||||
/// </summary>
|
||||
public async Task<List<PetSkinImageOutput>> GetSkinImagesBySkinIdAsync(long skinId)
|
||||
{
|
||||
var images = await petSkinImageRepository.Queryable()
|
||||
.Where(i => i.SkinId == skinId && !i.IsDeleted)
|
||||
.OrderBy(i => i.SortOrder)
|
||||
.ToListAsync();
|
||||
|
||||
return images.Select(BuildSkinImageOutput).ToList();
|
||||
}
|
||||
|
||||
private static PetSkinImageOutput BuildSkinImageOutput(PetSkinImage i)
|
||||
{
|
||||
return new PetSkinImageOutput
|
||||
{
|
||||
Id = i.Id,
|
||||
SkinId = i.SkinId,
|
||||
EvolutionStageId = i.EvolutionStageId,
|
||||
ImageUrl = i.ImageUrl,
|
||||
SortOrder = i.SortOrder,
|
||||
Type = i.Type.ToString(),
|
||||
CreatedBy = i.CreatedBy,
|
||||
CreatedAt = i.CreatedAt,
|
||||
UpdatedBy = i.UpdatedBy,
|
||||
UpdatedAt = i.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
274
QYZH.InteractiveMagazine.Service/PointsService.cs
Normal file
274
QYZH.InteractiveMagazine.Service/PointsService.cs
Normal file
@ -0,0 +1,274 @@
|
||||
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", 400);
|
||||
|
||||
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", 400);
|
||||
|
||||
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", 400);
|
||||
|
||||
// 查询用户当前积分
|
||||
var user = await Context.Queryable<Users>()
|
||||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (user == null)
|
||||
throw new BusinessException("用户不存在", 404);
|
||||
|
||||
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,
|
||||
Status = PointsRecordStatusEnum.Success,
|
||||
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)
|
||||
throw new BusinessException("扣除积分数量必须大于0", 400);
|
||||
|
||||
// 查询用户当前积分
|
||||
var user = await Context.Queryable<Users>()
|
||||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (user == null)
|
||||
throw new BusinessException("用户不存在", 404);
|
||||
|
||||
var previousBalance = user.Points;
|
||||
|
||||
// 余额不足校验
|
||||
if (previousBalance < input.Amount)
|
||||
throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {previousBalance}", 400);
|
||||
|
||||
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,
|
||||
Status = PointsRecordStatusEnum.Success,
|
||||
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)
|
||||
throw new BusinessException("用户不存在", 404);
|
||||
|
||||
// 查询累计收入(Income 类型)
|
||||
var totalIncome = await Context.Queryable<PointsRecord>()
|
||||
.Where(r => r.UserId == userId && !r.IsDeleted && r.Type == PointsFlowTypeEnum.Income && r.Status == 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 == 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", 400);
|
||||
|
||||
if (input.PageSize <= 0 || input.PageSize > 100)
|
||||
throw new BusinessException("每页条数必须在1-100之间", 400);
|
||||
|
||||
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
|
||||
}
|
||||
@ -3,6 +3,7 @@ using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -42,7 +43,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
Description = input.Description,
|
||||
ImageUrl = input.ImageUrl,
|
||||
Price = input.Price,
|
||||
Type = input.Type,
|
||||
Type = Enum.Parse<ProductTypeEnum>(input.Type, true),
|
||||
SaleStatus = input.SaleStatus,
|
||||
MetaData = input.MetaData,
|
||||
IsActive = input.IsActive,
|
||||
@ -70,7 +71,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
Description = product.Description,
|
||||
ImageUrl = product.ImageUrl,
|
||||
Price = product.Price,
|
||||
Type = product.Type,
|
||||
Type = product.Type.ToString(),
|
||||
SaleStatus = product.SaleStatus,
|
||||
MetaData = product.MetaData,
|
||||
IsActive = product.IsActive,
|
||||
@ -115,7 +116,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
product.Description = input.Description;
|
||||
product.ImageUrl = input.ImageUrl;
|
||||
product.Price = input.Price;
|
||||
product.Type = input.Type;
|
||||
product.Type = Enum.Parse<ProductTypeEnum>(input.Type, true);
|
||||
product.SaleStatus = input.SaleStatus;
|
||||
product.MetaData = input.MetaData;
|
||||
product.IsActive = input.IsActive;
|
||||
@ -139,7 +140,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
Description = product.Description,
|
||||
ImageUrl = product.ImageUrl,
|
||||
Price = product.Price,
|
||||
Type = product.Type,
|
||||
Type = product.Type.ToString(),
|
||||
SaleStatus = product.SaleStatus,
|
||||
MetaData = product.MetaData,
|
||||
IsActive = product.IsActive,
|
||||
@ -196,7 +197,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
Description = product.Description,
|
||||
ImageUrl = product.ImageUrl,
|
||||
Price = product.Price,
|
||||
Type = product.Type,
|
||||
Type = product.Type.ToString(),
|
||||
SaleStatus = product.SaleStatus,
|
||||
MetaData = product.MetaData,
|
||||
IsActive = product.IsActive,
|
||||
@ -228,7 +229,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var pageResult = await productRepository.Queryable()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), p => p.Name.Contains(input.Name))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), p => p.Type == input.Type)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), p => p.Type.ToString() == input.Type)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.SaleStatus), p => p.SaleStatus == input.SaleStatus)
|
||||
.WhereIF(input.IsActive.HasValue, p => p.IsActive == input.IsActive.Value)
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
@ -239,7 +240,7 @@ public class ProductService(BaseRepository<Product> productRepository, ILogger<P
|
||||
Description = p.Description,
|
||||
ImageUrl = p.ImageUrl,
|
||||
Price = p.Price,
|
||||
Type = p.Type,
|
||||
Type = p.Type.ToString(),
|
||||
SaleStatus = p.SaleStatus,
|
||||
MetaData = p.MetaData,
|
||||
IsActive = p.IsActive,
|
||||
|
||||
@ -3,6 +3,7 @@ using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -49,7 +50,7 @@ public class UserJournalService(
|
||||
}
|
||||
|
||||
// 校验期刊状态
|
||||
if (journal.Status != "Published")
|
||||
if (journal.Status != JournalStatusEnum.Published)
|
||||
{
|
||||
logger.LogWarning("绑定期刊失败,期刊未发布,JournalId: {JournalId}, Status: {Status}", input.JournalId, journal.Status);
|
||||
throw new BusinessException("该期刊暂未发布,无法绑定", 400);
|
||||
@ -75,8 +76,8 @@ public class UserJournalService(
|
||||
UserId = userId,
|
||||
JournalId = input.JournalId,
|
||||
Id = input.Id,
|
||||
Type = input.Type,
|
||||
Status = "Active",
|
||||
Type = Enum.Parse<UserJournalTypeEnum>(input.Type, true),
|
||||
Status = UserJournalStatusEnum.Active,
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -112,8 +113,8 @@ public class UserJournalService(
|
||||
Id = userJournal.Id,
|
||||
UserId = userJournal.UserId,
|
||||
JournalId = userJournal.JournalId,
|
||||
Type = userJournal.Type,
|
||||
Status = userJournal.Status,
|
||||
Type = userJournal.Type.ToString(),
|
||||
Status = userJournal.Status.ToString(),
|
||||
CreatedAt = userJournal.CreatedAt
|
||||
};
|
||||
}
|
||||
@ -139,15 +140,15 @@ public class UserJournalService(
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var pageResult = await userJournalRepository.Queryable()
|
||||
.Where(uj => uj.UserId == userId)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type == input.Type)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type.ToString() == input.Type)
|
||||
.OrderByDescending(uj => uj.CreatedAt)
|
||||
.Select(uj => new BindJournalOutput
|
||||
{
|
||||
Id = uj.Id,
|
||||
UserId = uj.UserId,
|
||||
JournalId = uj.JournalId,
|
||||
Type = uj.Type,
|
||||
Status = uj.Status,
|
||||
Type = uj.Type.ToString(),
|
||||
Status = uj.Status.ToString(),
|
||||
CreatedAt = uj.CreatedAt
|
||||
}, true)
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
@ -1,15 +1,27 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
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;
|
||||
|
||||
public class UsersService(BaseRepository<Users> usersRepository, ILogger<UsersService> _logger) : BaseRepository<Users>, IUsersService
|
||||
public class UsersService(
|
||||
BaseRepository<Users> usersRepository,
|
||||
ILogger<UsersService> _logger,
|
||||
IPointsService pointsService,
|
||||
ICheckInService checkInService,
|
||||
ICompensationTaskService compensationTaskService,
|
||||
IUserJournalService userJournalService,
|
||||
IOperationLogService operationLogService) : BaseRepository<Users>, IUsersService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询用户列表
|
||||
@ -25,16 +37,134 @@ public class UsersService(BaseRepository<Users> usersRepository, ILogger<UsersSe
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户详情
|
||||
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表)
|
||||
/// </summary>
|
||||
public async Task<BaseResponse<UsersOutput>> GetDetailAsync(long id)
|
||||
public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id)
|
||||
{
|
||||
var user = await GetByIdAsync<UsersOutput>(u => u.Id == id);
|
||||
if (user == null)
|
||||
{
|
||||
return BaseResponse<UsersOutput>.Fail("用户不存在");
|
||||
return BaseResponse<UserDetailOutput>.Fail("用户不存在");
|
||||
}
|
||||
|
||||
// 并行查询关联数据
|
||||
var pointsTask = GetPointsRecordsAsync(id);
|
||||
var checkInTask = GetCheckInRecordsAsync(id);
|
||||
var compensationTask = GetFailedCompensationTasksAsync(id);
|
||||
var journalsTask = GetUserJournalsWithDetailAsync(id);
|
||||
|
||||
await Task.WhenAll(pointsTask, checkInTask, compensationTask, journalsTask);
|
||||
|
||||
return BaseResponse<UserDetailOutput>.Success(new UserDetailOutput
|
||||
{
|
||||
BasicInfo = user,
|
||||
PointsRecords = await pointsTask,
|
||||
CheckInRecords = await checkInTask,
|
||||
FailedCompensationTasks = await compensationTask,
|
||||
Journals = await journalsTask
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户积分记录(最近20条)
|
||||
/// </summary>
|
||||
private async Task<List<PointsRecordOutput>> GetPointsRecordsAsync(long userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await pointsService.GetPointsRecordsAsync(new PointsRecordQueryInput
|
||||
{
|
||||
UserId = userId,
|
||||
PageIndex = 1,
|
||||
PageSize = 20
|
||||
});
|
||||
return result.Result ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户积分记录失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户签到记录
|
||||
/// </summary>
|
||||
private async Task<List<CheckInRecordOutput>> GetCheckInRecordsAsync(long userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await checkInService.GetCheckInInfoAsync(userId);
|
||||
return info.RecentRecords ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户签到记录失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户失败的补偿任务(需要手动处理)
|
||||
/// </summary>
|
||||
private async Task<List<CompensationTaskOutput>> GetFailedCompensationTasksAsync(long userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tasks = await compensationTaskService.GetTasksAsync(new GetCompensationTasksInput
|
||||
{
|
||||
Status = CompensationTaskStatusEnum.Failed,
|
||||
Limit = 50
|
||||
});
|
||||
return tasks.Where(t => t.UserId == userId).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户失败补偿任务失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户拥有的期刊列表(含期刊详情)
|
||||
/// </summary>
|
||||
private async Task<List<UserJournalItemOutput>> GetUserJournalsWithDetailAsync(long userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userJournals = await Context.Queryable<UserJournal>()
|
||||
.Where(uj => uj.UserId == userId && !uj.IsDeleted)
|
||||
.OrderByDescending(uj => uj.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new List<UserJournalItemOutput>();
|
||||
foreach (var uj in userJournals)
|
||||
{
|
||||
var journal = await Context.Queryable<Journal>()
|
||||
.Where(j => j.Id == uj.JournalId && !j.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (journal != null)
|
||||
{
|
||||
result.Add(new UserJournalItemOutput
|
||||
{
|
||||
BindId = uj.Id,
|
||||
JournalId = uj.JournalId,
|
||||
JournalTitle = journal.Title,
|
||||
CoverImageUrl = journal.CoverImageUrl,
|
||||
Type = uj.Type.ToString(),
|
||||
Status = uj.Status.ToString(),
|
||||
CreatedAt = uj.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户期刊列表失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
return BaseResponse<UsersOutput>.Success(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -48,7 +178,7 @@ public class UsersService(BaseRepository<Users> usersRepository, ILogger<UsersSe
|
||||
return BaseResponse.Fail("用户不存在");
|
||||
}
|
||||
|
||||
var statusValue = input.Status == 1 ? "Active" : "Disabled";
|
||||
var statusValue = input.Status == 1 ? UserStatusEnum.Active : UserStatusEnum.Disabled;
|
||||
var result = await UpdateAsync(
|
||||
u => new Users { Status = statusValue },
|
||||
u => u.Id == id
|
||||
@ -56,4 +186,104 @@ public class UsersService(BaseRepository<Users> usersRepository, ILogger<UsersSe
|
||||
|
||||
return result ? BaseResponse.Success() : BaseResponse.Fail("更新失败");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动增加用户积分
|
||||
/// </summary>
|
||||
public async Task<ManualPointsOutput> ManualAddPointsAsync(long userId, ManualAddPointsInput input, long operatorId, string operatorName, string? ipAddress = null)
|
||||
{
|
||||
_logger.LogInformation("管理员手动增加积分,UserId: {UserId}, Amount: {Amount}, Operator: {Operator}",
|
||||
userId, input.Amount, operatorName);
|
||||
|
||||
// 校验用户是否存在
|
||||
var user = await Queryable().Where(u => u.Id == userId && !u.IsDeleted).FirstAsync();
|
||||
if (user == null)
|
||||
throw new BusinessException("用户不存在", 404);
|
||||
|
||||
// 调用积分服务增加积分
|
||||
var result = await pointsService.AddPointsAsync(new AddPointsInput
|
||||
{
|
||||
UserId = userId,
|
||||
Amount = input.Amount,
|
||||
ChangeType = PointsChangeTypeEnum.ManualAdjust,
|
||||
Description = $"管理员手动增加: {input.Reason}",
|
||||
OperatorName = operatorName
|
||||
});
|
||||
|
||||
// 记录操作日志
|
||||
var detail = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Amount = input.Amount,
|
||||
Reason = input.Reason,
|
||||
PreviousBalance = result.PreviousBalance,
|
||||
NewBalance = result.NewBalance,
|
||||
RecordId = result.RecordId
|
||||
});
|
||||
|
||||
await operationLogService.LogAsync(
|
||||
operatorId, operatorName,
|
||||
OperationLogActionType.ManualAddPoints,
|
||||
OperationLogTargetType.User,
|
||||
userId, user.Name, detail, ipAddress);
|
||||
|
||||
return new ManualPointsOutput
|
||||
{
|
||||
RecordId = result.RecordId,
|
||||
PreviousBalance = result.PreviousBalance,
|
||||
NewBalance = result.NewBalance,
|
||||
ChangeAmount = input.Amount,
|
||||
OperatorName = operatorName,
|
||||
OperatedAt = DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动扣除用户积分
|
||||
/// </summary>
|
||||
public async Task<ManualPointsOutput> ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null)
|
||||
{
|
||||
_logger.LogInformation("管理员手动扣除积分,UserId: {UserId}, Amount: {Amount}, Operator: {Operator}",
|
||||
userId, input.Amount, operatorName);
|
||||
|
||||
// 校验用户是否存在
|
||||
var user = await Queryable().Where(u => u.Id == userId && !u.IsDeleted).FirstAsync();
|
||||
if (user == null)
|
||||
throw new BusinessException("用户不存在", 404);
|
||||
|
||||
// 调用积分服务扣除积分
|
||||
var result = await pointsService.DeductPointsAsync(new DeductPointsInput
|
||||
{
|
||||
UserId = userId,
|
||||
Amount = input.Amount,
|
||||
ChangeType = PointsChangeTypeEnum.ManualAdjust,
|
||||
Description = $"管理员手动扣除: {input.Reason}",
|
||||
OperatorName = operatorName
|
||||
});
|
||||
|
||||
// 记录操作日志
|
||||
var detail = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Amount = input.Amount,
|
||||
Reason = input.Reason,
|
||||
PreviousBalance = result.PreviousBalance,
|
||||
NewBalance = result.NewBalance,
|
||||
RecordId = result.RecordId
|
||||
});
|
||||
|
||||
await operationLogService.LogAsync(
|
||||
operatorId, operatorName,
|
||||
OperationLogActionType.ManualDeductPoints,
|
||||
OperationLogTargetType.User,
|
||||
userId, user.Name, detail, ipAddress);
|
||||
|
||||
return new ManualPointsOutput
|
||||
{
|
||||
RecordId = result.RecordId,
|
||||
PreviousBalance = result.PreviousBalance,
|
||||
NewBalance = result.NewBalance,
|
||||
ChangeAmount = -input.Amount,
|
||||
OperatorName = operatorName,
|
||||
OperatedAt = DateTime.Now
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ using QYZH.InteractiveMagazine.Infrastructure.Cache;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using QYZH.InteractiveMagazine.Models.WeChat;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
@ -70,8 +71,8 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
||||
OpenId = wxResponse.OpenId,
|
||||
UnionId = wxResponse.UnionId,
|
||||
Phone = phone,
|
||||
Type = "Normal",
|
||||
Status = "Active",
|
||||
Type = UsersTypeEnum.Normal,
|
||||
Status = UserStatusEnum.Active,
|
||||
GrowthPoints = 0,
|
||||
Points = 0,
|
||||
IsLastOnline = true
|
||||
@ -189,7 +190,7 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
||||
throw new BusinessException("无法切换到该用户", 403);
|
||||
}
|
||||
|
||||
if (targetUser.Status == "Disabled")
|
||||
if (targetUser.Status == UserStatusEnum.Disabled)
|
||||
{
|
||||
throw new BusinessException("目标账号已被禁用", 403);
|
||||
}
|
||||
@ -345,8 +346,8 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
||||
Phone = user.Phone,
|
||||
Points = user.Points,
|
||||
GrowthPoints = user.GrowthPoints,
|
||||
Type = user.Type,
|
||||
Status = user.Status,
|
||||
Type = user.Type.ToString(),
|
||||
Status = user.Status.ToString(),
|
||||
IsLastOnline = user.IsLastOnline,
|
||||
CreatedAt = user.CreatedAt,
|
||||
UpdatedAt = user.UpdatedAt,
|
||||
|
||||
@ -3,6 +3,7 @@ using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
@ -79,7 +80,7 @@ public class WeChatCommunityService(
|
||||
UserId = m.UserId,
|
||||
Content = m.Content,
|
||||
ImageUrl = m.ImageUrl,
|
||||
Type = m.Type,
|
||||
Type = m.Type.ToString(),
|
||||
LikeCount = m.LikeCount,
|
||||
IsFeatured = m.IsFeatured,
|
||||
IsLiked = likedIds.Contains(m.Id),
|
||||
@ -126,7 +127,7 @@ public class WeChatCommunityService(
|
||||
|
||||
// 检查是否已点赞
|
||||
var existingLike = await Context.Queryable<CommunityMessageLike>()
|
||||
.Where(l => l.UserId == userId && l.MessageId == input.MessageId && l.Type == "Like")
|
||||
.Where(l => l.UserId == userId && l.MessageId == input.MessageId && l.Type == CommunityMessageLikeTypeEnum.Like)
|
||||
.FirstAsync();
|
||||
|
||||
if (existingLike != null)
|
||||
@ -139,7 +140,7 @@ public class WeChatCommunityService(
|
||||
{
|
||||
UserId = userId,
|
||||
MessageId = input.MessageId,
|
||||
Type = "Like",
|
||||
Type = CommunityMessageLikeTypeEnum.Like,
|
||||
CreatedBy = "System",
|
||||
UpdatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -192,7 +193,7 @@ public class WeChatCommunityService(
|
||||
}
|
||||
|
||||
var existingLike = await Context.Queryable<CommunityMessageLike>()
|
||||
.Where(l => l.UserId == userId && l.MessageId == messageId && l.Type == "Like")
|
||||
.Where(l => l.UserId == userId && l.MessageId == messageId && l.Type == CommunityMessageLikeTypeEnum.Like)
|
||||
.FirstAsync();
|
||||
|
||||
if (existingLike == null)
|
||||
@ -235,7 +236,7 @@ public class WeChatCommunityService(
|
||||
private async Task<List<long>> GetUserJournalIds(long userId)
|
||||
{
|
||||
return await Context.Queryable<UserJournal>()
|
||||
.Where(uj => uj.UserId == userId && uj.Status == "Active")
|
||||
.Where(uj => uj.UserId == userId && uj.Status == UserJournalStatusEnum.Active)
|
||||
.Select(uj => uj.JournalId)
|
||||
.ToListAsync();
|
||||
}
|
||||
@ -248,7 +249,7 @@ public class WeChatCommunityService(
|
||||
if (!messageIds.Any()) return new HashSet<long>();
|
||||
|
||||
var likes = await Context.Queryable<CommunityMessageLike>()
|
||||
.Where(l => l.UserId == userId && messageIds.Contains(l.MessageId) && l.Type == "Like" && !l.IsDeleted)
|
||||
.Where(l => l.UserId == userId && messageIds.Contains(l.MessageId) && l.Type == CommunityMessageLikeTypeEnum.Like && !l.IsDeleted)
|
||||
.Select(l => l.MessageId)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
@ -3,7 +3,9 @@ using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Bag;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Mall;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Points;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using System.Text.Json;
|
||||
|
||||
@ -15,6 +17,7 @@ namespace QYZH.InteractiveMagazine.Service;
|
||||
public class WxMallService(
|
||||
BaseRepository<ExchangeRecord> exchangeRecordRepository,
|
||||
ICheckInService checkInService,
|
||||
IPointsService pointsService,
|
||||
ILogger<WxMallService> logger)
|
||||
: BaseRepository<ExchangeRecord>, IWxMallService
|
||||
{
|
||||
@ -43,14 +46,14 @@ public class WxMallService(
|
||||
{
|
||||
var query = exchangeRecordRepository.Context.Queryable<Product>()
|
||||
.Where(p => !p.IsDeleted && p.IsActive && p.SaleStatus == "OnSale")
|
||||
.WhereIF(!string.IsNullOrEmpty(type), p => p.Type == type)
|
||||
.WhereIF(!string.IsNullOrEmpty(type), p => p.Type.ToString() == type)
|
||||
.OrderByDescending(p => p.CreatedAt);
|
||||
|
||||
var products = await query.ToListAsync();
|
||||
|
||||
// 批量提取 PetBg 商品的 SkinId
|
||||
var skinProductMap = products
|
||||
.Where(p => p.Type == "PetBg")
|
||||
.Where(p => p.Type == ProductTypeEnum.PetBg)
|
||||
.Select(p => new { ProductId = p.Id, SkinId = GetSkinIdFromMetaData(p.MetaData) })
|
||||
.Where(x => x.SkinId > 0)
|
||||
.ToList();
|
||||
@ -80,7 +83,7 @@ public class WxMallService(
|
||||
var skinProductIds = skinProductMap.Select(x => x.ProductId).ToList();
|
||||
var bagItems = skinProductIds.Count > 0
|
||||
? await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "Available"
|
||||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available
|
||||
&& skinProductIds.Contains(b.ItemId))
|
||||
.ToListAsync()
|
||||
: new List<UserBag>();
|
||||
@ -99,7 +102,7 @@ public class WxMallService(
|
||||
Description = p.Description,
|
||||
ImageUrl = p.ImageUrl,
|
||||
Price = p.Price,
|
||||
Type = p.Type,
|
||||
Type = p.Type.ToString(),
|
||||
Owned = owned,
|
||||
Skin = skin != null ? new PetSkinBrief
|
||||
{
|
||||
@ -125,7 +128,7 @@ public class WxMallService(
|
||||
if (product == null) return null;
|
||||
|
||||
PetSkinBrief? skinBrief = null;
|
||||
if (product.Type == "PetBg")
|
||||
if (product.Type == ProductTypeEnum.PetBg)
|
||||
{
|
||||
var skinId = GetSkinIdFromMetaData(product.MetaData);
|
||||
if (skinId > 0)
|
||||
@ -155,7 +158,7 @@ public class WxMallService(
|
||||
}
|
||||
|
||||
var owned = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == "Available")
|
||||
.Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == UserBagStatusEnum.Available)
|
||||
.AnyAsync();
|
||||
|
||||
return new WxProductOutput
|
||||
@ -165,7 +168,7 @@ public class WxMallService(
|
||||
Description = product.Description,
|
||||
ImageUrl = product.ImageUrl,
|
||||
Price = product.Price,
|
||||
Type = product.Type,
|
||||
Type = product.Type.ToString(),
|
||||
Owned = owned,
|
||||
Skin = skinBrief
|
||||
};
|
||||
@ -195,40 +198,22 @@ public class WxMallService(
|
||||
|
||||
var totalCost = product.Price * input.Quantity;
|
||||
|
||||
// 查询用户积分
|
||||
var user = await exchangeRecordRepository.Context.Queryable<Users>()
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (user == null)
|
||||
throw new BusinessException("用户不存在", 404);
|
||||
|
||||
if (user.Points < totalCost)
|
||||
throw new BusinessException($"积分不足,需要 {totalCost} 积分,当前余额 {user.Points}", 400);
|
||||
|
||||
var newPointsBalance = user.Points - totalCost;
|
||||
DeductPointsOutput? pointsResult = null;
|
||||
long recordId = 0;
|
||||
|
||||
await exchangeRecordRepository.UseTranAsync(async () =>
|
||||
{
|
||||
// 扣除用户积分
|
||||
await exchangeRecordRepository.Context.Updateable<Users>()
|
||||
.SetColumns(u => u.Points == newPointsBalance)
|
||||
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 创建兑换记录
|
||||
var record = new ExchangeRecord
|
||||
{
|
||||
UserId = userId,
|
||||
ProductId = product.Id,
|
||||
ProductName = product.Name,
|
||||
ProductType = product.Type,
|
||||
ProductType = product.Type.ToString(),
|
||||
PointsCost = totalCost,
|
||||
PointsBalance = newPointsBalance,
|
||||
PointsBalance = 0, // 稍后赋值
|
||||
Quantity = input.Quantity,
|
||||
Status = "Success",
|
||||
Status = ExchangeRecordStatusEnum.Success,
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -238,9 +223,25 @@ public class WxMallService(
|
||||
var inserted = await exchangeRecordRepository.InsertReturnEntityAsync(record);
|
||||
recordId = inserted.Id;
|
||||
|
||||
// 扣除积分
|
||||
pointsResult = await pointsService.DeductPointsInTranAsync(new DeductPointsInput
|
||||
{
|
||||
UserId = userId,
|
||||
Amount = totalCost,
|
||||
ChangeType = PointsChangeTypeEnum.Exchange,
|
||||
RelatedId = recordId,
|
||||
Description = $"兑换 {product.Name} x{input.Quantity}"
|
||||
});
|
||||
|
||||
// 更新兑换记录的积分余额
|
||||
await exchangeRecordRepository.Updateable<ExchangeRecord>()
|
||||
.SetColumns(r => r.PointsBalance == pointsResult.NewBalance)
|
||||
.Where(r => r.Id == recordId)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
// 加入背包
|
||||
var existingBag = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == "Available")
|
||||
.Where(b => b.UserId == userId && b.ItemId == product.Id && !b.IsDeleted && b.Status == UserBagStatusEnum.Available)
|
||||
.FirstAsync();
|
||||
|
||||
if (existingBag != null)
|
||||
@ -259,11 +260,11 @@ public class WxMallService(
|
||||
{
|
||||
UserId = userId,
|
||||
ItemId = product.Id,
|
||||
ItemType = product.Type,
|
||||
ItemType = product.Type.ToString(),
|
||||
Quantity = input.Quantity,
|
||||
MetaData = product.MetaData,
|
||||
Type = product.Type,
|
||||
Status = "Available",
|
||||
Type = Enum.TryParse<UserBagTypeEnum>(product.Type.ToString(), out var bagType) ? bagType : default,
|
||||
Status = UserBagStatusEnum.Available,
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -272,25 +273,6 @@ public class WxMallService(
|
||||
};
|
||||
await exchangeRecordRepository.Context.Insertable(bagItem).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
// 创建积分消耗记录
|
||||
var pointsRecord = new PointsRecord
|
||||
{
|
||||
UserId = userId,
|
||||
ChangeAmount = -totalCost,
|
||||
BalanceAfter = newPointsBalance,
|
||||
ChangeType = "Exchange",
|
||||
RelatedId = recordId,
|
||||
Description = $"兑换 {product.Name} x{input.Quantity}",
|
||||
Type = "Expense",
|
||||
Status = "Success",
|
||||
IsDeleted = false,
|
||||
CreatedBy = userId.ToString(),
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedBy = userId.ToString(),
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
await exchangeRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
||||
});
|
||||
|
||||
logger.LogInformation("兑换成功,UserId: {UserId}, Product: {Product}, Cost: {Cost}",
|
||||
@ -301,7 +283,7 @@ public class WxMallService(
|
||||
RecordId = recordId,
|
||||
ProductName = product.Name,
|
||||
PointsCost = totalCost,
|
||||
PointsBalance = newPointsBalance,
|
||||
PointsBalance = pointsResult!.NewBalance,
|
||||
Message = $"兑换成功!{product.Name} x{input.Quantity} 已放入背包"
|
||||
};
|
||||
}
|
||||
@ -324,7 +306,7 @@ public class WxMallService(
|
||||
PointsCost = r.PointsCost,
|
||||
PointsBalance = r.PointsBalance,
|
||||
Quantity = r.Quantity,
|
||||
Status = r.Status,
|
||||
Status = r.Status.ToString(),
|
||||
CreatedAt = r.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
@ -336,7 +318,7 @@ public class WxMallService(
|
||||
public async Task<List<UserBagOutput>> GetBagItemsAsync(long userId, string? itemType = null)
|
||||
{
|
||||
var query = exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "Available")
|
||||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available)
|
||||
.WhereIF(!string.IsNullOrEmpty(itemType), b => b.ItemType == itemType)
|
||||
.OrderByDescending(b => b.CreatedAt);
|
||||
|
||||
@ -391,7 +373,7 @@ public class WxMallService(
|
||||
ItemId = b.ItemId,
|
||||
ItemType = b.ItemType,
|
||||
Quantity = b.Quantity,
|
||||
Status = b.Status,
|
||||
Status = b.Status.ToString(),
|
||||
CreatedAt = b.CreatedAt,
|
||||
Product = product != null ? new BagProductBrief
|
||||
{
|
||||
@ -421,7 +403,7 @@ public class WxMallService(
|
||||
throw new BusinessException("背包物品Id无效", 400);
|
||||
|
||||
var bagItem = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.Id == input.BagItemId && b.UserId == userId && !b.IsDeleted && b.Status == "Available")
|
||||
.Where(b => b.Id == input.BagItemId && b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available)
|
||||
.FirstAsync();
|
||||
|
||||
if (bagItem == null)
|
||||
@ -462,7 +444,7 @@ public class WxMallService(
|
||||
if (bagItem.Quantity <= 1)
|
||||
{
|
||||
await exchangeRecordRepository.Context.Updateable<UserBag>()
|
||||
.SetColumns(b => b.Status == "UsedUp")
|
||||
.SetColumns(b => b.Status == UserBagStatusEnum.Expired)
|
||||
.SetColumns(b => b.Quantity == 0)
|
||||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||||
.Where(b => b.Id == bagItem.Id)
|
||||
@ -525,7 +507,7 @@ public class WxMallService(
|
||||
|
||||
// 校验背包中是否拥有该皮肤(通过 MetaData 中的 SkinId 判断)
|
||||
var hasSkin = await exchangeRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "Available" && b.Quantity > 0
|
||||
.Where(b => b.UserId == userId && !b.IsDeleted && b.Status == UserBagStatusEnum.Available && b.Quantity > 0
|
||||
&& b.ItemType == "PetBg")
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user