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,52 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 签到配置服务接口
/// </summary>
public interface ICheckInConfigService : IBaseService<CheckInConfig>
{
/// <summary>
/// 创建签到配置
/// </summary>
/// <param name="input">签到配置输入</param>
/// <returns>创建的签到配置信息</returns>
Task<CheckInConfigOutput> CreateAsync(CheckInConfigInput input);
/// <summary>
/// 更新签到配置
/// </summary>
/// <param name="id">签到配置ID</param>
/// <param name="input">签到配置输入</param>
/// <returns>更新后的签到配置信息</returns>
Task<CheckInConfigOutput> UpdateAsync(long id, CheckInConfigInput input);
/// <summary>
/// 删除签到配置(软删除)
/// </summary>
/// <param name="id">签到配置ID</param>
Task DeleteAsync(long id);
/// <summary>
/// 根据ID获取签到配置
/// </summary>
/// <param name="id">签到配置ID</param>
/// <returns>签到配置信息</returns>
Task<CheckInConfigOutput> GetByIdAsync(long id);
/// <summary>
/// 分页查询签到配置列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<CheckInConfigOutput>> GetListAsync(CheckInConfigQueryInput input);
/// <summary>
/// 更新签到配置启用/禁用状态
/// </summary>
/// <param name="id">签到配置ID</param>
Task<bool> UpdateStatusAsync(long id);
}

View File

@ -169,4 +169,10 @@ public interface IPetService : IBaseService<UserPet>
/// 根据皮肤Id获取各阶段图片列表按进化阶段分组含阶段基本信息 /// 根据皮肤Id获取各阶段图片列表按进化阶段分组含阶段基本信息
/// </summary> /// </summary>
Task<List<SkinEvolutionStageOutput>> GetSkinImagesGroupedBySkinIdAsync(long skinId); Task<List<SkinEvolutionStageOutput>> GetSkinImagesGroupedBySkinIdAsync(long skinId);
/// <summary>
/// 获取所有皮肤下拉列表仅返回Id和Name按SortOrder排序
/// </summary>
/// <param name="types">皮肤类型列表(可选)</param>
Task<List<SelectOptionDto>> GetAllSkinsForSelectAsync(List<string>? types = null);
} }

View File

@ -1,4 +1,6 @@
using QYZH.InteractiveMagazine.Models.Dto; 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.Dto.Points;
using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Entity;
@ -12,7 +14,7 @@ public interface IUsersService : IBaseService<Users>
Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input); Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input);
/// <summary> /// <summary>
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表 /// 获取用户详情(仅基本信息
/// </summary> /// </summary>
Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id); Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id);
@ -30,4 +32,24 @@ public interface IUsersService : IBaseService<Users>
/// 手动扣除用户积分 /// 手动扣除用户积分
/// </summary> /// </summary>
Task<ManualPointsOutput> ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null); Task<ManualPointsOutput> ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null);
/// <summary>
/// 分页查询用户积分记录
/// </summary>
Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input);
/// <summary>
/// 分页查询用户签到记录
/// </summary>
Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input);
/// <summary>
/// 分页查询用户补偿任务
/// </summary>
Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasksAsync(long userId, CompensationTaskQueryInput input);
/// <summary>
/// 分页查询用户期刊列表(含期刊详情)
/// </summary>
Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournalsAsync(long userId, UserJournalQueryInput input);
} }

View File

@ -19,9 +19,12 @@ public interface IWeChatAuthService : IBaseService<WxUser>
Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input); Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input);
/// <summary> /// <summary>
/// 切换用户(同一 WxUser 下切换 User 身份,JWT 基于 WxUser 无需重新生成 Token /// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token
/// </summary> /// </summary>
Task<WeChatSwitchUserOutput> SwitchUserAsync(long wxUserId, WeChatSwitchUserInput input); /// <param name="wxUserId">当前 WxUser.Id来自 JWT</param>
/// <param name="currentUserId">当前激活 Users.Id来自 JWT用于清除旧 Redis Token</param>
/// <param name="input">切换目标用户参数</param>
Task<WeChatSwitchUserOutput> SwitchUserAsync(long wxUserId, long currentUserId, WeChatSwitchUserInput input);
/// <summary> /// <summary>
/// 获取当前 WxUser 下所有用户列表 /// 获取当前 WxUser 下所有用户列表
@ -37,4 +40,12 @@ public interface IWeChatAuthService : IBaseService<WxUser>
/// <param name="input">新增用户参数</param> /// <param name="input">新增用户参数</param>
/// <returns>新创建的用户信息</returns> /// <returns>新创建的用户信息</returns>
Task<WxUserOutput> CreateUserAsync(long wxUserId, CreateChildUserInput input); Task<WxUserOutput> CreateUserAsync(long wxUserId, CreateChildUserInput input);
/// <summary>
/// 修改家长名字WxUser.Name
/// </summary>
/// <param name="wxUserId">当前登录的 WxUser.Id来自 JWT</param>
/// <param name="input">修改名字参数</param>
/// <returns>更新后的微信用户信息</returns>
Task<WxUserInfoOutput> UpdateWxUserNameAsync(long wxUserId, UpdateWxUserNameInput input);
} }

View File

@ -12,7 +12,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Auth;
public static class JwtHelper public static class JwtHelper
{ {
/// <summary> /// <summary>
/// 生成JWT令牌 /// 生成JWT令牌(管理端 / 单用户场景)
/// </summary> /// </summary>
/// <param name="userId">用户ID</param> /// <param name="userId">用户ID</param>
/// <param name="userName">用户名</param> /// <param name="userName">用户名</param>
@ -29,6 +29,39 @@ public static class JwtHelper
new Claim(ClaimTypes.Name, userName) new Claim(ClaimTypes.Name, userName)
}; };
return BuildToken(claims, settings);
}
/// <summary>
/// 生成JWT令牌小程序场景同时携带 WxUserId 和 UserId
/// </summary>
/// <param name="wxUserId">微信用户IDWxUser.Id</param>
/// <param name="userId">当前激活用户IDUsers.Id无用户时为 0</param>
/// <param name="userName">用户名</param>
/// <param name="settings">JWT配置</param>
/// <returns>JWT令牌字符串</returns>
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);
}
/// <summary>
/// 自定义 Claim 类型WxUserId
/// </summary>
public const string WxUserIdClaimType = "WxUserId";
private static string BuildToken(Claim[] claims, JwtSettings settings)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!)); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
@ -77,6 +110,25 @@ public static class JwtHelper
return null; return null;
} }
/// <summary>
/// 从Token中获取微信用户IDWxUserId
/// </summary>
/// <param name="token">JWT令牌</param>
/// <returns>WxUserId</returns>
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;
}
/// <summary> /// <summary>
/// 从Token中获取用户名 /// 从Token中获取用户名
/// </summary> /// </summary>

View File

@ -24,10 +24,10 @@ public class ModelValidActionFilterAttribute : ActionFilterAttribute
errorDic.Add(key, errorStr); errorDic.Add(key, errorStr);
} }
} }
var result = new BaseResponse<Dictionary<string, string>>() { Code = ResultCode.FAIL }; var result = new BaseResponse<Dictionary<string, string>>() { code = ResultCode.FAIL };
result.Message = string.Join("|", errorDic.Select(e => e.Value).Distinct()); result.message = string.Join("|", errorDic.Select(e => e.Value).Distinct());
result.Result = errorDic; result.result = errorDic;
context.Result = new JsonResult(result); context.Result = new JsonResult(result);
} }
} }

View File

@ -13,7 +13,7 @@ public class BaseResponse
/// </summary> /// </summary>
public BaseResponse() public BaseResponse()
{ {
Code = ResultCode.SUCCESS; code = ResultCode.SUCCESS;
} }
#region #region
@ -21,12 +21,12 @@ public class BaseResponse
/// <summary> /// <summary>
/// 操作描述 /// 操作描述
/// </summary> /// </summary>
public string Message { get; set; } public string message { get; set; }
/// <summary> /// <summary>
/// 结果Code /// 结果Code
/// </summary> /// </summary>
public ResultCode Code { get; set; } public ResultCode code { get; set; }
#endregion #endregion
@ -37,7 +37,7 @@ public class BaseResponse
/// </summary> /// </summary>
public static BaseResponse Success() public static BaseResponse Success()
{ {
return new BaseResponse { Code = ResultCode.SUCCESS, Message = "操作成功。" }; return new BaseResponse { code = ResultCode.SUCCESS, message = "操作成功。" };
} }
/// <summary> /// <summary>
@ -45,7 +45,7 @@ public class BaseResponse
/// </summary> /// </summary>
public static BaseResponse Success(string message = "操作成功。") public static BaseResponse Success(string message = "操作成功。")
{ {
return new BaseResponse { Code = ResultCode.SUCCESS, Message = message }; return new BaseResponse { code = ResultCode.SUCCESS, message = message };
} }
/// <summary> /// <summary>
@ -53,7 +53,7 @@ public class BaseResponse
/// </summary> /// </summary>
public static BaseResponse Fail(string message, ResultCode code = ResultCode.FAIL) public static BaseResponse Fail(string message, ResultCode code = ResultCode.FAIL)
{ {
return new BaseResponse { Code = code, Message = message }; return new BaseResponse { code = code, message = message };
} }
/// <summary> /// <summary>
@ -61,7 +61,7 @@ public class BaseResponse
/// </summary> /// </summary>
public static BaseResponse AuthFail(string message, ResultCode code = ResultCode.FAIL) 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 #endregion
@ -69,7 +69,7 @@ public class BaseResponse
/// <summary> /// <summary>
/// 操作结果 /// 操作结果
/// </summary> /// </summary>
public bool IsSuccess => Code == ResultCode.SUCCESS; public bool isSuccess => code == ResultCode.SUCCESS;
} }
/// <summary> /// <summary>
@ -82,7 +82,7 @@ public class BaseResponse<T> : BaseResponse
/// <summary> /// <summary>
/// 操作结果 /// 操作结果
/// </summary> /// </summary>
public T? Result { get; set; } public T? result { get; set; }
#region #region
@ -91,7 +91,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary> /// </summary>
public static BaseResponse<T> Success(T result) public static BaseResponse<T> Success(T result)
{ {
return new BaseResponse<T> { Code = ResultCode.SUCCESS, Message = "", Result = result }; return new BaseResponse<T> { code = ResultCode.SUCCESS, message = "", result = result };
} }
/// <summary> /// <summary>
@ -99,7 +99,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary> /// </summary>
public static BaseResponse<T> Success(T result, string message = "操作成功。") public static BaseResponse<T> Success(T result, string message = "操作成功。")
{ {
return new BaseResponse<T> { Code = ResultCode.SUCCESS, Message = message, Result = result }; return new BaseResponse<T> { code = ResultCode.SUCCESS, message = message, result = result };
} }
/// <summary> /// <summary>
@ -107,7 +107,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary> /// </summary>
public static BaseResponse<T> Fail(string message = "fail") public static BaseResponse<T> Fail(string message = "fail")
{ {
return new BaseResponse<T> { Code = ResultCode.FAIL, Message = message }; return new BaseResponse<T> { code = ResultCode.FAIL, message = message };
} }
/// <summary> /// <summary>
@ -115,12 +115,12 @@ public class BaseResponse<T> : BaseResponse
/// </summary> /// </summary>
public static BaseResponse<T> Fail(ResultCode code, string message = "fail") public static BaseResponse<T> Fail(ResultCode code, string message = "fail")
{ {
return new BaseResponse<T> { Code = code, Message = message }; return new BaseResponse<T> { code = code, message = message };
} }
public static BaseResponse<T> Fail(T result, string message = "操作失败,请稍后再试") public static BaseResponse<T> Fail(T result, string message = "操作失败,请稍后再试")
{ {
return new BaseResponse<T> { Code = ResultCode.FAIL, Message = message, Result = result }; return new BaseResponse<T> { code = ResultCode.FAIL, message = message, result = result };
} }
/// <summary> /// <summary>
@ -128,7 +128,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary> /// </summary>
public static BaseResponse<T> AuthFail(string message = "Permission authentication failed") public static BaseResponse<T> AuthFail(string message = "Permission authentication failed")
{ {
return new BaseResponse<T> { Code = ResultCode.OAUTH_FAIL, Message = message }; return new BaseResponse<T> { code = ResultCode.OAUTH_FAIL, message = message };
} }
///// <summary> ///// <summary>
@ -149,7 +149,7 @@ public class BaseResponse<T> : BaseResponse
/// <summary> /// <summary>
/// 操作结果 /// 操作结果
/// </summary> /// </summary>
public bool IsSuccess => Code == ResultCode.SUCCESS; public bool isSuccess => code == ResultCode.SUCCESS;
} }

View File

@ -0,0 +1,96 @@
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.CheckIn;
/// <summary>
/// 签到配置创建/更新输入
/// </summary>
public class CheckInConfigInput
{
/// <summary>
/// 连续签到天数
/// </summary>
public int DayNumber { get; set; }
/// <summary>
/// 奖励积分数
/// </summary>
public int RewardPoints { get; set; }
/// <summary>
/// 额外奖励积分
/// </summary>
public int BonusPoints { get; set; }
/// <summary>
/// 配置类型: Daily, Streak
/// </summary>
public CheckInConfigTypeEnum Type { get; set; } = CheckInConfigTypeEnum.Daily;
}
/// <summary>
/// 签到配置输出
/// </summary>
public class CheckInConfigOutput
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 连续签到天数
/// </summary>
public int DayNumber { get; set; }
/// <summary>
/// 奖励积分数
/// </summary>
public int RewardPoints { get; set; }
/// <summary>
/// 额外奖励积分
/// </summary>
public int BonusPoints { get; set; }
/// <summary>
/// 配置类型: Daily, Streak
/// </summary>
public CheckInConfigTypeEnum Type { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 创建人
/// </summary>
public string? CreatedBy { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新人
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// 签到配置分页查询输入
/// </summary>
public class CheckInConfigQueryInput : PageQueryModel
{
/// <summary>
/// 配置类型筛选: Daily, Streak
/// </summary>
public CheckInConfigTypeEnum? Type { get; set; }
}

View File

@ -94,6 +94,17 @@ public class CheckInInfoOutput
public List<CheckInRecordOutput> RecentRecords { get; set; } = []; public List<CheckInRecordOutput> RecentRecords { get; set; } = [];
} }
/// <summary>
/// 补签输入
/// </summary>
public class MakeUpCheckInInput
{
/// <summary>
/// 补签目标日期yyyy-MM-dd
/// </summary>
public DateTime TargetDate { get; set; }
}
/// <summary> /// <summary>
/// 签到记录输出 /// 签到记录输出
/// </summary> /// </summary>
@ -108,6 +119,10 @@ public class CheckInRecordOutput
/// 签到日期 /// 签到日期
/// </summary> /// </summary>
public DateTime CheckInDate { get; set; } public DateTime CheckInDate { get; set; }
/// <summary>
/// 签到时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary> /// <summary>
/// 连续签到天数 /// 连续签到天数

View File

@ -52,6 +52,16 @@ public class ProductInput
/// 库存(-1无限) /// 库存(-1无限)
/// </summary> /// </summary>
public int Stock { get; set; } = -1; public int Stock { get; set; } = -1;
/// <summary>
/// 是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Type对应的Id
/// </summary>
public long TypeId { get; set; }
} }
/// <summary> /// <summary>
@ -128,6 +138,16 @@ public class ProductOutput
/// 更新时间 /// 更新时间
/// </summary> /// </summary>
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Type对应的Id
/// </summary>
public long TypeId { get; set; }
} }
/// <summary> /// <summary>

View File

@ -12,6 +12,16 @@ public class WxProductOutput
public int Price { get; set; } public int Price { get; set; }
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
/// <summary>
/// 是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Type对应的Id
/// </summary>
public long TypeId { get; set; }
/// <summary> /// <summary>
/// 皮肤信息(仅 PetBg 类型有值) /// 皮肤信息(仅 PetBg 类型有值)
/// </summary> /// </summary>

View File

@ -0,0 +1,17 @@
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
/// 下拉列表选项基类
/// </summary>
public class SelectOptionDto
{
/// <summary>
/// 选项Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 选项显示名称
/// </summary>
public string Name { get; set; } = string.Empty;
}

View File

@ -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; namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary> /// <summary>
@ -62,7 +58,7 @@ public class UsersOutput
} }
/// <summary> /// <summary>
/// 用户详情输出DTO包含基本信息、积分记录、签到记录、补偿任务、期刊列表 /// 用户详情输出DTO基本信息)
/// </summary> /// </summary>
public class UserDetailOutput public class UserDetailOutput
{ {
@ -70,26 +66,6 @@ public class UserDetailOutput
/// 用户基本信息 /// 用户基本信息
/// </summary> /// </summary>
public UsersOutput BasicInfo { get; set; } = new(); public UsersOutput BasicInfo { get; set; } = new();
/// <summary>
/// 积分使用记录最近20条
/// </summary>
public List<PointsRecordOutput> PointsRecords { get; set; } = [];
/// <summary>
/// 签到记录最近30条
/// </summary>
public List<CheckInRecordOutput> CheckInRecords { get; set; } = [];
/// <summary>
/// 失败的补偿任务(需要手动处理)
/// </summary>
public List<CompensationTaskOutput> FailedCompensationTasks { get; set; } = [];
/// <summary>
/// 用户拥有的期刊列表
/// </summary>
public List<UserJournalItemOutput> Journals { get; set; } = [];
} }
/// <summary> /// <summary>

View File

@ -36,10 +36,15 @@ public class WeChatQuickLoginInput
public class WeChatLoginOutput public class WeChatLoginOutput
{ {
/// <summary> /// <summary>
/// 访问令牌(基于 WxUser,不绑定具体 User /// 访问令牌(同时携带 WxUserId 和当前激活 UserId
/// </summary> /// </summary>
public string Token { get; set; } = string.Empty; public string Token { get; set; } = string.Empty;
/// <summary>
/// 当前激活用户IDUsers.Id无用户时为 0
/// </summary>
public long CurrentUserId { get; set; }
/// <summary> /// <summary>
/// 微信用户信息 /// 微信用户信息
/// </summary> /// </summary>
@ -171,10 +176,26 @@ public class WeChatSwitchUserInput
} }
/// <summary> /// <summary>
/// 微信切换用户输出JWT 基于 WxUser切换不更换 Token /// 修改家长名字输入
/// </summary>
public class UpdateWxUserNameInput
{
/// <summary>
/// 新名字
/// </summary>
public string Name { get; set; } = string.Empty;
}
/// <summary>
/// 微信切换用户输出(重新生成 Token切换激活用户
/// </summary> /// </summary>
public class WeChatSwitchUserOutput public class WeChatSwitchUserOutput
{ {
/// <summary>
/// 新的访问令牌(指向切换后的用户)
/// </summary>
public string Token { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 当前切换的用户详情 /// 当前切换的用户详情
/// </summary> /// </summary>

View File

@ -9,7 +9,8 @@ namespace QYZH.InteractiveMagazine.Models.Entity
[SugarTable("Product")] [SugarTable("Product")]
public partial class Product : SqlSugarBaseEntity public partial class Product : SqlSugarBaseEntity
{ {
public Product(){ public Product()
{
} }
@ -69,5 +70,14 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
public int Stock { get; set; } public int Stock { get; set; }
/// <summary>
/// Desc:是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Desc:Type对应的Id
/// </summary>
public long TypeId { get; set; }
} }
} }

View File

@ -7,6 +7,11 @@ namespace QYZH.InteractiveMagazine.Models.Enum;
/// </summary> /// </summary>
public enum PetSkinTypeEnum public enum PetSkinTypeEnum
{ {
/// <summary>
/// 初始皮肤
/// </summary>
[Description("初始皮肤")]
System = 0,
/// <summary> /// <summary>
/// 普通皮肤 /// 普通皮肤
/// </summary> /// </summary>

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, GrowthPointsAwarded = growthReward,
ConsecutiveDays = consecutiveDays, ConsecutiveDays = consecutiveDays,
Type = CheckInRecordTypeEnum.Normal, Type = CheckInRecordTypeEnum.Normal,
Status = (int)CheckInRecordStatusEnum.Success Status = (int)CheckInRecordStatusEnum.Success,
CreatedBy = userId.ToString(),
UpdatedBy = userId.ToString()
}; };
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync(); var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
checkInRecord.Id = recordId; checkInRecord.Id = recordId;
@ -198,8 +200,8 @@ public class CheckInService(
.Where(u => u.Id == userId && !u.IsDeleted) .Where(u => u.Id == userId && !u.IsDeleted)
.FirstAsync(); .FirstAsync();
// 最近 30 天签到记录 // 最近 7 天签到记录
var thirtyDaysAgo = today.AddDays(-29); var thirtyDaysAgo = today.AddDays(-6);
var recentRecords = await checkInRecordRepository.Context.Queryable<CheckInRecord>() var recentRecords = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= thirtyDaysAgo) .Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= thirtyDaysAgo)
.OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc) .OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc)
@ -207,6 +209,7 @@ public class CheckInService(
{ {
Id = (long)r.Id, Id = (long)r.Id,
CheckInDate = r.CheckInDate, CheckInDate = r.CheckInDate,
CreatedAt = r.CreatedAt,
ConsecutiveDays = r.ConsecutiveDays, ConsecutiveDays = r.ConsecutiveDays,
PointsAwarded = r.PointsAwarded, PointsAwarded = r.PointsAwarded,
GrowthPointsAwarded = r.GrowthPointsAwarded, GrowthPointsAwarded = r.GrowthPointsAwarded,
@ -247,6 +250,19 @@ public class CheckInService(
if (alreadyCheckedIn) if (alreadyCheckedIn)
throw new BusinessException($"{targetDate:yyyy-MM-dd} 已签到,无需补签", 400); 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>() var user = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted) .Where(u => u.Id == userId && !u.IsDeleted)
@ -304,6 +320,25 @@ public class CheckInService(
Description = $"补签奖励({targetDate:yyyy-MM-dd}" 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.RecordId = (long)recordId;
result.CheckInDate = targetDate; result.CheckInDate = targetDate;
result.ConsecutiveDays = 0; result.ConsecutiveDays = 0;
@ -344,7 +379,7 @@ public class CheckInService(
/// <summary> /// <summary>
/// 获取用户漏签日期列表 /// 获取用户漏签日期列表
/// </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); var startDate = DateTime.Now.Date.AddDays(-days);

View File

@ -1281,4 +1281,23 @@ public class PetService(
Images = imagesByStage.GetValueOrDefault(e.Id, new List<PetSkinImageOutput>()) Images = imagesByStage.GetValueOrDefault(e.Id, new List<PetSkinImageOutput>())
}).ToList(); }).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 Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
@ -52,6 +53,8 @@ public class ProductService(
MetaData = input.MetaData, MetaData = input.MetaData,
IsActive = input.IsActive, IsActive = input.IsActive,
Stock = input.Stock, Stock = input.Stock,
IsVirtual = input.IsVirtual,
TypeId = input.TypeId,
CreatedBy = "System", CreatedBy = "System",
UpdatedBy = "System", UpdatedBy = "System",
CreatedAt = DateTime.Now, CreatedAt = DateTime.Now,
@ -90,6 +93,8 @@ public class ProductService(
MetaData = product.MetaData, MetaData = product.MetaData,
IsActive = product.IsActive, IsActive = product.IsActive,
Stock = product.Stock, Stock = product.Stock,
IsVirtual = product.IsVirtual,
TypeId = product.TypeId,
CreatedBy = product.CreatedBy, CreatedBy = product.CreatedBy,
CreatedAt = product.CreatedAt, CreatedAt = product.CreatedAt,
UpdatedBy = product.UpdatedBy, UpdatedBy = product.UpdatedBy,
@ -144,6 +149,8 @@ public class ProductService(
product.MetaData = input.MetaData; product.MetaData = input.MetaData;
product.IsActive = input.IsActive; product.IsActive = input.IsActive;
product.Stock = input.Stock; product.Stock = input.Stock;
product.IsVirtual = input.IsVirtual;
product.TypeId = input.TypeId;
product.UpdatedBy = "System"; product.UpdatedBy = "System";
product.UpdatedAt = DateTime.Now; product.UpdatedAt = DateTime.Now;
@ -168,6 +175,8 @@ public class ProductService(
MetaData = product.MetaData, MetaData = product.MetaData,
IsActive = product.IsActive, IsActive = product.IsActive,
Stock = product.Stock, Stock = product.Stock,
IsVirtual = product.IsVirtual,
TypeId = product.TypeId,
CreatedBy = product.CreatedBy, CreatedBy = product.CreatedBy,
CreatedAt = product.CreatedAt, CreatedAt = product.CreatedAt,
UpdatedBy = product.UpdatedBy, UpdatedBy = product.UpdatedBy,
@ -218,13 +227,15 @@ public class ProductService(
Id = product.Id, Id = product.Id,
Name = product.Name, Name = product.Name,
Description = product.Description, Description = product.Description,
ImageUrl = product.ImageUrl, ImageUrl = DomainHelper.OssFullUrl(product.ImageUrl),
Price = product.Price, Price = product.Price,
Type = product.Type.ToString(), Type = product.Type.ToString(),
Status = product.Status.ToString(), Status = product.Status.ToString(),
MetaData = product.MetaData, MetaData = product.MetaData,
IsActive = product.IsActive, IsActive = product.IsActive,
Stock = product.Stock, Stock = product.Stock,
IsVirtual = product.IsVirtual,
TypeId = product.TypeId,
CreatedBy = product.CreatedBy, CreatedBy = product.CreatedBy,
CreatedAt = product.CreatedAt, CreatedAt = product.CreatedAt,
UpdatedBy = product.UpdatedBy, UpdatedBy = product.UpdatedBy,
@ -256,26 +267,29 @@ public class ProductService(
.WhereIF(input.Status.HasValue, p => p.Status == (int)input.Status) .WhereIF(input.Status.HasValue, p => p.Status == (int)input.Status)
.WhereIF(input.IsActive.HasValue, p => p.IsActive == input.IsActive.Value) .WhereIF(input.IsActive.HasValue, p => p.IsActive == input.IsActive.Value)
.OrderByDescending(p => p.CreatedAt) .OrderByDescending(p => p.CreatedAt)
.Select(p => new ProductOutput .ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
var result = pageResult.Select(p => new ProductOutput
{ {
Id = p.Id, Id = p.Id,
Name = p.Name, Name = p.Name,
Description = p.Description, Description = p.Description,
ImageUrl = p.ImageUrl, ImageUrl = DomainHelper.OssFullUrl(p.ImageUrl),
Price = p.Price, Price = p.Price,
Type = p.Type.ToString(), Type = p.Type.ToString(),
Status = p.Status.ToString(), Status = p.Status.ToString(),
MetaData = p.MetaData, MetaData = p.MetaData,
IsActive = p.IsActive, IsActive = p.IsActive,
Stock = p.Stock, Stock = p.Stock,
IsVirtual = p.IsVirtual,
TypeId = p.TypeId,
CreatedBy = p.CreatedBy, CreatedBy = p.CreatedBy,
CreatedAt = p.CreatedAt, CreatedAt = p.CreatedAt,
UpdatedBy = p.UpdatedBy, UpdatedBy = p.UpdatedBy,
UpdatedAt = p.UpdatedAt UpdatedAt = p.UpdatedAt
}, true) }).ToList();
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
return new PageListModel<ProductOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber); return new PageListModel<ProductOutput>(result, input.PageIndex, input.PageSize, totalNumber);
} }
/// <summary> /// <summary>

View File

@ -37,7 +37,7 @@ public class UsersService(
} }
/// <summary> /// <summary>
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表 /// 获取用户详情(仅基本信息
/// </summary> /// </summary>
public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id) public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id)
{ {
@ -47,126 +47,125 @@ public class UsersService(
return BaseResponse<UserDetailOutput>.Fail("用户不存在"); return BaseResponse<UserDetailOutput>.Fail("用户不存在");
} }
// 并行查询关联数据
var pointsTask = GetPointsRecordsAsync(id);
var checkInTask = GetCheckInRecordsAsync(id);
var compensationTask = GetFailedCompensationTasksAsync(id);
var journalsTask = GetUserJournalsWithDetailAsync(id);
await Task.WhenAll(pointsTask, checkInTask, compensationTask, journalsTask);
return BaseResponse<UserDetailOutput>.Success(new UserDetailOutput return BaseResponse<UserDetailOutput>.Success(new UserDetailOutput
{ {
BasicInfo = user, BasicInfo = user
PointsRecords = await pointsTask,
CheckInRecords = await checkInTask,
FailedCompensationTasks = await compensationTask,
Journals = await journalsTask
}); });
} }
/// <summary> /// <summary>
/// 获取用户积分记录最近20条 /// 分页查询用户积分记录
/// </summary> /// </summary>
private async Task<List<PointsRecordOutput>> GetPointsRecordsAsync(long userId) public async Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input)
{ {
try input.UserId = userId;
{ var result = await pointsService.GetPointsRecordsAsync(input);
var result = await pointsService.GetPointsRecordsAsync(new PointsRecordQueryInput return BaseResponse<PageListModel<PointsRecordOutput>>.Success(result);
{
UserId = userId,
PageIndex = 1,
PageSize = 20
});
return result.Result ?? [];
}
catch (Exception ex)
{
_logger.LogWarning(ex, "获取用户积分记录失败UserId: {UserId}", userId);
return [];
}
} }
/// <summary> /// <summary>
/// 获取用户签到记录 /// 分页查询用户签到记录
/// </summary> /// </summary>
private async Task<List<CheckInRecordOutput>> GetCheckInRecordsAsync(long userId) public async Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input)
{ {
try 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
{ {
var info = await checkInService.GetCheckInInfoAsync(userId); Id = (long)r.Id,
return info.RecentRecords ?? []; CheckInDate = r.CheckInDate,
} CreatedAt = r.CreatedAt,
catch (Exception ex) ConsecutiveDays = r.ConsecutiveDays,
{ PointsAwarded = r.PointsAwarded,
_logger.LogWarning(ex, "获取用户签到记录失败UserId: {UserId}", userId); GrowthPointsAwarded = r.GrowthPointsAwarded,
return []; 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>
/// 获取用户失败的补偿任务(需要手动处理) /// 分页查询用户补偿任务
/// </summary> /// </summary>
private async Task<List<CompensationTaskOutput>> GetFailedCompensationTasksAsync(long userId) public async Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasksAsync(long userId, CompensationTaskQueryInput input)
{ {
try 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
{ {
var tasks = await compensationTaskService.GetTasksAsync(new GetCompensationTasksInput Id = t.Id,
{ TaskType = (CompensationTaskTypeEnum)t.TaskType,
UserId = userId, BusinessSource = t.BusinessSource,
Status = CompensationTaskStatusEnum.Failed, BusinessId = t.BusinessId,
Limit = 20 UserId = t.UserId,
}); Payload = t.Payload,
return tasks; ErrorMessage = t.ErrorMessage,
} ErrorSource = t.ErrorSource,
catch (Exception ex) RetryCount = t.RetryCount,
{ MaxRetries = t.MaxRetries,
_logger.LogWarning(ex, "获取用户失败补偿任务失败UserId: {UserId}", userId); Status = (CompensationTaskStatusEnum)t.Status,
return []; 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>
/// 获取用户拥有的期刊列表(含期刊详情) /// 分页查询用户期刊列表(含期刊详情)
/// </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>()
var userJournals = await Context.Queryable<UserJournal>()
.Where(uj => uj.UserId == userId && !uj.IsDeleted) .Where(uj => uj.UserId == userId && !uj.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type.ToString() == input.Type)
.OrderByDescending(uj => uj.CreatedAt) .OrderByDescending(uj => uj.CreatedAt)
.ToListAsync(); .Select(uj => new UserJournalItemOutput
var result = new List<UserJournalItemOutput>();
foreach (var uj in userJournals)
{
var journal = await Context.Queryable<Journal>()
.Where(j => j.Id == uj.JournalId && !j.IsDeleted)
.FirstAsync();
if (journal != null)
{
result.Add(new UserJournalItemOutput
{ {
BindId = uj.Id, BindId = uj.Id,
JournalId = uj.JournalId, JournalId = uj.JournalId,
JournalTitle = journal.Title,
CoverImageUrl = journal.CoverImageUrl,
Type = uj.Type.ToString(), Type = uj.Type.ToString(),
Status = uj.Status.ToString(), Status = uj.Status.ToString(),
CreatedAt = uj.CreatedAt CreatedAt = uj.CreatedAt
}); })
} .ToPageListAsync(input.PageIndex, input.PageSize, total);
}
return result; // 填充期刊详情(标题、封面)
} var journalIds = items.Select(i => i.JournalId).Distinct().ToList();
catch (Exception ex) if (journalIds.Count > 0)
{ {
_logger.LogWarning(ex, "获取用户期刊列表失败UserId: {UserId}", userId); var journals = await Context.Queryable<Journal>()
return []; .Where(j => journalIds.Contains(j.Id) && !j.IsDeleted)
.ToListAsync();
var journalDict = journals.ToDictionary(j => j.Id);
foreach (var item in items)
{
if (journalDict.TryGetValue(item.JournalId, out var journal))
{
item.JournalTitle = journal.Title;
item.CoverImageUrl = journal.CoverImageUrl;
} }
} }
}
var page = new PageListModel<UserJournalItemOutput>(items, input.PageIndex, input.PageSize, total);
return BaseResponse<PageListModel<UserJournalItemOutput>>.Success(page);
}
/// <summary> /// <summary>
/// 更新用户状态 /// 更新用户状态

View File

@ -152,15 +152,16 @@ public class WeChatAuthService(
} }
/// <summary> /// <summary>
/// 切换用户(同一 WxUser 下切换 User 身份,JWT 基于 WxUser 无需重新生成) /// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token
/// </summary> /// </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>() var targetUser = await wxUserRepository.Context.Queryable<Users>()
.Where(u => u.Id == input.UserId) .Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync(); .FirstAsync();
if (targetUser == null) if (targetUser == null)
@ -179,23 +180,32 @@ public class WeChatAuthService(
// 更新 IsLastOnline清除所有设置目标为 true // 更新 IsLastOnline清除所有设置目标为 true
await wxUserRepository.Context.Updateable<Users>() await wxUserRepository.Context.Updateable<Users>()
.SetColumns(u => u.IsLastOnline == false) .SetColumns(u => u.IsLastOnline == false)
.Where(u => u.WxUserId == wxUserId ) .Where(u => u.WxUserId == wxUserId && !u.IsDeleted)
.ExecuteCommandAsync(); .ExecuteCommandAsync();
await wxUserRepository.Context.Updateable<Users>() await wxUserRepository.Context.Updateable<Users>()
.SetColumns(u => u.IsLastOnline == true) .SetColumns(u => u.IsLastOnline == true)
.Where(u => u.Id == input.UserId ) .Where(u => u.Id == input.UserId && !u.IsDeleted)
.ExecuteCommandAsync(); .ExecuteCommandAsync();
logger.LogInformation("IsLastOnline 已更新,目标用户 {UserId} 设为 true", input.UserId); logger.LogInformation("IsLastOnline 已更新,目标用户 {UserId} 设为 true", input.UserId);
// 重新查询目标用户获取最新数据 // 重新查询目标用户获取最新数据
var refreshedUser = await wxUserRepository.Context.Queryable<Users>() var refreshedUser = await wxUserRepository.Context.Queryable<Users>()
.Where(u => u.Id == input.UserId) .Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync(); .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 return new WeChatSwitchUserOutput
{ {
Token = token,
User = MapUserToOutput(refreshedUser) User = MapUserToOutput(refreshedUser)
}; };
} }
@ -263,21 +273,66 @@ public class WeChatAuthService(
return MapUserToOutput(newUser); 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 #region
/// <summary> /// <summary>
/// 构建登录输出JWT 基于 WxUser,不绑定具体 User /// 构建登录输出JWT 同时携带 WxUserId 和当前激活 UserId
/// </summary> /// </summary>
private async Task<WeChatLoginOutput> BuildLoginOutputAsync(WxUser wxUser, List<Users> users) private async Task<WeChatLoginOutput> BuildLoginOutputAsync(WxUser wxUser, List<Users> users)
{ {
var jwtSettings = GetJwtSettings(); var activeUser = users.FirstOrDefault(u => u.IsLastOnline) ?? users.FirstOrDefault();
var token = JwtHelper.GenerateToken(wxUser.Id, wxUser.Name ?? wxUser.OpenId, jwtSettings);
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 return new WeChatLoginOutput
{ {
Token = token, Token = token,
CurrentUserId = userId,
WxUser = new WxUserInfoOutput WxUser = new WxUserInfoOutput
{ {
Id = wxUser.Id, Id = wxUser.Id,

View File

@ -103,6 +103,8 @@ public class WxMallService(
ImageUrl = p.ImageUrl, ImageUrl = p.ImageUrl,
Price = p.Price, Price = p.Price,
Type = p.Type.ToString(), Type = p.Type.ToString(),
IsVirtual = p.IsVirtual,
TypeId = p.TypeId,
Owned = owned, Owned = owned,
Skin = skin != null ? new PetSkinBrief Skin = skin != null ? new PetSkinBrief
{ {
@ -169,6 +171,8 @@ public class WxMallService(
ImageUrl = product.ImageUrl, ImageUrl = product.ImageUrl,
Price = product.Price, Price = product.Price,
Type = product.Type.ToString(), Type = product.Type.ToString(),
IsVirtual = product.IsVirtual,
TypeId = product.TypeId,
Owned = owned, Owned = owned,
Skin = skinBrief Skin = skinBrief
}; };
@ -403,7 +407,7 @@ public class WxMallService(
throw new BusinessException("背包物品Id无效", 400); throw new BusinessException("背包物品Id无效", 400);
var bagItem = await exchangeRecordRepository.Context.Queryable<UserBag>() 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(); .FirstAsync();
if (bagItem == null) if (bagItem == null)
@ -437,28 +441,9 @@ public class WxMallService(
if (targetDate >= DateTime.Now.Date) if (targetDate >= DateTime.Now.Date)
throw new BusinessException("只能补签过去的日期", 400); throw new BusinessException("只能补签过去的日期", 400);
// 调用签到服务执行补签 // 调用签到服务执行补签(内部会检查并扣减补签卡)
var checkInResult = await checkInService.MakeUpCheckInAsync(userId, targetDate); 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); logger.LogInformation("补签卡使用成功UserId: {UserId}, TargetDate: {Date}", userId, targetDate);
return new UseItemOutput return new UseItemOutput

View File

@ -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;
/// <summary>
/// 签到配置管理控制器
/// </summary>
[Route("api/[controller]")]
[ApiController]
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
public class CheckInConfigController : BaseController
{
private readonly ICheckInConfigService _checkInConfigService;
private readonly ILogger<CheckInConfigController> _logger;
public CheckInConfigController(ICheckInConfigService checkInConfigService, ILogger<CheckInConfigController> logger)
{
_checkInConfigService = checkInConfigService;
_logger = logger;
}
/// <summary>
/// 创建签到配置
/// </summary>
/// <param name="input">签到配置信息</param>
/// <returns>创建的签到配置信息</returns>
[HttpPost]
public async Task<BaseResponse<CheckInConfigOutput>> 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<CheckInConfigOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建签到配置系统异常,参数:{Input}", input);
return BaseResponse<CheckInConfigOutput>.Fail("创建签到配置失败,请稍后重试");
}
}
/// <summary>
/// 更新签到配置
/// </summary>
/// <param name="id">签到配置ID</param>
/// <param name="input">签到配置信息</param>
/// <returns>更新后的签到配置信息</returns>
[HttpPut("{id}")]
public async Task<BaseResponse<CheckInConfigOutput>> 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<CheckInConfigOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "更新签到配置系统异常ID{Id},参数:{Input}", id, input);
return BaseResponse<CheckInConfigOutput>.Fail("更新签到配置失败,请稍后重试");
}
}
/// <summary>
/// 删除签到配置(软删除)
/// </summary>
/// <param name="id">签到配置ID</param>
/// <returns>操作结果</returns>
[HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id)
{
try
{
await _checkInConfigService.DeleteAsync(id);
return Success(new object(), "删除签到配置成功");
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "删除签到配置业务异常: {Message}", ex.Message);
return BaseResponse<object>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "删除签到配置系统异常ID{Id}", id);
return BaseResponse<object>.Fail("删除签到配置失败,请稍后重试");
}
}
/// <summary>
/// 根据ID获取签到配置
/// </summary>
/// <param name="id">签到配置ID</param>
/// <returns>签到配置信息</returns>
[HttpGet("{id}")]
public async Task<BaseResponse<CheckInConfigOutput>> GetByIdAsync(long id)
{
try
{
var result = await _checkInConfigService.GetByIdAsync(id);
return Success(result);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "获取签到配置业务异常: {Message}", ex.Message);
return BaseResponse<CheckInConfigOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取签到配置系统异常ID{Id}", id);
return BaseResponse<CheckInConfigOutput>.Fail("获取签到配置信息失败,请稍后重试");
}
}
/// <summary>
/// 分页查询签到配置列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
[HttpPost("list")]
public async Task<BaseResponse<PageListModel<CheckInConfigOutput>>> 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<PageListModel<CheckInConfigOutput>>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "查询签到配置列表系统异常,参数:{Input}", input);
return BaseResponse<PageListModel<CheckInConfigOutput>>.Fail("查询签到配置列表失败,请稍后重试");
}
}
/// <summary>
/// 更新签到配置启用/禁用状态
/// </summary>
/// <param name="id">签到配置ID</param>
/// <returns>操作结果</returns>
[HttpPut("{id}/status")]
public async Task<BaseResponse<bool>> UpdateStatusAsync(long id)
{
try
{
var res = await _checkInConfigService.UpdateStatusAsync(id);
return Success(res, "更新签到配置状态成功");
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "更新签到配置状态业务异常: {Message}", ex.Message);
return BaseResponse<bool>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "更新签到配置状态系统异常ID{Id}", id);
return BaseResponse<bool>.Fail("更新签到配置状态失败,请稍后重试");
}
}
}

View File

@ -562,4 +562,23 @@ public class PetManageController : BaseController
return BaseResponse<List<SkinEvolutionStageOutput>>.Fail("获取分组皮肤图片失败,请稍后重试"); return BaseResponse<List<SkinEvolutionStageOutput>>.Fail("获取分组皮肤图片失败,请稍后重试");
} }
} }
/// <summary>
/// 获取所有皮肤下拉列表
/// </summary>
/// <param name="types">皮肤类型列表(可选)</param>
[HttpGet("skins/select")]
public async Task<BaseResponse<List<SelectOptionDto>>> GetAllSkinsForSelectAsync([FromQuery] List<string>? types = null)
{
try
{
var result = await _petService.GetAllSkinsForSelectAsync(types);
return Success(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取皮肤下拉列表系统异常");
return BaseResponse<List<SelectOptionDto>>.Fail("获取皮肤下拉列表失败,请稍后重试");
}
}
} }

View File

@ -3,6 +3,8 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; 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.Dto.Points;
namespace QYZH.InteractiveMagazine.WebApi.Controllers; namespace QYZH.InteractiveMagazine.WebApi.Controllers;
@ -33,7 +35,7 @@ public class UsersController : BaseController
} }
/// <summary> /// <summary>
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表 /// 获取用户详情(仅基本信息
/// </summary> /// </summary>
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<BaseResponse<UserDetailOutput>> GetDetail(long id) public async Task<BaseResponse<UserDetailOutput>> GetDetail(long id)
@ -41,6 +43,42 @@ public class UsersController : BaseController
return await _usersService.GetDetailAsync(id); return await _usersService.GetDetailAsync(id);
} }
/// <summary>
/// 分页查询用户积分记录
/// </summary>
[HttpGet("{id}/pointsRecords")]
public async Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecords(long id, [FromQuery] PointsRecordQueryInput input)
{
return await _usersService.GetUserPointsRecordsAsync(id, input);
}
/// <summary>
/// 分页查询用户签到记录
/// </summary>
[HttpGet("{id}/checkInRecords")]
public async Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecords(long id, [FromQuery] PageQueryModel input)
{
return await _usersService.GetUserCheckInRecordsAsync(id, input);
}
/// <summary>
/// 分页查询用户补偿任务
/// </summary>
[HttpGet("{id}/compensationTasks")]
public async Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasks(long id, [FromQuery] CompensationTaskQueryInput input)
{
return await _usersService.GetUserCompensationTasksAsync(id, input);
}
/// <summary>
/// 分页查询用户期刊列表
/// </summary>
[HttpGet("{id}/journals")]
public async Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournals(long id, [FromQuery] UserJournalQueryInput input)
{
return await _usersService.GetUserJournalsAsync(id, input);
}
/// <summary> /// <summary>
/// 更新用户状态 /// 更新用户状态
/// </summary> /// </summary>

View File

@ -17,10 +17,10 @@ public class BagController(IWxMallService mallService, ILogger<BagController> lo
[HttpGet("items")] [HttpGet("items")]
public async Task<BaseResponse<List<UserBagOutput>>> GetBagItems([FromQuery] string? itemType = null) public async Task<BaseResponse<List<UserBagOutput>>> GetBagItems([FromQuery] string? itemType = null)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; 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); return Success(items);
} }
@ -30,10 +30,10 @@ public class BagController(IWxMallService mallService, ILogger<BagController> lo
[HttpPost("useItem")] [HttpPost("useItem")]
public async Task<BaseResponse<UseItemOutput>> UseItem([FromBody] UseItemInput input) public async Task<BaseResponse<UseItemOutput>> UseItem([FromBody] UseItemInput input)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; 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); return Success(result);
} }
@ -43,10 +43,10 @@ public class BagController(IWxMallService mallService, ILogger<BagController> lo
[HttpPost("equipSkin")] [HttpPost("equipSkin")]
public async Task<BaseResponse<object>> EquipSkin([FromBody] EquipSkinInput input) public async Task<BaseResponse<object>> EquipSkin([FromBody] EquipSkinInput input)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; if (userId == 0) return Fail("未获取到用户信息") as dynamic;
await mallService.EquipSkinAsync(userId.Value, input); await mallService.EquipSkinAsync(userId, input);
return Success<object>(null!, input.SkinId == 0 ? "已恢复默认皮肤" : "换肤成功"); return Success<object>(null!, input.SkinId == 0 ? "已恢复默认皮肤" : "换肤成功");
} }
} }

View File

@ -29,13 +29,13 @@ public class CheckInController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<CheckInOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<CheckInOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await _checkInService.CheckInAsync(userId.Value); var result = await _checkInService.CheckInAsync(userId);
return Success(result, "签到成功"); return Success(result, "签到成功");
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -59,13 +59,13 @@ public class CheckInController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<CheckInInfoOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<CheckInInfoOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await _checkInService.GetCheckInInfoAsync(userId.Value); var result = await _checkInService.GetCheckInInfoAsync(userId);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -79,4 +79,66 @@ public class CheckInController : WeChatBaseController
return BaseResponse<CheckInInfoOutput>.Fail("获取签到信息失败,请稍后重试"); return BaseResponse<CheckInInfoOutput>.Fail("获取签到信息失败,请稍后重试");
} }
} }
/// <summary>
/// 补签(消耗补签卡,补签历史漏签日期)
/// </summary>
/// <param name="input">补签输入(目标日期)</param>
/// <returns>补签结果(含奖励详情和余额)</returns>
[HttpPost("makeUp")]
public async Task<BaseResponse<CheckInOutput>> MakeUpCheckInAsync([FromBody] MakeUpCheckInInput input)
{
try
{
var userId = GetCurrentUserId();
if (userId == 0)
{
return BaseResponse<CheckInOutput>.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<CheckInOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "补签系统异常");
return BaseResponse<CheckInOutput>.Fail("补签失败,请稍后重试");
}
}
/// <summary>
/// 获取可补签的日期列表(历史漏签日期)
/// </summary>
/// <param name="days">往前查看天数默认30天</param>
/// <returns>漏签日期列表</returns>
[HttpGet("missedDates")]
public async Task<BaseResponse<List<DateTime>>> GetMissedDatesAsync([FromQuery] int days = 30)
{
try
{
var userId = GetCurrentUserId();
if (userId == 0)
{
return BaseResponse<List<DateTime>>.Fail(ResultCode.DENY, "未获取到用户信息");
}
var result = await _checkInService.GetMissedDatesAsync(userId, days);
return Success(result);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "获取可补签日期业务异常: {Message}", ex.Message);
return BaseResponse<List<DateTime>>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取可补签日期系统异常");
return BaseResponse<List<DateTime>>.Fail("获取可补签日期失败,请稍后重试");
}
}
} }

View File

@ -19,13 +19,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<WxFeedOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<WxFeedOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await communityService.GetFeedAsync(userId.Value, cursor); var result = await communityService.GetFeedAsync(userId, cursor);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -48,13 +48,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<WxFeedOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<WxFeedOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await communityService.RefreshFeedAsync(userId.Value); var result = await communityService.RefreshFeedAsync(userId);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -77,13 +77,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<WxLikeOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<WxLikeOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await communityService.LikeAsync(userId.Value, input); var result = await communityService.LikeAsync(userId, input);
return Success(result, "点赞成功"); return Success(result, "点赞成功");
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -106,13 +106,13 @@ public class CommunityController(IWeChatCommunityService communityService, ILogg
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<WxLikeOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<WxLikeOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await communityService.UnlikeAsync(userId.Value, input.MessageId); var result = await communityService.UnlikeAsync(userId, input.MessageId);
return Success(result, "已取消点赞"); return Success(result, "已取消点赞");
} }
catch (BusinessException ex) catch (BusinessException ex)

View File

@ -29,13 +29,13 @@ public class JournalController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<BindJournalOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<BindJournalOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await _userJournalService.BindJournalAsync(userId.Value, input); var result = await _userJournalService.BindJournalAsync(userId, input);
return Success(result, "绑定期刊成功"); return Success(result, "绑定期刊成功");
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -60,13 +60,13 @@ public class JournalController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<PageListModel<BindJournalOutput>>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<PageListModel<BindJournalOutput>>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await _userJournalService.GetUserJournalsAsync(userId.Value, input); var result = await _userJournalService.GetUserJournalsAsync(userId, input);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)

View File

@ -17,10 +17,10 @@ public class MallController(IWxMallService mallService, ILogger<MallController>
[HttpGet("products")] [HttpGet("products")]
public async Task<BaseResponse<List<WxProductOutput>>> GetProducts([FromQuery] string? type = null) public async Task<BaseResponse<List<WxProductOutput>>> GetProducts([FromQuery] string? type = null)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; 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); return Success(products);
} }
@ -31,10 +31,10 @@ public class MallController(IWxMallService mallService, ILogger<MallController>
[HttpGet("product/{id}")] [HttpGet("product/{id}")]
public async Task<BaseResponse<WxProductOutput>> GetProductDetail(long id) public async Task<BaseResponse<WxProductOutput>> GetProductDetail(long id)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; 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) if (product == null)
return BaseResponse<WxProductOutput>.Fail(ResultCode.DENY, "商品不存在或已下架"); return BaseResponse<WxProductOutput>.Fail(ResultCode.DENY, "商品不存在或已下架");
@ -47,10 +47,10 @@ public class MallController(IWxMallService mallService, ILogger<MallController>
[HttpPost("exchange")] [HttpPost("exchange")]
public async Task<BaseResponse<ExchangeOutput>> Exchange([FromBody] ExchangeInput input) public async Task<BaseResponse<ExchangeOutput>> Exchange([FromBody] ExchangeInput input)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; 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); return Success(result);
} }
@ -61,10 +61,10 @@ public class MallController(IWxMallService mallService, ILogger<MallController>
[HttpGet("exchangeRecords")] [HttpGet("exchangeRecords")]
public async Task<BaseResponse<List<ExchangeRecordOutput>>> GetExchangeRecords([FromQuery] int limit = 20) public async Task<BaseResponse<List<ExchangeRecordOutput>>> GetExchangeRecords([FromQuery] int limit = 20)
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) return Fail("未获取到用户信息") as dynamic; 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); return Success(records);
} }
} }

View File

@ -18,13 +18,13 @@ public class MedalController(IMedalService medalService, ILogger<MedalController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<List<WxMedalListOutput>>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<List<WxMedalListOutput>>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await medalService.GetAllMedalsAsync(userId.Value); var result = await medalService.GetAllMedalsAsync(userId);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -47,13 +47,13 @@ public class MedalController(IMedalService medalService, ILogger<MedalController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<List<WxUserMedalOutput>>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<List<WxUserMedalOutput>>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await medalService.GetUserMedalsAsync(userId.Value); var result = await medalService.GetUserMedalsAsync(userId);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -76,13 +76,13 @@ public class MedalController(IMedalService medalService, ILogger<MedalController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<object>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<object>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
await medalService.ActivateMedalAsync(userId.Value, input); await medalService.ActivateMedalAsync(userId, input);
return Success<object>(null!, "勋章激活成功"); return Success<object>(null!, "勋章激活成功");
} }
catch (BusinessException ex) catch (BusinessException ex)

View File

@ -28,13 +28,13 @@ public class PetController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<PetOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<PetOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var pet = await _petService.GetPetByUserIdAsync(userId.Value); var pet = await _petService.GetPetByUserIdAsync(userId);
if (pet == null) if (pet == null)
{ {
return BaseResponse<PetOutput>.Fail("未找到宠物信息"); return BaseResponse<PetOutput>.Fail("未找到宠物信息");
@ -62,13 +62,13 @@ public class PetController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<FeedPetOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<FeedPetOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await _petService.FeedPetAsync(userId.Value, input); var result = await _petService.FeedPetAsync(userId, input);
return Success(result, "喂养成功"); return Success(result, "喂养成功");
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -91,13 +91,13 @@ public class PetController : WeChatBaseController
{ {
try try
{ {
var userId = GetCurrentWxUserId(); var userId = GetCurrentUserId();
if (userId == null) if (userId == 0)
{ {
return BaseResponse<PageListModel<FeedingRecordOutput>>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<PageListModel<FeedingRecordOutput>>.Fail(ResultCode.DENY, "未获取到用户信息");
} }
var result = await _petService.GetFeedingRecordsAsync(userId.Value, petId, pageQuery); var result = await _petService.GetFeedingRecordsAsync(userId, petId, pageQuery);
return Success(result); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)

View File

@ -86,7 +86,8 @@ public class WeChatAuthController : WeChatBaseController
if (wxUserId == null) if (wxUserId == null)
return BaseResponse<WeChatSwitchUserOutput>.Fail(ResultCode.DENY, "未获取到用户信息"); return BaseResponse<WeChatSwitchUserOutput>.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); return Success(result);
} }
catch (BusinessException ex) catch (BusinessException ex)
@ -154,4 +155,31 @@ public class WeChatAuthController : WeChatBaseController
return BaseResponse<WxUserOutput>.Fail("新增用户失败,请稍后重试"); return BaseResponse<WxUserOutput>.Fail("新增用户失败,请稍后重试");
} }
} }
/// <summary>
/// 修改家长名字WxUser.Name
/// </summary>
[HttpPost("updateName")]
public async Task<BaseResponse<WxUserInfoOutput>> UpdateWxUserNameAsync([FromBody] UpdateWxUserNameInput input)
{
try
{
var wxUserId = GetCurrentWxUserId();
if (wxUserId == null)
return BaseResponse<WxUserInfoOutput>.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<WxUserInfoOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "修改家长名字系统异常");
return BaseResponse<WxUserInfoOutput>.Fail("修改家长名字失败,请稍后重试");
}
}
} }

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
using System.Security.Claims; using System.Security.Claims;
@ -16,16 +17,30 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat;
public abstract class WeChatBaseController : ControllerBase public abstract class WeChatBaseController : ControllerBase
{ {
/// <summary> /// <summary>
/// 获取当前微信用户IDWxUser.Id来自 JWT /// 获取当前激活用户IDUsers.Id来自 JWT NameIdentifier
/// </summary> /// </summary>
/// <returns>WxUser ID</returns> /// <returns>User ID(无激活用户时为 0</returns>
protected long? GetCurrentWxUserId() protected long GetCurrentUserId()
{ {
var userIdClaim = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); var userIdClaim = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (userIdClaim != null && long.TryParse(userIdClaim.Value, out var userId)) if (userIdClaim != null && long.TryParse(userIdClaim.Value, out var userId))
{ {
return userId; return userId;
} }
return 0;
}
/// <summary>
/// 获取当前微信用户IDWxUser.Id来自 JWT WxUserId claim
/// </summary>
/// <returns>WxUser ID</returns>
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; return null;
} }