feat: 完成多模块功能迭代与优化

1.  新增宠物皮肤初始枚举值、下拉列表接口及相关DTO
2.  完善商品实体与DTO,新增虚拟商品标记和类型ID字段
3.  重构用户认证体系,支持多用户切换并重新生成JWT令牌
4.  新增签到配置管理全套功能,包括增删改查和状态管理
5.  优化模型验证过滤器和基础响应类的命名规范
6.  新增补签功能,完善签到服务逻辑
7.  拆分用户详情DTO,新增各子数据分页查询接口
8.  重构微信控制器的用户ID获取逻辑,统一使用激活用户ID
9.  修复背包服务中补签卡的扣减逻辑
10. 新增家长姓名修改接口和相关服务实现
This commit is contained in:
glz
2026-06-10 17:53:38 +08:00
parent 028e3dfb34
commit ae4cd627df
35 changed files with 1334 additions and 316 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 TokenWxUserId + 新 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,

View File

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