feat: 完成多模块功能迭代与优化
1. 新增宠物皮肤初始枚举值、下拉列表接口及相关DTO 2. 完善商品实体与DTO,新增虚拟商品标记和类型ID字段 3. 重构用户认证体系,支持多用户切换并重新生成JWT令牌 4. 新增签到配置管理全套功能,包括增删改查和状态管理 5. 优化模型验证过滤器和基础响应类的命名规范 6. 新增补签功能,完善签到服务逻辑 7. 拆分用户详情DTO,新增各子数据分页查询接口 8. 重构微信控制器的用户ID获取逻辑,统一使用激活用户ID 9. 修复背包服务中补签卡的扣减逻辑 10. 新增家长姓名修改接口和相关服务实现
This commit is contained in:
259
QYZH.InteractiveMagazine.Service/CheckInConfigService.cs
Normal file
259
QYZH.InteractiveMagazine.Service/CheckInConfigService.cs
Normal file
@ -0,0 +1,259 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Repository;
|
||||
using SqlSugar;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 签到配置服务实现
|
||||
/// </summary>
|
||||
public class CheckInConfigService(
|
||||
BaseRepository<CheckInConfig> checkInConfigRepository,
|
||||
ILogger<CheckInConfigService> logger)
|
||||
: BaseRepository<CheckInConfig>, ICheckInConfigService
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建签到配置
|
||||
/// </summary>
|
||||
public async Task<CheckInConfigOutput> CreateAsync(CheckInConfigInput input)
|
||||
{
|
||||
logger.LogInformation("正在创建签到配置,DayNumber: {DayNumber}, Type: {Type}", input.DayNumber, input.Type);
|
||||
|
||||
if (input.DayNumber <= 0)
|
||||
{
|
||||
throw new BusinessException("连续签到天数必须大于0", 400);
|
||||
}
|
||||
|
||||
if (input.RewardPoints < 0)
|
||||
{
|
||||
throw new BusinessException("奖励积分不能为负数", 400);
|
||||
}
|
||||
|
||||
if (input.BonusPoints < 0)
|
||||
{
|
||||
throw new BusinessException("额外奖励积分不能为负数", 400);
|
||||
}
|
||||
|
||||
// 检查同类型下是否已存在相同天数配置
|
||||
var exists = await checkInConfigRepository.Context.Queryable<CheckInConfig>()
|
||||
.Where(c => c.Type == input.Type && c.DayNumber == input.DayNumber && !c.IsDeleted)
|
||||
.AnyAsync();
|
||||
|
||||
if (exists)
|
||||
{
|
||||
throw new BusinessException($"该类型下已存在连续{input.DayNumber}天的配置", 400);
|
||||
}
|
||||
|
||||
var config = new CheckInConfig
|
||||
{
|
||||
DayNumber = input.DayNumber,
|
||||
RewardPoints = input.RewardPoints,
|
||||
BonusPoints = input.BonusPoints,
|
||||
Type = input.Type,
|
||||
Status = (int)DefaultStatusEnum.Active,
|
||||
CreatedBy = "System",
|
||||
UpdatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
var result = await checkInConfigRepository.InsertAsync(config);
|
||||
if (!result)
|
||||
{
|
||||
throw new BusinessException("创建签到配置失败", 500);
|
||||
}
|
||||
|
||||
logger.LogInformation("签到配置创建成功,ID: {Id}", config.Id);
|
||||
return MapToOutput(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新签到配置
|
||||
/// </summary>
|
||||
public async Task<CheckInConfigOutput> UpdateAsync(long id, CheckInConfigInput input)
|
||||
{
|
||||
logger.LogInformation("正在更新签到配置,ID: {Id}", id);
|
||||
|
||||
var config = await checkInConfigRepository.GetByIdAsync(id);
|
||||
if (config == null)
|
||||
{
|
||||
logger.LogWarning("未找到要更新的签到配置,ID: {Id}", id);
|
||||
throw new BusinessException("签到配置不存在", 404);
|
||||
}
|
||||
|
||||
if (input.DayNumber <= 0)
|
||||
{
|
||||
throw new BusinessException("连续签到天数必须大于0", 400);
|
||||
}
|
||||
|
||||
if (input.RewardPoints < 0)
|
||||
{
|
||||
throw new BusinessException("奖励积分不能为负数", 400);
|
||||
}
|
||||
|
||||
if (input.BonusPoints < 0)
|
||||
{
|
||||
throw new BusinessException("额外奖励积分不能为负数", 400);
|
||||
}
|
||||
|
||||
// 检查同类型下是否已存在相同天数配置(排除自身)
|
||||
var exists = await checkInConfigRepository.Context.Queryable<CheckInConfig>()
|
||||
.Where(c => c.Type == input.Type && c.DayNumber == input.DayNumber && c.Id != id && !c.IsDeleted)
|
||||
.AnyAsync();
|
||||
|
||||
if (exists)
|
||||
{
|
||||
throw new BusinessException($"该类型下已存在连续{input.DayNumber}天的配置", 400);
|
||||
}
|
||||
|
||||
config.DayNumber = input.DayNumber;
|
||||
config.RewardPoints = input.RewardPoints;
|
||||
config.BonusPoints = input.BonusPoints;
|
||||
config.Type = input.Type;
|
||||
config.UpdatedBy = "System";
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
|
||||
var updateResult = await checkInConfigRepository.UpdateAsync(config);
|
||||
if (!updateResult)
|
||||
{
|
||||
throw new BusinessException("更新签到配置失败", 500);
|
||||
}
|
||||
|
||||
logger.LogInformation("签到配置更新成功,ID: {Id}", id);
|
||||
return MapToOutput(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除签到配置(软删除)
|
||||
/// </summary>
|
||||
public async Task DeleteAsync(long id)
|
||||
{
|
||||
logger.LogInformation("正在删除签到配置,ID: {Id}", id);
|
||||
|
||||
var config = await checkInConfigRepository.GetByIdAsync(id);
|
||||
if (config == null)
|
||||
{
|
||||
logger.LogWarning("未找到要删除的签到配置,ID: {Id}", id);
|
||||
throw new BusinessException("签到配置不存在", 404);
|
||||
}
|
||||
|
||||
var result = await checkInConfigRepository.Context.Updateable<CheckInConfig>()
|
||||
.SetColumns(c => new CheckInConfig
|
||||
{
|
||||
IsDeleted = true,
|
||||
UpdatedBy = "System",
|
||||
UpdatedAt = DateTime.Now
|
||||
})
|
||||
.Where(c => c.Id == id)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
if (result <= 0)
|
||||
{
|
||||
throw new BusinessException("删除签到配置失败", 500);
|
||||
}
|
||||
|
||||
logger.LogInformation("签到配置删除成功,ID: {Id}", id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取签到配置
|
||||
/// </summary>
|
||||
public async Task<CheckInConfigOutput> GetByIdAsync(long id)
|
||||
{
|
||||
var config = await checkInConfigRepository.GetByIdAsync(id);
|
||||
if (config == null)
|
||||
{
|
||||
throw new BusinessException("签到配置不存在", 404);
|
||||
}
|
||||
|
||||
return MapToOutput(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询签到配置列表
|
||||
/// </summary>
|
||||
public async Task<PageListModel<CheckInConfigOutput>> GetListAsync(CheckInConfigQueryInput input)
|
||||
{
|
||||
logger.LogInformation("正在查询签到配置列表,页码: {PageIndex}, 每页条数: {PageSize}", input.PageIndex, input.PageSize);
|
||||
|
||||
if (input.PageIndex <= 0)
|
||||
{
|
||||
throw new BusinessException("页码必须大于0", 400);
|
||||
}
|
||||
|
||||
if (input.PageSize <= 0 || input.PageSize > 100)
|
||||
{
|
||||
throw new BusinessException("每页条数必须在1-100之间", 400);
|
||||
}
|
||||
|
||||
RefAsync<int> totalNumber = 0;
|
||||
var configs = await checkInConfigRepository.Queryable()
|
||||
.Where(c => !c.IsDeleted)
|
||||
.WhereIF(input.Type.HasValue, c => c.Type == input.Type.Value)
|
||||
.OrderBy(c => c.Type)
|
||||
.OrderBy(c => c.DayNumber)
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
var pageResult = configs.Select(MapToOutput).ToList();
|
||||
|
||||
var result = new PageListModel<CheckInConfigOutput>(new List<CheckInConfigOutput>(), input.PageIndex, input.PageSize, totalNumber);
|
||||
result.Result = pageResult;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新签到配置启用/禁用状态
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateStatusAsync(long id)
|
||||
{
|
||||
var config = await checkInConfigRepository.GetByIdAsync(id);
|
||||
if (config == null)
|
||||
{
|
||||
logger.LogWarning("未找到要更新状态的签到配置,ID: {Id}", id);
|
||||
throw new BusinessException("签到配置不存在", 404);
|
||||
}
|
||||
|
||||
config.Status = config.Status == (int)DefaultStatusEnum.Active
|
||||
? (int)DefaultStatusEnum.Inactive
|
||||
: (int)DefaultStatusEnum.Active;
|
||||
config.UpdatedBy = "System";
|
||||
config.UpdatedAt = DateTime.Now;
|
||||
|
||||
var result = await checkInConfigRepository.UpdateAsync(config);
|
||||
if (!result)
|
||||
{
|
||||
logger.LogError("签到配置状态更新失败,ID: {Id}", id);
|
||||
throw new BusinessException("更新签到配置状态失败", 500);
|
||||
}
|
||||
|
||||
logger.LogInformation("签到配置状态更新成功,ID: {Id}, Status: {Status}", id, config.Status);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实体映射为输出DTO
|
||||
/// </summary>
|
||||
private static CheckInConfigOutput MapToOutput(CheckInConfig config)
|
||||
{
|
||||
return new CheckInConfigOutput
|
||||
{
|
||||
Id = config.Id,
|
||||
DayNumber = config.DayNumber,
|
||||
RewardPoints = config.RewardPoints,
|
||||
BonusPoints = config.BonusPoints,
|
||||
Type = config.Type,
|
||||
Status = config.Status,
|
||||
CreatedBy = config.CreatedBy,
|
||||
CreatedAt = config.CreatedAt,
|
||||
UpdatedBy = config.UpdatedBy,
|
||||
UpdatedAt = config.UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -82,7 +82,9 @@ public class CheckInService(
|
||||
GrowthPointsAwarded = growthReward,
|
||||
ConsecutiveDays = consecutiveDays,
|
||||
Type = CheckInRecordTypeEnum.Normal,
|
||||
Status = (int)CheckInRecordStatusEnum.Success
|
||||
Status = (int)CheckInRecordStatusEnum.Success,
|
||||
CreatedBy = userId.ToString(),
|
||||
UpdatedBy = userId.ToString()
|
||||
};
|
||||
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
|
||||
checkInRecord.Id = recordId;
|
||||
@ -198,8 +200,8 @@ public class CheckInService(
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
// 最近 30 天签到记录
|
||||
var thirtyDaysAgo = today.AddDays(-29);
|
||||
// 最近 7 天签到记录
|
||||
var thirtyDaysAgo = today.AddDays(-6);
|
||||
var recentRecords = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= thirtyDaysAgo)
|
||||
.OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc)
|
||||
@ -207,6 +209,7 @@ public class CheckInService(
|
||||
{
|
||||
Id = (long)r.Id,
|
||||
CheckInDate = r.CheckInDate,
|
||||
CreatedAt = r.CreatedAt,
|
||||
ConsecutiveDays = r.ConsecutiveDays,
|
||||
PointsAwarded = r.PointsAwarded,
|
||||
GrowthPointsAwarded = r.GrowthPointsAwarded,
|
||||
@ -247,6 +250,19 @@ public class CheckInService(
|
||||
if (alreadyCheckedIn)
|
||||
throw new BusinessException($"{targetDate:yyyy-MM-dd} 已签到,无需补签", 400);
|
||||
|
||||
// 检查用户背包中是否有补签卡
|
||||
var makeUpCard = await checkInRecordRepository.Context.Queryable<UserBag>()
|
||||
.Where(b => b.UserId == userId
|
||||
&& b.ItemType == "MakeUpCard"
|
||||
&& b.Quantity > 0
|
||||
&& b.Status == (int)UserBagStatusEnum.Available
|
||||
&& !b.IsDeleted)
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.FirstAsync();
|
||||
|
||||
if (makeUpCard == null)
|
||||
throw new BusinessException("补签卡不足,无法补签", 400);
|
||||
|
||||
// 查询用户信息
|
||||
var user = await checkInRecordRepository.Context.Queryable<Users>()
|
||||
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||
@ -304,6 +320,25 @@ public class CheckInService(
|
||||
Description = $"补签奖励({targetDate:yyyy-MM-dd})"
|
||||
});
|
||||
|
||||
// 扣减补签卡
|
||||
if (makeUpCard.Quantity <= 1)
|
||||
{
|
||||
await checkInRecordRepository.Context.Updateable<UserBag>()
|
||||
.SetColumns(b => b.Status == (int)UserBagStatusEnum.Expired)
|
||||
.SetColumns(b => b.Quantity == 0)
|
||||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||||
.Where(b => b.Id == makeUpCard.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await checkInRecordRepository.Context.Updateable<UserBag>()
|
||||
.SetColumns(b => b.Quantity == makeUpCard.Quantity - 1)
|
||||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||||
.Where(b => b.Id == makeUpCard.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
result.RecordId = (long)recordId;
|
||||
result.CheckInDate = targetDate;
|
||||
result.ConsecutiveDays = 0;
|
||||
@ -344,7 +379,7 @@ public class CheckInService(
|
||||
/// <summary>
|
||||
/// 获取用户漏签日期列表
|
||||
/// </summary>
|
||||
public async Task<List<DateTime>> GetMissedDatesAsync(long userId, int days = 30)
|
||||
public async Task<List<DateTime>> GetMissedDatesAsync(long userId, int days = 7)
|
||||
{
|
||||
var startDate = DateTime.Now.Date.AddDays(-days);
|
||||
|
||||
|
||||
@ -1281,4 +1281,23 @@ public class PetService(
|
||||
Images = imagesByStage.GetValueOrDefault(e.Id, new List<PetSkinImageOutput>())
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有皮肤下拉列表(仅返回Id和Name,按SortOrder排序)
|
||||
/// </summary>
|
||||
public async Task<List<SelectOptionDto>> GetAllSkinsForSelectAsync(List<string>? types = null)
|
||||
{
|
||||
var list = await petSkinRepository.Queryable()
|
||||
.Where(s => !s.IsDeleted)
|
||||
.WhereIF(types != null && types.Count > 0, s => types.Contains(s.Type.ToString()))
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.Select(s => new SelectOptionDto
|
||||
{
|
||||
Id = s.Id,
|
||||
Name = s.Name
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
@ -52,6 +53,8 @@ public class ProductService(
|
||||
MetaData = input.MetaData,
|
||||
IsActive = input.IsActive,
|
||||
Stock = input.Stock,
|
||||
IsVirtual = input.IsVirtual,
|
||||
TypeId = input.TypeId,
|
||||
CreatedBy = "System",
|
||||
UpdatedBy = "System",
|
||||
CreatedAt = DateTime.Now,
|
||||
@ -90,6 +93,8 @@ public class ProductService(
|
||||
MetaData = product.MetaData,
|
||||
IsActive = product.IsActive,
|
||||
Stock = product.Stock,
|
||||
IsVirtual = product.IsVirtual,
|
||||
TypeId = product.TypeId,
|
||||
CreatedBy = product.CreatedBy,
|
||||
CreatedAt = product.CreatedAt,
|
||||
UpdatedBy = product.UpdatedBy,
|
||||
@ -144,6 +149,8 @@ public class ProductService(
|
||||
product.MetaData = input.MetaData;
|
||||
product.IsActive = input.IsActive;
|
||||
product.Stock = input.Stock;
|
||||
product.IsVirtual = input.IsVirtual;
|
||||
product.TypeId = input.TypeId;
|
||||
product.UpdatedBy = "System";
|
||||
product.UpdatedAt = DateTime.Now;
|
||||
|
||||
@ -168,6 +175,8 @@ public class ProductService(
|
||||
MetaData = product.MetaData,
|
||||
IsActive = product.IsActive,
|
||||
Stock = product.Stock,
|
||||
IsVirtual = product.IsVirtual,
|
||||
TypeId = product.TypeId,
|
||||
CreatedBy = product.CreatedBy,
|
||||
CreatedAt = product.CreatedAt,
|
||||
UpdatedBy = product.UpdatedBy,
|
||||
@ -218,13 +227,15 @@ public class ProductService(
|
||||
Id = product.Id,
|
||||
Name = product.Name,
|
||||
Description = product.Description,
|
||||
ImageUrl = product.ImageUrl,
|
||||
ImageUrl = DomainHelper.OssFullUrl(product.ImageUrl),
|
||||
Price = product.Price,
|
||||
Type = product.Type.ToString(),
|
||||
Status = product.Status.ToString(),
|
||||
MetaData = product.MetaData,
|
||||
IsActive = product.IsActive,
|
||||
Stock = product.Stock,
|
||||
IsVirtual = product.IsVirtual,
|
||||
TypeId = product.TypeId,
|
||||
CreatedBy = product.CreatedBy,
|
||||
CreatedAt = product.CreatedAt,
|
||||
UpdatedBy = product.UpdatedBy,
|
||||
@ -256,26 +267,29 @@ public class ProductService(
|
||||
.WhereIF(input.Status.HasValue, p => p.Status == (int)input.Status)
|
||||
.WhereIF(input.IsActive.HasValue, p => p.IsActive == input.IsActive.Value)
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Select(p => new ProductOutput
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
ImageUrl = p.ImageUrl,
|
||||
Price = p.Price,
|
||||
Type = p.Type.ToString(),
|
||||
Status = p.Status.ToString(),
|
||||
MetaData = p.MetaData,
|
||||
IsActive = p.IsActive,
|
||||
Stock = p.Stock,
|
||||
CreatedBy = p.CreatedBy,
|
||||
CreatedAt = p.CreatedAt,
|
||||
UpdatedBy = p.UpdatedBy,
|
||||
UpdatedAt = p.UpdatedAt
|
||||
}, true)
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||
|
||||
return new PageListModel<ProductOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
|
||||
var result = pageResult.Select(p => new ProductOutput
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Description = p.Description,
|
||||
ImageUrl = DomainHelper.OssFullUrl(p.ImageUrl),
|
||||
Price = p.Price,
|
||||
Type = p.Type.ToString(),
|
||||
Status = p.Status.ToString(),
|
||||
MetaData = p.MetaData,
|
||||
IsActive = p.IsActive,
|
||||
Stock = p.Stock,
|
||||
IsVirtual = p.IsVirtual,
|
||||
TypeId = p.TypeId,
|
||||
CreatedBy = p.CreatedBy,
|
||||
CreatedAt = p.CreatedAt,
|
||||
UpdatedBy = p.UpdatedBy,
|
||||
UpdatedAt = p.UpdatedAt
|
||||
}).ToList();
|
||||
|
||||
return new PageListModel<ProductOutput>(result, input.PageIndex, input.PageSize, totalNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -37,7 +37,7 @@ public class UsersService(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表)
|
||||
/// 获取用户详情(仅基本信息)
|
||||
/// </summary>
|
||||
public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id)
|
||||
{
|
||||
@ -47,125 +47,124 @@ public class UsersService(
|
||||
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
|
||||
BasicInfo = user
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户积分记录(最近20条)
|
||||
/// 分页查询用户积分记录
|
||||
/// </summary>
|
||||
private async Task<List<PointsRecordOutput>> GetPointsRecordsAsync(long userId)
|
||||
public async Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await pointsService.GetPointsRecordsAsync(new PointsRecordQueryInput
|
||||
input.UserId = userId;
|
||||
var result = await pointsService.GetPointsRecordsAsync(input);
|
||||
return BaseResponse<PageListModel<PointsRecordOutput>>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询用户签到记录
|
||||
/// </summary>
|
||||
public async Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input)
|
||||
{
|
||||
RefAsync<int> total = 0;
|
||||
var records = await Context.Queryable<CheckInRecord>()
|
||||
.Where(r => r.UserId == userId && !r.IsDeleted)
|
||||
.OrderBy(r => r.CheckInDate, OrderByType.Desc)
|
||||
.Select(r => new CheckInRecordOutput
|
||||
{
|
||||
UserId = userId,
|
||||
PageIndex = 1,
|
||||
PageSize = 20
|
||||
});
|
||||
return result.Result ?? [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户积分记录失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
Id = (long)r.Id,
|
||||
CheckInDate = r.CheckInDate,
|
||||
CreatedAt = r.CreatedAt,
|
||||
ConsecutiveDays = r.ConsecutiveDays,
|
||||
PointsAwarded = r.PointsAwarded,
|
||||
GrowthPointsAwarded = r.GrowthPointsAwarded,
|
||||
Type = r.Type.ToString(),
|
||||
Status = r.Status.ToString()
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, total);
|
||||
|
||||
var page = new PageListModel<CheckInRecordOutput>(records, input.PageIndex, input.PageSize, total);
|
||||
return BaseResponse<PageListModel<CheckInRecordOutput>>.Success(page);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户签到记录
|
||||
/// 分页查询用户补偿任务
|
||||
/// </summary>
|
||||
private async Task<List<CheckInRecordOutput>> GetCheckInRecordsAsync(long userId)
|
||||
public async Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasksAsync(long userId, CompensationTaskQueryInput input)
|
||||
{
|
||||
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
|
||||
RefAsync<int> total = 0;
|
||||
var tasks = await Context.Queryable<CompensationTask>()
|
||||
.Where(t => t.UserId == userId && !t.IsDeleted)
|
||||
.WhereIF(input.Status.HasValue, t => t.Status == (int)input.Status)
|
||||
.WhereIF(input.TaskType.HasValue, t => t.TaskType == (int)input.TaskType)
|
||||
.WhereIF(!string.IsNullOrEmpty(input.BusinessSource), t => t.BusinessSource == input.BusinessSource)
|
||||
.OrderBy(t => t.CreatedAt, OrderByType.Desc)
|
||||
.Select(t => new CompensationTaskOutput
|
||||
{
|
||||
UserId = userId,
|
||||
Status = CompensationTaskStatusEnum.Failed,
|
||||
Limit = 20
|
||||
});
|
||||
return tasks;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户失败补偿任务失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
Id = t.Id,
|
||||
TaskType = (CompensationTaskTypeEnum)t.TaskType,
|
||||
BusinessSource = t.BusinessSource,
|
||||
BusinessId = t.BusinessId,
|
||||
UserId = t.UserId,
|
||||
Payload = t.Payload,
|
||||
ErrorMessage = t.ErrorMessage,
|
||||
ErrorSource = t.ErrorSource,
|
||||
RetryCount = t.RetryCount,
|
||||
MaxRetries = t.MaxRetries,
|
||||
Status = (CompensationTaskStatusEnum)t.Status,
|
||||
ProcessedAt = t.ProcessedAt,
|
||||
ScheduledAt = t.ScheduledAt,
|
||||
ResultMessage = t.ResultMessage,
|
||||
CreatedAt = t.CreatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, total);
|
||||
|
||||
var page = new PageListModel<CompensationTaskOutput>(tasks, input.PageIndex, input.PageSize, total);
|
||||
return BaseResponse<PageListModel<CompensationTaskOutput>>.Success(page);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户拥有的期刊列表(含期刊详情)
|
||||
/// 分页查询用户期刊列表(含期刊详情)
|
||||
/// </summary>
|
||||
private async Task<List<UserJournalItemOutput>> GetUserJournalsWithDetailAsync(long userId)
|
||||
public async Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournalsAsync(long userId, UserJournalQueryInput input)
|
||||
{
|
||||
try
|
||||
RefAsync<int> total = 0;
|
||||
var items = await Context.Queryable<UserJournal>()
|
||||
.Where(uj => uj.UserId == userId && !uj.IsDeleted)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type.ToString() == input.Type)
|
||||
.OrderByDescending(uj => uj.CreatedAt)
|
||||
.Select(uj => new UserJournalItemOutput
|
||||
{
|
||||
BindId = uj.Id,
|
||||
JournalId = uj.JournalId,
|
||||
Type = uj.Type.ToString(),
|
||||
Status = uj.Status.ToString(),
|
||||
CreatedAt = uj.CreatedAt
|
||||
})
|
||||
.ToPageListAsync(input.PageIndex, input.PageSize, total);
|
||||
|
||||
// 填充期刊详情(标题、封面)
|
||||
var journalIds = items.Select(i => i.JournalId).Distinct().ToList();
|
||||
if (journalIds.Count > 0)
|
||||
{
|
||||
var userJournals = await Context.Queryable<UserJournal>()
|
||||
.Where(uj => uj.UserId == userId && !uj.IsDeleted)
|
||||
.OrderByDescending(uj => uj.CreatedAt)
|
||||
var journals = await Context.Queryable<Journal>()
|
||||
.Where(j => journalIds.Contains(j.Id) && !j.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new List<UserJournalItemOutput>();
|
||||
foreach (var uj in userJournals)
|
||||
var journalDict = journals.ToDictionary(j => j.Id);
|
||||
foreach (var item in items)
|
||||
{
|
||||
var journal = await Context.Queryable<Journal>()
|
||||
.Where(j => j.Id == uj.JournalId && !j.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (journal != null)
|
||||
if (journalDict.TryGetValue(item.JournalId, out var journal))
|
||||
{
|
||||
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
|
||||
});
|
||||
item.JournalTitle = journal.Title;
|
||||
item.CoverImageUrl = journal.CoverImageUrl;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "获取用户期刊列表失败,UserId: {UserId}", userId);
|
||||
return [];
|
||||
}
|
||||
|
||||
var page = new PageListModel<UserJournalItemOutput>(items, input.PageIndex, input.PageSize, total);
|
||||
return BaseResponse<PageListModel<UserJournalItemOutput>>.Success(page);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -152,15 +152,16 @@ public class WeChatAuthService(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换用户(同一 WxUser 下切换 User 身份,JWT 基于 WxUser 无需重新生成)
|
||||
/// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token)
|
||||
/// </summary>
|
||||
public async Task<WeChatSwitchUserOutput> SwitchUserAsync(long wxUserId, WeChatSwitchUserInput input)
|
||||
public async Task<WeChatSwitchUserOutput> SwitchUserAsync(long wxUserId, long currentUserId, WeChatSwitchUserInput input)
|
||||
{
|
||||
logger.LogInformation("切换用户,WxUserId: {WxUserId}, 目标 UserId: {TargetUserId}", wxUserId, input.UserId);
|
||||
logger.LogInformation("切换用户,WxUserId: {WxUserId}, 当前 UserId: {CurrentUserId}, 目标 UserId: {TargetUserId}",
|
||||
wxUserId, currentUserId, input.UserId);
|
||||
|
||||
// 查询目标用户
|
||||
var targetUser = await wxUserRepository.Context.Queryable<Users>()
|
||||
.Where(u => u.Id == input.UserId)
|
||||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (targetUser == null)
|
||||
@ -179,23 +180,32 @@ public class WeChatAuthService(
|
||||
// 更新 IsLastOnline(清除所有,设置目标为 true)
|
||||
await wxUserRepository.Context.Updateable<Users>()
|
||||
.SetColumns(u => u.IsLastOnline == false)
|
||||
.Where(u => u.WxUserId == wxUserId )
|
||||
.Where(u => u.WxUserId == wxUserId && !u.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
await wxUserRepository.Context.Updateable<Users>()
|
||||
.SetColumns(u => u.IsLastOnline == true)
|
||||
.Where(u => u.Id == input.UserId )
|
||||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
logger.LogInformation("IsLastOnline 已更新,目标用户 {UserId} 设为 true", input.UserId);
|
||||
|
||||
// 重新查询目标用户获取最新数据
|
||||
var refreshedUser = await wxUserRepository.Context.Queryable<Users>()
|
||||
.Where(u => u.Id == input.UserId)
|
||||
.Where(u => u.Id == input.UserId && !u.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
// 生成新 JWT Token(WxUserId + 新 UserId)
|
||||
var jwtSettings = GetJwtSettings();
|
||||
var token = JwtHelper.GenerateToken(wxUserId, refreshedUser.Id, refreshedUser.Name ?? string.Empty, jwtSettings);
|
||||
|
||||
// 清除旧 Redis Token,写入新 Token
|
||||
await RedisHelper.DelAsync($"{TokenKeyPrefix}:{currentUserId}");
|
||||
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
|
||||
return new WeChatSwitchUserOutput
|
||||
{
|
||||
Token = token,
|
||||
User = MapUserToOutput(refreshedUser)
|
||||
};
|
||||
}
|
||||
@ -263,21 +273,66 @@ public class WeChatAuthService(
|
||||
return MapUserToOutput(newUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改家长名字(WxUser.Name)
|
||||
/// </summary>
|
||||
public async Task<WxUserInfoOutput> UpdateWxUserNameAsync(long wxUserId, UpdateWxUserNameInput input)
|
||||
{
|
||||
logger.LogInformation("修改家长名字,WxUserId: {WxUserId}, NewName: {Name}", wxUserId, input.Name);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.Name))
|
||||
throw new BusinessException("名字不能为空", 400);
|
||||
|
||||
var wxUser = await wxUserRepository.Context.Queryable<WxUser>()
|
||||
.Where(w => w.Id == wxUserId && !w.IsDeleted)
|
||||
.FirstAsync();
|
||||
|
||||
if (wxUser == null)
|
||||
throw new BusinessException("微信用户不存在", 404);
|
||||
|
||||
await wxUserRepository.Context.Updateable<WxUser>()
|
||||
.SetColumns(w => w.Name == input.Name.Trim())
|
||||
.SetColumns(w => w.UpdatedAt == DateTime.Now)
|
||||
.SetColumns(w => w.UpdatedBy == wxUserId.ToString())
|
||||
.Where(w => w.Id == wxUserId)
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
wxUser.Name = input.Name.Trim();
|
||||
|
||||
logger.LogInformation("家长名字修改成功,WxUserId: {WxUserId}, NewName: {Name}", wxUserId, input.Name);
|
||||
|
||||
return new WxUserInfoOutput
|
||||
{
|
||||
Id = wxUser.Id,
|
||||
OpenId = wxUser.OpenId,
|
||||
UnionId = wxUser.UnionId,
|
||||
Name = wxUser.Name,
|
||||
AvatarUrl = wxUser.AvatarUrl,
|
||||
Phone = wxUser.Phone
|
||||
};
|
||||
}
|
||||
|
||||
#region 私有辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 构建登录输出(JWT 基于 WxUser,不绑定具体 User)
|
||||
/// 构建登录输出(JWT 同时携带 WxUserId 和当前激活 UserId)
|
||||
/// </summary>
|
||||
private async Task<WeChatLoginOutput> BuildLoginOutputAsync(WxUser wxUser, List<Users> users)
|
||||
{
|
||||
var jwtSettings = GetJwtSettings();
|
||||
var token = JwtHelper.GenerateToken(wxUser.Id, wxUser.Name ?? wxUser.OpenId, jwtSettings);
|
||||
var activeUser = users.FirstOrDefault(u => u.IsLastOnline) ?? users.FirstOrDefault();
|
||||
|
||||
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{wxUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
var userId = activeUser?.Id ?? 0L;
|
||||
var userName = activeUser?.Name ?? wxUser.Name ?? wxUser.OpenId;
|
||||
|
||||
var jwtSettings = GetJwtSettings();
|
||||
var token = JwtHelper.GenerateToken(wxUser.Id, userId, userName, jwtSettings);
|
||||
|
||||
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{userId}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
|
||||
return new WeChatLoginOutput
|
||||
{
|
||||
Token = token,
|
||||
CurrentUserId = userId,
|
||||
WxUser = new WxUserInfoOutput
|
||||
{
|
||||
Id = wxUser.Id,
|
||||
|
||||
@ -103,6 +103,8 @@ public class WxMallService(
|
||||
ImageUrl = p.ImageUrl,
|
||||
Price = p.Price,
|
||||
Type = p.Type.ToString(),
|
||||
IsVirtual = p.IsVirtual,
|
||||
TypeId = p.TypeId,
|
||||
Owned = owned,
|
||||
Skin = skin != null ? new PetSkinBrief
|
||||
{
|
||||
@ -169,6 +171,8 @@ public class WxMallService(
|
||||
ImageUrl = product.ImageUrl,
|
||||
Price = product.Price,
|
||||
Type = product.Type.ToString(),
|
||||
IsVirtual = product.IsVirtual,
|
||||
TypeId = product.TypeId,
|
||||
Owned = owned,
|
||||
Skin = skinBrief
|
||||
};
|
||||
@ -403,7 +407,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 == (int)UserBagStatusEnum.Available)
|
||||
.Where(b => b.Id == input.BagItemId && b.UserId == userId && b.Status == (int)UserBagStatusEnum.Available)
|
||||
.FirstAsync();
|
||||
|
||||
if (bagItem == null)
|
||||
@ -437,28 +441,9 @@ public class WxMallService(
|
||||
if (targetDate >= DateTime.Now.Date)
|
||||
throw new BusinessException("只能补签过去的日期", 400);
|
||||
|
||||
// 调用签到服务执行补签
|
||||
// 调用签到服务执行补签(内部会检查并扣减补签卡)
|
||||
var checkInResult = await checkInService.MakeUpCheckInAsync(userId, targetDate);
|
||||
|
||||
// 扣减补签卡数量
|
||||
if (bagItem.Quantity <= 1)
|
||||
{
|
||||
await exchangeRecordRepository.Context.Updateable<UserBag>()
|
||||
.SetColumns(b => b.Status == (int)UserBagStatusEnum.Expired)
|
||||
.SetColumns(b => b.Quantity == 0)
|
||||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||||
.Where(b => b.Id == bagItem.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await exchangeRecordRepository.Context.Updateable<UserBag>()
|
||||
.SetColumns(b => b.Quantity == bagItem.Quantity - 1)
|
||||
.SetColumns(b => b.UpdatedAt == DateTime.Now)
|
||||
.Where(b => b.Id == bagItem.Id)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
logger.LogInformation("补签卡使用成功,UserId: {UserId}, TargetDate: {Date}", userId, targetDate);
|
||||
|
||||
return new UseItemOutput
|
||||
|
||||
Reference in New Issue
Block a user