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