From ae4cd627df4caa52c9dad19451e9f0901a98925c Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Wed, 10 Jun 2026 17:53:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=A4=9A=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8A=9F=E8=83=BD=E8=BF=AD=E4=BB=A3=E4=B8=8E=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增宠物皮肤初始枚举值、下拉列表接口及相关DTO 2. 完善商品实体与DTO,新增虚拟商品标记和类型ID字段 3. 重构用户认证体系,支持多用户切换并重新生成JWT令牌 4. 新增签到配置管理全套功能,包括增删改查和状态管理 5. 优化模型验证过滤器和基础响应类的命名规范 6. 新增补签功能,完善签到服务逻辑 7. 拆分用户详情DTO,新增各子数据分页查询接口 8. 重构微信控制器的用户ID获取逻辑,统一使用激活用户ID 9. 修复背包服务中补签卡的扣减逻辑 10. 新增家长姓名修改接口和相关服务实现 --- .../ICheckInConfigService.cs | 52 ++++ .../IPetService.cs | 6 + .../IUsersService.cs | 24 +- .../IWeChatAuthService.cs | 15 +- .../Auth/JwtHelper.cs | 54 +++- .../Middleware/ModelValidActionFilter.cs | 6 +- .../Dto/BaseResponse.cs | 32 +-- .../Dto/CheckIn/CheckInConfigDto.cs | 96 +++++++ .../Dto/CheckIn/CheckInDto.cs | 15 + .../Dto/Mall/ProductDto.cs | 20 ++ .../Dto/Mall/WxMallDto.cs | 10 + .../Dto/SelectOptionDto.cs | 17 ++ .../Dto/UsersDto.cs | 26 +- .../Dto/WeChat/WeChatDto.cs | 25 +- .../Entity/Product.cs | 110 ++++---- .../Enum/PetSkinTypeEnum.cs | 5 + .../CheckInConfigService.cs | 259 ++++++++++++++++++ .../CheckInService.cs | 43 ++- .../PetService.cs | 19 ++ .../ProductService.cs | 52 ++-- .../UsersService.cs | 185 +++++++------ .../WeChatAuthService.cs | 77 +++++- .../WxMallService.cs | 27 +- .../Controllers/CheckInConfigController.cs | 177 ++++++++++++ .../Controllers/PetManageController.cs | 19 ++ .../Controllers/UsersController.cs | 40 ++- .../Controllers/WeChat/BagController.cs | 18 +- .../Controllers/WeChat/CheckInController.cs | 74 ++++- .../Controllers/WeChat/CommunityController.cs | 24 +- .../Controllers/WeChat/JournalController.cs | 12 +- .../Controllers/WeChat/MallController.cs | 24 +- .../Controllers/WeChat/MedalController.cs | 18 +- .../Controllers/WeChat/PetController.cs | 18 +- .../WeChat/WeChatAuthController.cs | 30 +- .../WeChat/WeChatBaseController.cs | 21 +- 35 files changed, 1334 insertions(+), 316 deletions(-) create mode 100644 QYZH.InteractiveMagazine.IService/ICheckInConfigService.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInConfigDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/SelectOptionDto.cs create mode 100644 QYZH.InteractiveMagazine.Service/CheckInConfigService.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs diff --git a/QYZH.InteractiveMagazine.IService/ICheckInConfigService.cs b/QYZH.InteractiveMagazine.IService/ICheckInConfigService.cs new file mode 100644 index 0000000..de9a95b --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/ICheckInConfigService.cs @@ -0,0 +1,52 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.CheckIn; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 签到配置服务接口 +/// +public interface ICheckInConfigService : IBaseService +{ + /// + /// 创建签到配置 + /// + /// 签到配置输入 + /// 创建的签到配置信息 + Task CreateAsync(CheckInConfigInput input); + + /// + /// 更新签到配置 + /// + /// 签到配置ID + /// 签到配置输入 + /// 更新后的签到配置信息 + Task UpdateAsync(long id, CheckInConfigInput input); + + /// + /// 删除签到配置(软删除) + /// + /// 签到配置ID + Task DeleteAsync(long id); + + /// + /// 根据ID获取签到配置 + /// + /// 签到配置ID + /// 签到配置信息 + Task GetByIdAsync(long id); + + /// + /// 分页查询签到配置列表 + /// + /// 查询条件 + /// 分页结果 + Task> GetListAsync(CheckInConfigQueryInput input); + + /// + /// 更新签到配置启用/禁用状态 + /// + /// 签到配置ID + Task UpdateStatusAsync(long id); +} diff --git a/QYZH.InteractiveMagazine.IService/IPetService.cs b/QYZH.InteractiveMagazine.IService/IPetService.cs index a0b9f48..a611d8c 100644 --- a/QYZH.InteractiveMagazine.IService/IPetService.cs +++ b/QYZH.InteractiveMagazine.IService/IPetService.cs @@ -169,4 +169,10 @@ public interface IPetService : IBaseService /// 根据皮肤Id获取各阶段图片列表(按进化阶段分组,含阶段基本信息) /// Task> GetSkinImagesGroupedBySkinIdAsync(long skinId); + + /// + /// 获取所有皮肤下拉列表(仅返回Id和Name,按SortOrder排序) + /// + /// 皮肤类型列表(可选) + Task> GetAllSkinsForSelectAsync(List? types = null); } diff --git a/QYZH.InteractiveMagazine.IService/IUsersService.cs b/QYZH.InteractiveMagazine.IService/IUsersService.cs index 6a1c47b..108b9af 100644 --- a/QYZH.InteractiveMagazine.IService/IUsersService.cs +++ b/QYZH.InteractiveMagazine.IService/IUsersService.cs @@ -1,4 +1,6 @@ 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; @@ -12,7 +14,7 @@ public interface IUsersService : IBaseService Task>> GetListAsync(UsersQueryInput input); /// - /// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表) + /// 获取用户详情(仅基本信息) /// Task> GetDetailAsync(long id); @@ -30,4 +32,24 @@ public interface IUsersService : IBaseService /// 手动扣除用户积分 /// Task ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null); + + /// + /// 分页查询用户积分记录 + /// + Task>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input); + + /// + /// 分页查询用户签到记录 + /// + Task>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input); + + /// + /// 分页查询用户补偿任务 + /// + Task>> GetUserCompensationTasksAsync(long userId, CompensationTaskQueryInput input); + + /// + /// 分页查询用户期刊列表(含期刊详情) + /// + Task>> GetUserJournalsAsync(long userId, UserJournalQueryInput input); } diff --git a/QYZH.InteractiveMagazine.IService/IWeChatAuthService.cs b/QYZH.InteractiveMagazine.IService/IWeChatAuthService.cs index e5e9932..6f70ef9 100644 --- a/QYZH.InteractiveMagazine.IService/IWeChatAuthService.cs +++ b/QYZH.InteractiveMagazine.IService/IWeChatAuthService.cs @@ -19,9 +19,12 @@ public interface IWeChatAuthService : IBaseService Task QuickLoginAsync(WeChatQuickLoginInput input); /// - /// 切换用户(同一 WxUser 下切换 User 身份,JWT 基于 WxUser 无需重新生成 Token) + /// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token) /// - Task SwitchUserAsync(long wxUserId, WeChatSwitchUserInput input); + /// 当前 WxUser.Id(来自 JWT) + /// 当前激活 Users.Id(来自 JWT,用于清除旧 Redis Token) + /// 切换目标用户参数 + Task SwitchUserAsync(long wxUserId, long currentUserId, WeChatSwitchUserInput input); /// /// 获取当前 WxUser 下所有用户列表 @@ -37,4 +40,12 @@ public interface IWeChatAuthService : IBaseService /// 新增用户参数 /// 新创建的用户信息 Task CreateUserAsync(long wxUserId, CreateChildUserInput input); + + /// + /// 修改家长名字(WxUser.Name) + /// + /// 当前登录的 WxUser.Id(来自 JWT) + /// 修改名字参数 + /// 更新后的微信用户信息 + Task UpdateWxUserNameAsync(long wxUserId, UpdateWxUserNameInput input); } diff --git a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs index 9bfa282..0cd55d0 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs @@ -12,7 +12,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Auth; public static class JwtHelper { /// - /// 生成JWT令牌 + /// 生成JWT令牌(管理端 / 单用户场景) /// /// 用户ID /// 用户名 @@ -29,6 +29,39 @@ public static class JwtHelper new Claim(ClaimTypes.Name, userName) }; + return BuildToken(claims, settings); + } + + /// + /// 生成JWT令牌(小程序场景,同时携带 WxUserId 和 UserId) + /// + /// 微信用户ID(WxUser.Id) + /// 当前激活用户ID(Users.Id,无用户时为 0) + /// 用户名 + /// JWT配置 + /// JWT令牌字符串 + public static string GenerateToken(long wxUserId, long userId, string userName, JwtSettings settings) + { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), + new Claim(JwtRegisteredClaimNames.Name, userName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.NameIdentifier, userId.ToString()), + new Claim(ClaimTypes.Name, userName), + new Claim(WxUserIdClaimType, wxUserId.ToString()) + }; + + return BuildToken(claims, settings); + } + + /// + /// 自定义 Claim 类型:WxUserId + /// + public const string WxUserIdClaimType = "WxUserId"; + + private static string BuildToken(Claim[] claims, JwtSettings settings) + { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); @@ -77,6 +110,25 @@ public static class JwtHelper return null; } + /// + /// 从Token中获取微信用户ID(WxUserId) + /// + /// JWT令牌 + /// WxUserId + public static long? GetWxUserIdFromToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken) + { + var wxUserIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == WxUserIdClaimType); + if (long.TryParse(wxUserIdClaim?.Value, out long wxUserId)) + { + return wxUserId; + } + } + return null; + } + /// /// 从Token中获取用户名 /// diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/ModelValidActionFilter.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/ModelValidActionFilter.cs index 0b208f3..b9729ca 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Middleware/ModelValidActionFilter.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/ModelValidActionFilter.cs @@ -24,10 +24,10 @@ public class ModelValidActionFilterAttribute : ActionFilterAttribute errorDic.Add(key, errorStr); } } - var result = new BaseResponse>() { Code = ResultCode.FAIL }; + var result = new BaseResponse>() { code = ResultCode.FAIL }; - result.Message = string.Join("|", errorDic.Select(e => e.Value).Distinct()); - result.Result = errorDic; + result.message = string.Join("|", errorDic.Select(e => e.Value).Distinct()); + result.result = errorDic; context.Result = new JsonResult(result); } } diff --git a/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs b/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs index 4e105ce..3e923fa 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs @@ -13,7 +13,7 @@ public class BaseResponse /// public BaseResponse() { - Code = ResultCode.SUCCESS; + code = ResultCode.SUCCESS; } #region 公共属性 @@ -21,12 +21,12 @@ public class BaseResponse /// /// 操作描述 /// - public string Message { get; set; } + public string message { get; set; } /// /// 结果Code /// - public ResultCode Code { get; set; } + public ResultCode code { get; set; } #endregion 公共属性 @@ -37,7 +37,7 @@ public class BaseResponse /// public static BaseResponse Success() { - return new BaseResponse { Code = ResultCode.SUCCESS, Message = "操作成功。" }; + return new BaseResponse { code = ResultCode.SUCCESS, message = "操作成功。" }; } /// @@ -45,7 +45,7 @@ public class BaseResponse /// public static BaseResponse Success(string message = "操作成功。") { - return new BaseResponse { Code = ResultCode.SUCCESS, Message = message }; + return new BaseResponse { code = ResultCode.SUCCESS, message = message }; } /// @@ -53,7 +53,7 @@ public class BaseResponse /// public static BaseResponse Fail(string message, ResultCode code = ResultCode.FAIL) { - return new BaseResponse { Code = code, Message = message }; + return new BaseResponse { code = code, message = message }; } /// @@ -61,7 +61,7 @@ public class BaseResponse /// public static BaseResponse AuthFail(string message, ResultCode code = ResultCode.FAIL) { - return new BaseResponse { Code = code, Message = message }; + return new BaseResponse { code = code, message = message }; } #endregion 公用方法 @@ -69,7 +69,7 @@ public class BaseResponse /// /// 操作结果 /// - public bool IsSuccess => Code == ResultCode.SUCCESS; + public bool isSuccess => code == ResultCode.SUCCESS; } /// @@ -82,7 +82,7 @@ public class BaseResponse : BaseResponse /// /// 操作结果 /// - public T? Result { get; set; } + public T? result { get; set; } #region 公用方法 @@ -91,7 +91,7 @@ public class BaseResponse : BaseResponse /// public static BaseResponse Success(T result) { - return new BaseResponse { Code = ResultCode.SUCCESS, Message = "", Result = result }; + return new BaseResponse { code = ResultCode.SUCCESS, message = "", result = result }; } /// @@ -99,7 +99,7 @@ public class BaseResponse : BaseResponse /// public static BaseResponse Success(T result, string message = "操作成功。") { - return new BaseResponse { Code = ResultCode.SUCCESS, Message = message, Result = result }; + return new BaseResponse { code = ResultCode.SUCCESS, message = message, result = result }; } /// @@ -107,7 +107,7 @@ public class BaseResponse : BaseResponse /// public static BaseResponse Fail(string message = "fail") { - return new BaseResponse { Code = ResultCode.FAIL, Message = message }; + return new BaseResponse { code = ResultCode.FAIL, message = message }; } /// @@ -115,12 +115,12 @@ public class BaseResponse : BaseResponse /// public static BaseResponse Fail(ResultCode code, string message = "fail") { - return new BaseResponse { Code = code, Message = message }; + return new BaseResponse { code = code, message = message }; } public static BaseResponse Fail(T result, string message = "操作失败,请稍后再试") { - return new BaseResponse { Code = ResultCode.FAIL, Message = message, Result = result }; + return new BaseResponse { code = ResultCode.FAIL, message = message, result = result }; } /// @@ -128,7 +128,7 @@ public class BaseResponse : BaseResponse /// public static BaseResponse AuthFail(string message = "Permission authentication failed") { - return new BaseResponse { Code = ResultCode.OAUTH_FAIL, Message = message }; + return new BaseResponse { code = ResultCode.OAUTH_FAIL, message = message }; } ///// @@ -149,7 +149,7 @@ public class BaseResponse : BaseResponse /// /// 操作结果 /// - public bool IsSuccess => Code == ResultCode.SUCCESS; + public bool isSuccess => code == ResultCode.SUCCESS; } diff --git a/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInConfigDto.cs b/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInConfigDto.cs new file mode 100644 index 0000000..9209fa5 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInConfigDto.cs @@ -0,0 +1,96 @@ +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.Models.Dto.CheckIn; + +/// +/// 签到配置创建/更新输入 +/// +public class CheckInConfigInput +{ + /// + /// 连续签到天数 + /// + public int DayNumber { get; set; } + + /// + /// 奖励积分数 + /// + public int RewardPoints { get; set; } + + /// + /// 额外奖励积分 + /// + public int BonusPoints { get; set; } + + /// + /// 配置类型: Daily, Streak + /// + public CheckInConfigTypeEnum Type { get; set; } = CheckInConfigTypeEnum.Daily; +} + +/// +/// 签到配置输出 +/// +public class CheckInConfigOutput +{ + /// + /// 主键ID + /// + public long Id { get; set; } + + /// + /// 连续签到天数 + /// + public int DayNumber { get; set; } + + /// + /// 奖励积分数 + /// + public int RewardPoints { get; set; } + + /// + /// 额外奖励积分 + /// + public int BonusPoints { get; set; } + + /// + /// 配置类型: Daily, Streak + /// + public CheckInConfigTypeEnum Type { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } + + /// + /// 创建人 + /// + public string? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 更新人 + /// + public string? UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdatedAt { get; set; } +} + +/// +/// 签到配置分页查询输入 +/// +public class CheckInConfigQueryInput : PageQueryModel +{ + /// + /// 配置类型筛选: Daily, Streak + /// + public CheckInConfigTypeEnum? Type { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs b/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs index 968c0dd..282a502 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs @@ -94,6 +94,17 @@ public class CheckInInfoOutput public List RecentRecords { get; set; } = []; } +/// +/// 补签输入 +/// +public class MakeUpCheckInInput +{ + /// + /// 补签目标日期(yyyy-MM-dd) + /// + public DateTime TargetDate { get; set; } +} + /// /// 签到记录输出 /// @@ -108,6 +119,10 @@ public class CheckInRecordOutput /// 签到日期 /// public DateTime CheckInDate { get; set; } + /// + /// 签到时间 + /// + public DateTime CreatedAt { get; set; } /// /// 连续签到天数 diff --git a/QYZH.InteractiveMagazine.Models/Dto/Mall/ProductDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Mall/ProductDto.cs index 989260d..d2b1f97 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Mall/ProductDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Mall/ProductDto.cs @@ -52,6 +52,16 @@ public class ProductInput /// 库存(-1无限) /// public int Stock { get; set; } = -1; + + /// + /// 是否是虚拟商品 + /// + public bool IsVirtual { get; set; } + + /// + /// Type对应的Id + /// + public long TypeId { get; set; } } /// @@ -128,6 +138,16 @@ public class ProductOutput /// 更新时间 /// public DateTime? UpdatedAt { get; set; } + + /// + /// 是否是虚拟商品 + /// + public bool IsVirtual { get; set; } + + /// + /// Type对应的Id + /// + public long TypeId { get; set; } } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs index 92f6c9f..6d83737 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs @@ -12,6 +12,16 @@ public class WxProductOutput public int Price { get; set; } public string Type { get; set; } = string.Empty; + /// + /// 是否是虚拟商品 + /// + public bool IsVirtual { get; set; } + + /// + /// Type对应的Id + /// + public long TypeId { get; set; } + /// /// 皮肤信息(仅 PetBg 类型有值) /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/SelectOptionDto.cs b/QYZH.InteractiveMagazine.Models/Dto/SelectOptionDto.cs new file mode 100644 index 0000000..d5bf44d --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/SelectOptionDto.cs @@ -0,0 +1,17 @@ +namespace QYZH.InteractiveMagazine.Models.Dto; + +/// +/// 下拉列表选项基类 +/// +public class SelectOptionDto +{ + /// + /// 选项Id + /// + public long Id { get; set; } + + /// + /// 选项显示名称 + /// + public string Name { get; set; } = string.Empty; +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs b/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs index 1af8426..29f0b3f 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UsersDto.cs @@ -1,7 +1,3 @@ -using QYZH.InteractiveMagazine.Models.Dto.CheckIn; -using QYZH.InteractiveMagazine.Models.Dto.Compensation; -using QYZH.InteractiveMagazine.Models.Dto.Points; - namespace QYZH.InteractiveMagazine.Models.Dto; /// @@ -62,7 +58,7 @@ public class UsersOutput } /// -/// 用户详情输出DTO(包含基本信息、积分记录、签到记录、补偿任务、期刊列表) +/// 用户详情输出DTO(仅基本信息) /// public class UserDetailOutput { @@ -70,26 +66,6 @@ public class UserDetailOutput /// 用户基本信息 /// public UsersOutput BasicInfo { get; set; } = new(); - - /// - /// 积分使用记录(最近20条) - /// - public List PointsRecords { get; set; } = []; - - /// - /// 签到记录(最近30条) - /// - public List CheckInRecords { get; set; } = []; - - /// - /// 失败的补偿任务(需要手动处理) - /// - public List FailedCompensationTasks { get; set; } = []; - - /// - /// 用户拥有的期刊列表 - /// - public List Journals { get; set; } = []; } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs b/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs index aed5637..0d45043 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/WeChat/WeChatDto.cs @@ -36,10 +36,15 @@ public class WeChatQuickLoginInput public class WeChatLoginOutput { /// - /// 访问令牌(基于 WxUser,不绑定具体 User) + /// 访问令牌(同时携带 WxUserId 和当前激活 UserId) /// public string Token { get; set; } = string.Empty; + /// + /// 当前激活用户ID(Users.Id,无用户时为 0) + /// + public long CurrentUserId { get; set; } + /// /// 微信用户信息 /// @@ -171,10 +176,26 @@ public class WeChatSwitchUserInput } /// -/// 微信切换用户输出(JWT 基于 WxUser,切换不更换 Token) +/// 修改家长名字输入 +/// +public class UpdateWxUserNameInput +{ + /// + /// 新名字 + /// + public string Name { get; set; } = string.Empty; +} + +/// +/// 微信切换用户输出(重新生成 Token,切换激活用户) /// public class WeChatSwitchUserOutput { + /// + /// 新的访问令牌(指向切换后的用户) + /// + public string Token { get; set; } = string.Empty; + /// /// 当前切换的用户详情 /// diff --git a/QYZH.InteractiveMagazine.Models/Entity/Product.cs b/QYZH.InteractiveMagazine.Models/Entity/Product.cs index 6d97640..29196a7 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/Product.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/Product.cs @@ -9,65 +9,75 @@ namespace QYZH.InteractiveMagazine.Models.Entity [SugarTable("Product")] public partial class Product : SqlSugarBaseEntity { - public Product(){ + public Product() + { - } + } - /// - /// Desc:商品名称 - /// Default: - /// Nullable:False - /// - public string Name {get;set;} + /// + /// Desc:商品名称 + /// Default: + /// Nullable:False + /// + public string Name { get; set; } - /// - /// Desc:描述 - /// Default: - /// Nullable:True - /// - public string Description {get;set;} + /// + /// Desc:描述 + /// Default: + /// Nullable:True + /// + public string Description { get; set; } - /// - /// Desc:商品图片 - /// Default: - /// Nullable:True - /// - public string ImageUrl {get;set;} + /// + /// Desc:商品图片 + /// Default: + /// Nullable:True + /// + public string ImageUrl { get; set; } - /// - /// Desc:所需积分 - /// Default: - /// Nullable:False - /// - public int Price {get;set;} + /// + /// Desc:所需积分 + /// Default: + /// Nullable:False + /// + public int Price { get; set; } - /// - /// Desc:商品类型: MakeUpCard, PetBg - /// Default: - /// Nullable:False - /// - public ProductTypeEnum Type {get;set;} + /// + /// Desc:商品类型: MakeUpCard, PetBg + /// Default: + /// Nullable:False + /// + public ProductTypeEnum Type { get; set; } - /// - /// Desc:扩展数据 - /// Default: - /// Nullable:True - /// - public string MetaData {get;set;} + /// + /// Desc:扩展数据 + /// Default: + /// Nullable:True + /// + public string MetaData { get; set; } - /// - /// Desc:是否上架 - /// Default:b'1' - /// Nullable:False - /// - public bool IsActive {get;set;} + /// + /// Desc:是否上架 + /// Default:b'1' + /// Nullable:False + /// + public bool IsActive { get; set; } + + /// + /// Desc:库存(-1无限) + /// Default:-1 + /// Nullable:False + /// + public int Stock { get; set; } + /// + /// Desc:是否是虚拟商品 + /// + public bool IsVirtual { get; set; } + /// + /// Desc:Type对应的Id + /// + public long TypeId { get; set; } - /// - /// Desc:库存(-1无限) - /// Default:-1 - /// Nullable:False - /// - public int Stock {get;set;} } } diff --git a/QYZH.InteractiveMagazine.Models/Enum/PetSkinTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/PetSkinTypeEnum.cs index f1d782b..0fe7f0b 100644 --- a/QYZH.InteractiveMagazine.Models/Enum/PetSkinTypeEnum.cs +++ b/QYZH.InteractiveMagazine.Models/Enum/PetSkinTypeEnum.cs @@ -7,6 +7,11 @@ namespace QYZH.InteractiveMagazine.Models.Enum; /// public enum PetSkinTypeEnum { + /// + /// 初始皮肤 + /// + [Description("初始皮肤")] + System = 0, /// /// 普通皮肤 /// diff --git a/QYZH.InteractiveMagazine.Service/CheckInConfigService.cs b/QYZH.InteractiveMagazine.Service/CheckInConfigService.cs new file mode 100644 index 0000000..f673a1d --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/CheckInConfigService.cs @@ -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; + +/// +/// 签到配置服务实现 +/// +public class CheckInConfigService( + BaseRepository checkInConfigRepository, + ILogger logger) + : BaseRepository, ICheckInConfigService +{ + /// + /// 创建签到配置 + /// + public async Task 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() + .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); + } + + /// + /// 更新签到配置 + /// + public async Task 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() + .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); + } + + /// + /// 删除签到配置(软删除) + /// + 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() + .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); + } + + /// + /// 根据ID获取签到配置 + /// + public async Task GetByIdAsync(long id) + { + var config = await checkInConfigRepository.GetByIdAsync(id); + if (config == null) + { + throw new BusinessException("签到配置不存在", 404); + } + + return MapToOutput(config); + } + + /// + /// 分页查询签到配置列表 + /// + public async Task> 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 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(new List(), input.PageIndex, input.PageSize, totalNumber); + result.Result = pageResult; + return result; + } + + /// + /// 更新签到配置启用/禁用状态 + /// + public async Task 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; + } + + /// + /// 实体映射为输出DTO + /// + 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 + }; + } +} diff --git a/QYZH.InteractiveMagazine.Service/CheckInService.cs b/QYZH.InteractiveMagazine.Service/CheckInService.cs index 1879361..daee141 100644 --- a/QYZH.InteractiveMagazine.Service/CheckInService.cs +++ b/QYZH.InteractiveMagazine.Service/CheckInService.cs @@ -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() .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() + .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() .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() + .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() + .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( /// /// 获取用户漏签日期列表 /// - public async Task> GetMissedDatesAsync(long userId, int days = 30) + public async Task> GetMissedDatesAsync(long userId, int days = 7) { var startDate = DateTime.Now.Date.AddDays(-days); diff --git a/QYZH.InteractiveMagazine.Service/PetService.cs b/QYZH.InteractiveMagazine.Service/PetService.cs index 8d45d00..94889d1 100644 --- a/QYZH.InteractiveMagazine.Service/PetService.cs +++ b/QYZH.InteractiveMagazine.Service/PetService.cs @@ -1281,4 +1281,23 @@ public class PetService( Images = imagesByStage.GetValueOrDefault(e.Id, new List()) }).ToList(); } + + /// + /// 获取所有皮肤下拉列表(仅返回Id和Name,按SortOrder排序) + /// + public async Task> GetAllSkinsForSelectAsync(List? 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; + } } diff --git a/QYZH.InteractiveMagazine.Service/ProductService.cs b/QYZH.InteractiveMagazine.Service/ProductService.cs index db11059..f75679f 100644 --- a/QYZH.InteractiveMagazine.Service/ProductService.cs +++ b/QYZH.InteractiveMagazine.Service/ProductService.cs @@ -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(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(result, input.PageIndex, input.PageSize, totalNumber); } /// diff --git a/QYZH.InteractiveMagazine.Service/UsersService.cs b/QYZH.InteractiveMagazine.Service/UsersService.cs index 908040c..445ea48 100644 --- a/QYZH.InteractiveMagazine.Service/UsersService.cs +++ b/QYZH.InteractiveMagazine.Service/UsersService.cs @@ -37,7 +37,7 @@ public class UsersService( } /// - /// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表) + /// 获取用户详情(仅基本信息) /// public async Task> GetDetailAsync(long id) { @@ -47,125 +47,124 @@ public class UsersService( return BaseResponse.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.Success(new UserDetailOutput { - BasicInfo = user, - PointsRecords = await pointsTask, - CheckInRecords = await checkInTask, - FailedCompensationTasks = await compensationTask, - Journals = await journalsTask + BasicInfo = user }); } /// - /// 获取用户积分记录(最近20条) + /// 分页查询用户积分记录 /// - private async Task> GetPointsRecordsAsync(long userId) + public async Task>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input) { - try - { - var result = await pointsService.GetPointsRecordsAsync(new PointsRecordQueryInput + input.UserId = userId; + var result = await pointsService.GetPointsRecordsAsync(input); + return BaseResponse>.Success(result); + } + + /// + /// 分页查询用户签到记录 + /// + public async Task>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input) + { + RefAsync total = 0; + var records = await Context.Queryable() + .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(records, input.PageIndex, input.PageSize, total); + return BaseResponse>.Success(page); } /// - /// 获取用户签到记录 + /// 分页查询用户补偿任务 /// - private async Task> GetCheckInRecordsAsync(long userId) + public async Task>> 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 []; - } - } - - /// - /// 获取用户失败的补偿任务(需要手动处理) - /// - private async Task> GetFailedCompensationTasksAsync(long userId) - { - try - { - var tasks = await compensationTaskService.GetTasksAsync(new GetCompensationTasksInput + RefAsync total = 0; + var tasks = await Context.Queryable() + .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(tasks, input.PageIndex, input.PageSize, total); + return BaseResponse>.Success(page); } /// - /// 获取用户拥有的期刊列表(含期刊详情) + /// 分页查询用户期刊列表(含期刊详情) /// - private async Task> GetUserJournalsWithDetailAsync(long userId) + public async Task>> GetUserJournalsAsync(long userId, UserJournalQueryInput input) { - try + RefAsync total = 0; + var items = await Context.Queryable() + .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() - .Where(uj => uj.UserId == userId && !uj.IsDeleted) - .OrderByDescending(uj => uj.CreatedAt) + var journals = await Context.Queryable() + .Where(j => journalIds.Contains(j.Id) && !j.IsDeleted) .ToListAsync(); - - var result = new List(); - foreach (var uj in userJournals) + var journalDict = journals.ToDictionary(j => j.Id); + foreach (var item in items) { - var journal = await Context.Queryable() - .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(items, input.PageIndex, input.PageSize, total); + return BaseResponse>.Success(page); } /// diff --git a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs index e7c2488..26802c8 100644 --- a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs +++ b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs @@ -152,15 +152,16 @@ public class WeChatAuthService( } /// - /// 切换用户(同一 WxUser 下切换 User 身份,JWT 基于 WxUser 无需重新生成) + /// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token) /// - public async Task SwitchUserAsync(long wxUserId, WeChatSwitchUserInput input) + public async Task 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() - .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() .SetColumns(u => u.IsLastOnline == false) - .Where(u => u.WxUserId == wxUserId ) + .Where(u => u.WxUserId == wxUserId && !u.IsDeleted) .ExecuteCommandAsync(); await wxUserRepository.Context.Updateable() .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() - .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); } + /// + /// 修改家长名字(WxUser.Name) + /// + public async Task 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() + .Where(w => w.Id == wxUserId && !w.IsDeleted) + .FirstAsync(); + + if (wxUser == null) + throw new BusinessException("微信用户不存在", 404); + + await wxUserRepository.Context.Updateable() + .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 私有辅助方法 /// - /// 构建登录输出(JWT 基于 WxUser,不绑定具体 User) + /// 构建登录输出(JWT 同时携带 WxUserId 和当前激活 UserId) /// private async Task BuildLoginOutputAsync(WxUser wxUser, List 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, diff --git a/QYZH.InteractiveMagazine.Service/WxMallService.cs b/QYZH.InteractiveMagazine.Service/WxMallService.cs index bdf3dd4..1ee9d00 100644 --- a/QYZH.InteractiveMagazine.Service/WxMallService.cs +++ b/QYZH.InteractiveMagazine.Service/WxMallService.cs @@ -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() - .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() - .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() - .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 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs new file mode 100644 index 0000000..6e9608b --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs @@ -0,0 +1,177 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.CheckIn; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 签到配置管理控制器 +/// +[Route("api/[controller]")] +[ApiController] +[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] +public class CheckInConfigController : BaseController +{ + private readonly ICheckInConfigService _checkInConfigService; + private readonly ILogger _logger; + + public CheckInConfigController(ICheckInConfigService checkInConfigService, ILogger logger) + { + _checkInConfigService = checkInConfigService; + _logger = logger; + } + + /// + /// 创建签到配置 + /// + /// 签到配置信息 + /// 创建的签到配置信息 + [HttpPost] + public async Task> CreateAsync([FromBody] CheckInConfigInput input) + { + try + { + var result = await _checkInConfigService.CreateAsync(input); + return Success(result, "创建签到配置成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "创建签到配置业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "创建签到配置系统异常,参数:{Input}", input); + return BaseResponse.Fail("创建签到配置失败,请稍后重试"); + } + } + + /// + /// 更新签到配置 + /// + /// 签到配置ID + /// 签到配置信息 + /// 更新后的签到配置信息 + [HttpPut("{id}")] + public async Task> UpdateAsync(long id, [FromBody] CheckInConfigInput input) + { + try + { + var result = await _checkInConfigService.UpdateAsync(id, input); + return Success(result, "更新签到配置成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "更新签到配置业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新签到配置系统异常,ID:{Id},参数:{Input}", id, input); + return BaseResponse.Fail("更新签到配置失败,请稍后重试"); + } + } + + /// + /// 删除签到配置(软删除) + /// + /// 签到配置ID + /// 操作结果 + [HttpDelete("{id}")] + public async Task> DeleteAsync(long id) + { + try + { + await _checkInConfigService.DeleteAsync(id); + return Success(new object(), "删除签到配置成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "删除签到配置业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "删除签到配置系统异常,ID:{Id}", id); + return BaseResponse.Fail("删除签到配置失败,请稍后重试"); + } + } + + /// + /// 根据ID获取签到配置 + /// + /// 签到配置ID + /// 签到配置信息 + [HttpGet("{id}")] + public async Task> GetByIdAsync(long id) + { + try + { + var result = await _checkInConfigService.GetByIdAsync(id); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "获取签到配置业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取签到配置系统异常,ID:{Id}", id); + return BaseResponse.Fail("获取签到配置信息失败,请稍后重试"); + } + } + + /// + /// 分页查询签到配置列表 + /// + /// 查询条件 + /// 分页结果 + [HttpPost("list")] + public async Task>> GetListAsync([FromBody] CheckInConfigQueryInput input) + { + try + { + var result = await _checkInConfigService.GetListAsync(input); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "查询签到配置列表业务异常: {Message}", ex.Message); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "查询签到配置列表系统异常,参数:{Input}", input); + return BaseResponse>.Fail("查询签到配置列表失败,请稍后重试"); + } + } + + /// + /// 更新签到配置启用/禁用状态 + /// + /// 签到配置ID + /// 操作结果 + [HttpPut("{id}/status")] + public async Task> UpdateStatusAsync(long id) + { + try + { + var res = await _checkInConfigService.UpdateStatusAsync(id); + return Success(res, "更新签到配置状态成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "更新签到配置状态业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新签到配置状态系统异常,ID:{Id}", id); + return BaseResponse.Fail("更新签到配置状态失败,请稍后重试"); + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs index 7e5c394..85ba2a8 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs @@ -562,4 +562,23 @@ public class PetManageController : BaseController return BaseResponse>.Fail("获取分组皮肤图片失败,请稍后重试"); } } + + /// + /// 获取所有皮肤下拉列表 + /// + /// 皮肤类型列表(可选) + [HttpGet("skins/select")] + public async Task>> GetAllSkinsForSelectAsync([FromQuery] List? types = null) + { + try + { + var result = await _petService.GetAllSkinsForSelectAsync(types); + return Success(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取皮肤下拉列表系统异常"); + return BaseResponse>.Fail("获取皮肤下拉列表失败,请稍后重试"); + } + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs index 4dfc148..ebc6763 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs @@ -3,6 +3,8 @@ 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; namespace QYZH.InteractiveMagazine.WebApi.Controllers; @@ -33,7 +35,7 @@ public class UsersController : BaseController } /// - /// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表) + /// 获取用户详情(仅基本信息) /// [HttpGet("{id}")] public async Task> GetDetail(long id) @@ -41,6 +43,42 @@ public class UsersController : BaseController return await _usersService.GetDetailAsync(id); } + /// + /// 分页查询用户积分记录 + /// + [HttpGet("{id}/pointsRecords")] + public async Task>> GetUserPointsRecords(long id, [FromQuery] PointsRecordQueryInput input) + { + return await _usersService.GetUserPointsRecordsAsync(id, input); + } + + /// + /// 分页查询用户签到记录 + /// + [HttpGet("{id}/checkInRecords")] + public async Task>> GetUserCheckInRecords(long id, [FromQuery] PageQueryModel input) + { + return await _usersService.GetUserCheckInRecordsAsync(id, input); + } + + /// + /// 分页查询用户补偿任务 + /// + [HttpGet("{id}/compensationTasks")] + public async Task>> GetUserCompensationTasks(long id, [FromQuery] CompensationTaskQueryInput input) + { + return await _usersService.GetUserCompensationTasksAsync(id, input); + } + + /// + /// 分页查询用户期刊列表 + /// + [HttpGet("{id}/journals")] + public async Task>> GetUserJournals(long id, [FromQuery] UserJournalQueryInput input) + { + return await _usersService.GetUserJournalsAsync(id, input); + } + /// /// 更新用户状态 /// diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs index 3dc93ce..8133d3b 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs @@ -17,10 +17,10 @@ public class BagController(IWxMallService mallService, ILogger lo [HttpGet("items")] public async Task>> GetBagItems([FromQuery] string? itemType = null) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - var items = await mallService.GetBagItemsAsync(userId.Value, itemType); + var items = await mallService.GetBagItemsAsync(userId, itemType); return Success(items); } @@ -30,10 +30,10 @@ public class BagController(IWxMallService mallService, ILogger lo [HttpPost("useItem")] public async Task> UseItem([FromBody] UseItemInput input) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - var result = await mallService.UseItemAsync(userId.Value, input); + var result = await mallService.UseItemAsync(userId, input); return Success(result); } @@ -43,10 +43,10 @@ public class BagController(IWxMallService mallService, ILogger lo [HttpPost("equipSkin")] public async Task> EquipSkin([FromBody] EquipSkinInput input) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - await mallService.EquipSkinAsync(userId.Value, input); + await mallService.EquipSkinAsync(userId, input); return Success(null!, input.SkinId == 0 ? "已恢复默认皮肤" : "换肤成功"); } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs index 51bbdeb..a03ee23 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs @@ -29,13 +29,13 @@ public class CheckInController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _checkInService.CheckInAsync(userId.Value); + var result = await _checkInService.CheckInAsync(userId); return Success(result, "签到成功"); } catch (BusinessException ex) @@ -59,13 +59,13 @@ public class CheckInController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _checkInService.GetCheckInInfoAsync(userId.Value); + var result = await _checkInService.GetCheckInInfoAsync(userId); return Success(result); } catch (BusinessException ex) @@ -79,4 +79,66 @@ public class CheckInController : WeChatBaseController return BaseResponse.Fail("获取签到信息失败,请稍后重试"); } } + + /// + /// 补签(消耗补签卡,补签历史漏签日期) + /// + /// 补签输入(目标日期) + /// 补签结果(含奖励详情和余额) + [HttpPost("makeUp")] + public async Task> MakeUpCheckInAsync([FromBody] MakeUpCheckInInput input) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await _checkInService.MakeUpCheckInAsync(userId, input.TargetDate); + return Success(result, "补签成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "补签业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "补签系统异常"); + return BaseResponse.Fail("补签失败,请稍后重试"); + } + } + + /// + /// 获取可补签的日期列表(历史漏签日期) + /// + /// 往前查看天数(默认30天) + /// 漏签日期列表 + [HttpGet("missedDates")] + public async Task>> GetMissedDatesAsync([FromQuery] int days = 30) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse>.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await _checkInService.GetMissedDatesAsync(userId, days); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "获取可补签日期业务异常: {Message}", ex.Message); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取可补签日期系统异常"); + return BaseResponse>.Fail("获取可补签日期失败,请稍后重试"); + } + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs index 5fd4cf5..8ca4973 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs @@ -19,13 +19,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await communityService.GetFeedAsync(userId.Value, cursor); + var result = await communityService.GetFeedAsync(userId, cursor); return Success(result); } catch (BusinessException ex) @@ -48,13 +48,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await communityService.RefreshFeedAsync(userId.Value); + var result = await communityService.RefreshFeedAsync(userId); return Success(result); } catch (BusinessException ex) @@ -77,13 +77,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await communityService.LikeAsync(userId.Value, input); + var result = await communityService.LikeAsync(userId, input); return Success(result, "点赞成功"); } catch (BusinessException ex) @@ -106,13 +106,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await communityService.UnlikeAsync(userId.Value, input.MessageId); + var result = await communityService.UnlikeAsync(userId, input.MessageId); return Success(result, "已取消点赞"); } catch (BusinessException ex) diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs index a99738f..0292c27 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs @@ -29,13 +29,13 @@ public class JournalController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _userJournalService.BindJournalAsync(userId.Value, input); + var result = await _userJournalService.BindJournalAsync(userId, input); return Success(result, "绑定期刊成功"); } catch (BusinessException ex) @@ -60,13 +60,13 @@ public class JournalController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse>.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _userJournalService.GetUserJournalsAsync(userId.Value, input); + var result = await _userJournalService.GetUserJournalsAsync(userId, input); return Success(result); } catch (BusinessException ex) diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs index 0cb46b4..90e2f77 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs @@ -17,10 +17,10 @@ public class MallController(IWxMallService mallService, ILogger [HttpGet("products")] public async Task>> GetProducts([FromQuery] string? type = null) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - var products = await mallService.GetProductsAsync(userId.Value, type); + var products = await mallService.GetProductsAsync(userId, type); return Success(products); } @@ -31,10 +31,10 @@ public class MallController(IWxMallService mallService, ILogger [HttpGet("product/{id}")] public async Task> GetProductDetail(long id) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - var product = await mallService.GetProductDetailAsync(userId.Value, id); + var product = await mallService.GetProductDetailAsync(userId, id); if (product == null) return BaseResponse.Fail(ResultCode.DENY, "商品不存在或已下架"); @@ -47,10 +47,10 @@ public class MallController(IWxMallService mallService, ILogger [HttpPost("exchange")] public async Task> Exchange([FromBody] ExchangeInput input) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - var result = await mallService.ExchangeAsync(userId.Value, input); + var result = await mallService.ExchangeAsync(userId, input); return Success(result); } @@ -61,10 +61,10 @@ public class MallController(IWxMallService mallService, ILogger [HttpGet("exchangeRecords")] public async Task>> GetExchangeRecords([FromQuery] int limit = 20) { - var userId = GetCurrentWxUserId(); - if (userId == null) return Fail("未获取到用户信息") as dynamic; + var userId = GetCurrentUserId(); + if (userId == 0) return Fail("未获取到用户信息") as dynamic; - var records = await mallService.GetExchangeRecordsAsync(userId.Value, limit); + var records = await mallService.GetExchangeRecordsAsync(userId, limit); return Success(records); } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs index 52b25a2..42ffac6 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs @@ -18,13 +18,13 @@ public class MedalController(IMedalService medalService, ILogger>.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await medalService.GetAllMedalsAsync(userId.Value); + var result = await medalService.GetAllMedalsAsync(userId); return Success(result); } catch (BusinessException ex) @@ -47,13 +47,13 @@ public class MedalController(IMedalService medalService, ILogger>.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await medalService.GetUserMedalsAsync(userId.Value); + var result = await medalService.GetUserMedalsAsync(userId); return Success(result); } catch (BusinessException ex) @@ -76,13 +76,13 @@ public class MedalController(IMedalService medalService, ILogger.Fail(ResultCode.DENY, "未获取到用户信息"); } - await medalService.ActivateMedalAsync(userId.Value, input); + await medalService.ActivateMedalAsync(userId, input); return Success(null!, "勋章激活成功"); } catch (BusinessException ex) diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs index e883e67..894f79b 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs @@ -28,13 +28,13 @@ public class PetController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var pet = await _petService.GetPetByUserIdAsync(userId.Value); + var pet = await _petService.GetPetByUserIdAsync(userId); if (pet == null) { return BaseResponse.Fail("未找到宠物信息"); @@ -62,13 +62,13 @@ public class PetController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _petService.FeedPetAsync(userId.Value, input); + var result = await _petService.FeedPetAsync(userId, input); return Success(result, "喂养成功"); } catch (BusinessException ex) @@ -91,13 +91,13 @@ public class PetController : WeChatBaseController { try { - var userId = GetCurrentWxUserId(); - if (userId == null) + var userId = GetCurrentUserId(); + if (userId == 0) { return BaseResponse>.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _petService.GetFeedingRecordsAsync(userId.Value, petId, pageQuery); + var result = await _petService.GetFeedingRecordsAsync(userId, petId, pageQuery); return Success(result); } catch (BusinessException ex) diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs index 28d1251..3e0bcae 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs @@ -86,7 +86,8 @@ public class WeChatAuthController : WeChatBaseController if (wxUserId == null) return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); - var result = await _weChatAuthService.SwitchUserAsync(wxUserId.Value, input); + var currentUserId = GetCurrentUserId(); + var result = await _weChatAuthService.SwitchUserAsync(wxUserId.Value, currentUserId, input); return Success(result); } catch (BusinessException ex) @@ -154,4 +155,31 @@ public class WeChatAuthController : WeChatBaseController return BaseResponse.Fail("新增用户失败,请稍后重试"); } } + + /// + /// 修改家长名字(WxUser.Name) + /// + [HttpPost("updateName")] + public async Task> UpdateWxUserNameAsync([FromBody] UpdateWxUserNameInput input) + { + try + { + var wxUserId = GetCurrentWxUserId(); + if (wxUserId == null) + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + + var result = await _weChatAuthService.UpdateWxUserNameAsync(wxUserId.Value, input); + return Success(result, "修改成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "修改家长名字业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "修改家长名字系统异常"); + return BaseResponse.Fail("修改家长名字失败,请稍后重试"); + } + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs index f6d4376..ae97bc9 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Auth; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Enum; using System.Security.Claims; @@ -16,16 +17,30 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; public abstract class WeChatBaseController : ControllerBase { /// - /// 获取当前微信用户ID(WxUser.Id,来自 JWT) + /// 获取当前激活用户ID(Users.Id,来自 JWT NameIdentifier) /// - /// WxUser ID - protected long? GetCurrentWxUserId() + /// User ID(无激活用户时为 0) + protected long GetCurrentUserId() { var userIdClaim = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); if (userIdClaim != null && long.TryParse(userIdClaim.Value, out var userId)) { return userId; } + return 0; + } + + /// + /// 获取当前微信用户ID(WxUser.Id,来自 JWT WxUserId claim) + /// + /// WxUser ID + protected long? GetCurrentWxUserId() + { + var wxUserIdClaim = User.Claims.FirstOrDefault(c => c.Type == JwtHelper.WxUserIdClaimType); + if (wxUserIdClaim != null && long.TryParse(wxUserIdClaim.Value, out var wxUserId)) + { + return wxUserId; + } return null; }