refactor: 批量新增枚举类型并完成实体、DTO、服务层枚举替换

- 新增30+业务枚举类型覆盖用户、宠物、商城、社区、积分等模块
- 完成实体类、DTO、服务层的字符串枚举替换为强类型枚举
- 修复用户状态枚举名称变更,将Frozen改为Disabled
- 新增批量发布社区消息接口与控制器实现
- 新增操作日志、积分管理、补偿任务相关服务与DTO
This commit is contained in:
glz
2026-06-08 16:57:44 +08:00
parent 698c404b72
commit 78b686f9e5
116 changed files with 4861 additions and 307 deletions

View File

@ -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
};
}

View File

@ -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,

View File

@ -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();

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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)

View File

@ -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,

View 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;
}
}

View File

@ -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
};
}
}

View 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
}

View File

@ -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,

View File

@ -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);

View File

@ -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
};
}
}

View File

@ -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,

View File

@ -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();

View File

@ -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();