feat: 新增签到、宠物、期刊绑定、补偿任务等业务模块,优化微信登录流程
本次提交完成了多个核心业务模块的开发与优化: 1. 宠物模块:新增宠物实体、服务接口与实现,支持创建默认宠物、激活、喂养、进化以及喂养记录查询 2. 签到模块:新增签到实体、服务接口、控制器以及相关DTO,支持用户签到和签到信息查询,新增成长值奖励字段 3. 期刊绑定模块:新增用户期刊关联实体、服务接口与控制器,支持扫码绑定期刊、解绑和查询绑定列表 4. 补偿任务模块:新增补偿任务实体、服务接口与实现,用于处理业务失败后的异步重试补偿 5. 优化微信登录流程:拆分登录与快捷登录接口,支持手机号获取,新增首次登录自动创建默认宠物逻辑 6. 调整基础路由与实体状态:修改微信控制器路由前缀,更新宠物状态枚举与默认值
This commit is contained in:
24
QYZH.InteractiveMagazine.IService/ICheckInService.cs
Normal file
24
QYZH.InteractiveMagazine.IService/ICheckInService.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ICheckInService : IBaseService<CheckInRecord>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户签到
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <returns>签到结果</returns>
|
||||||
|
Task<CheckInOutput> CheckInAsync(long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户签到信息(今日状态 + 连续天数 + 最近记录)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <returns>签到信息</returns>
|
||||||
|
Task<CheckInInfoOutput> GetCheckInInfoAsync(long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿任务服务接口(仅负责记录和状态管理,处理逻辑由外部项目实现)
|
||||||
|
/// </summary>
|
||||||
|
public interface ICompensationTaskService : IBaseService<CompensationTask>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建补偿任务(公共调用入口)
|
||||||
|
/// 当业务操作部分成功但某个后续步骤失败时调用,记录失败操作以便后续重试
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">补偿任务创建参数</param>
|
||||||
|
/// <returns>补偿任务Id</returns>
|
||||||
|
Task<long> CreateTaskAsync(CreateCompensationTaskInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取待处理的补偿任务列表(Pending + 已过 ScheduledAt 的任务)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="limit">每次获取数量上限</param>
|
||||||
|
/// <returns>待处理任务列表</returns>
|
||||||
|
Task<List<CompensationTaskOutput>> GetPendingTasksAsync(int limit = 50);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按条件查询补偿任务(供外部项目按类型/状态/来源拉取)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询条件</param>
|
||||||
|
/// <returns>任务列表</returns>
|
||||||
|
Task<List<CompensationTaskOutput>> GetTasksAsync(GetCompensationTasksInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新补偿任务状态(供外部处理项目回调更新处理结果)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">任务Id</param>
|
||||||
|
/// <param name="input">状态更新参数</param>
|
||||||
|
Task UpdateTaskStatusAsync(long taskId, UpdateCompensationStatusInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消补偿任务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">任务Id</param>
|
||||||
|
/// <param name="reason">取消原因</param>
|
||||||
|
Task CancelTaskAsync(long taskId, string reason);
|
||||||
|
}
|
||||||
47
QYZH.InteractiveMagazine.IService/IPetService.cs
Normal file
47
QYZH.InteractiveMagazine.IService/IPetService.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IPetService : IBaseService<Pet>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户宠物信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <returns>宠物信息</returns>
|
||||||
|
Task<PetOutput?> GetPetByUserIdAsync(long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为用户创建默认宠物(最低形态、成长值为0、未激活状态)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
Task CreateDefaultPetAsync(long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 激活宠物(将状态从 Inactive 改为 Active)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
Task ActivatePetAsync(long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养宠物(增加成长值 + 记录喂养记录 + 触发进化检查)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <param name="input">喂养输入</param>
|
||||||
|
/// <returns>喂养结果</returns>
|
||||||
|
Task<FeedPetOutput> FeedPetAsync(long userId, FeedPetInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取宠物喂养记录列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <param name="petId">宠物Id</param>
|
||||||
|
/// <returns>喂养记录列表</returns>
|
||||||
|
Task<PageListModel<FeedingRecordOutput>> GetFeedingRecordsAsync(long userId, long petId, PageQueryModel pageQuery);
|
||||||
|
}
|
||||||
33
QYZH.InteractiveMagazine.IService/IUserJournalService.cs
Normal file
33
QYZH.InteractiveMagazine.IService/IUserJournalService.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户期刊关联服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserJournalService : IBaseService<UserJournal>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户绑定期刊(扫码绑定)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">当前用户Id</param>
|
||||||
|
/// <param name="input">绑定输入</param>
|
||||||
|
/// <returns>绑定结果</returns>
|
||||||
|
Task<BindJournalOutput> BindJournalAsync(long userId, BindJournalInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户的期刊绑定列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <param name="input">查询条件</param>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
Task<PageListModel<BindJournalOutput>> GetUserJournalsAsync(long userId, UserJournalQueryInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消期刊绑定
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <param name="id">绑定记录Id</param>
|
||||||
|
Task UnbindJournalAsync(long userId, long id);
|
||||||
|
}
|
||||||
@ -9,12 +9,19 @@ namespace QYZH.InteractiveMagazine.IService;
|
|||||||
public interface IWeChatAuthService : IBaseService<Users>
|
public interface IWeChatAuthService : IBaseService<Users>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信小程序一键登录
|
/// 微信小程序登录(首次创建用户,非首次直接登录)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">登录输入(含微信 code)</param>
|
/// <param name="input">登录输入(含微信 code 和可选的手机号 code)</param>
|
||||||
/// <returns>登录结果(含 Token 和用户信息)</returns>
|
/// <returns>登录结果(含 Token 和用户信息)</returns>
|
||||||
Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input);
|
Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">快捷登录输入(含 OpenId)</param>
|
||||||
|
/// <returns>登录结果(含 Token 和用户列表)</returns>
|
||||||
|
Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 切换用户(同一 OpenId 下切换身份)
|
/// 切换用户(同一 OpenId 下切换身份)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
136
QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs
Normal file
136
QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到输出
|
||||||
|
/// </summary>
|
||||||
|
public class CheckInOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 签到记录Id
|
||||||
|
/// </summary>
|
||||||
|
public long RecordId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到日期
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CheckInDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连续签到天数
|
||||||
|
/// </summary>
|
||||||
|
public int ConsecutiveDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次获得积分
|
||||||
|
/// </summary>
|
||||||
|
public int PointsAwarded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次获得成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPointsAwarded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到后用户积分余额
|
||||||
|
/// </summary>
|
||||||
|
public int PointsBalance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到后用户成长值余额
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPointsBalance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否有宠物(成长值是否喂养到宠物)
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPet { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物是否触发进化
|
||||||
|
/// </summary>
|
||||||
|
public bool HasEvolved { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进化后的阶段名称(未进化则为空)
|
||||||
|
/// </summary>
|
||||||
|
public string? EvolvedStageName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到信息输出(查询用)
|
||||||
|
/// </summary>
|
||||||
|
public class CheckInInfoOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 今日是否已签到
|
||||||
|
/// </summary>
|
||||||
|
public bool HasCheckedInToday { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前连续签到天数
|
||||||
|
/// </summary>
|
||||||
|
public int ConsecutiveDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计签到天数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCheckInDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户当前积分余额
|
||||||
|
/// </summary>
|
||||||
|
public int PointsBalance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户当前成长值余额
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPointsBalance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最近签到记录
|
||||||
|
/// </summary>
|
||||||
|
public List<CheckInRecordOutput> RecentRecords { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到记录输出
|
||||||
|
/// </summary>
|
||||||
|
public class CheckInRecordOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录Id
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到日期
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CheckInDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连续签到天数
|
||||||
|
/// </summary>
|
||||||
|
public int ConsecutiveDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次获得积分
|
||||||
|
/// </summary>
|
||||||
|
public int PointsAwarded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次获得成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPointsAwarded { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到类型: Normal, MakeUp
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿任务类型常量
|
||||||
|
/// </summary>
|
||||||
|
public static class CompensationTaskType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物喂养补偿
|
||||||
|
/// Payload: { "PetId": long, "GrowthPoints": int }
|
||||||
|
/// </summary>
|
||||||
|
public const string PetFeeding = "PetFeeding";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户积分补偿
|
||||||
|
/// Payload: { "Points": int, "ChangeType": string, "Description": string }
|
||||||
|
/// </summary>
|
||||||
|
public const string UserPoints = "UserPoints";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户成长值补偿
|
||||||
|
/// Payload: { "GrowthPoints": int }
|
||||||
|
/// </summary>
|
||||||
|
public const string UserGrowth = "UserGrowth";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送通知补偿
|
||||||
|
/// Payload: { "TemplateId": string, "Data": object }
|
||||||
|
/// </summary>
|
||||||
|
public const string SendNotification = "SendNotification";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿任务状态常量
|
||||||
|
/// </summary>
|
||||||
|
public static class CompensationTaskStatus
|
||||||
|
{
|
||||||
|
public const string Pending = "Pending";
|
||||||
|
public const string Processing = "Processing";
|
||||||
|
public const string Success = "Success";
|
||||||
|
public const string Failed = "Failed";
|
||||||
|
public const string Cancelled = "Cancelled";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建补偿任务输入
|
||||||
|
/// </summary>
|
||||||
|
public class CreateCompensationTaskInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务类型(使用 CompensationTaskType 常量)
|
||||||
|
/// </summary>
|
||||||
|
public string TaskType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务来源(如 CheckIn、Purchase)
|
||||||
|
/// </summary>
|
||||||
|
public string BusinessSource { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联业务记录Id
|
||||||
|
/// </summary>
|
||||||
|
public string? BusinessId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联用户Id
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理参数(匿名对象或字典,会自动序列化为JSON)
|
||||||
|
/// </summary>
|
||||||
|
public object Payload { get; set; } = new { };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常消息
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常来源(类名.方法名,如 CheckInService.CheckInAsync)
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorSource { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大重试次数(默认3次)
|
||||||
|
/// </summary>
|
||||||
|
public int MaxRetries { get; set; } = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿任务输出
|
||||||
|
/// </summary>
|
||||||
|
public class CompensationTaskOutput
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string TaskType { get; set; } = string.Empty;
|
||||||
|
public string BusinessSource { get; set; } = string.Empty;
|
||||||
|
public string? BusinessId { get; set; }
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string Payload { get; set; } = string.Empty;
|
||||||
|
public string ErrorMessage { get; set; } = string.Empty;
|
||||||
|
public string ErrorSource { get; set; } = string.Empty;
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
public int MaxRetries { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime? ProcessedAt { get; set; }
|
||||||
|
public DateTime? ScheduledAt { get; set; }
|
||||||
|
public string? ResultMessage { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询补偿任务输入(供外部项目按条件拉取)
|
||||||
|
/// </summary>
|
||||||
|
public class GetCompensationTasksInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 按状态筛选
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按任务类型筛选
|
||||||
|
/// </summary>
|
||||||
|
public string? TaskType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按业务来源筛选
|
||||||
|
/// </summary>
|
||||||
|
public string? BusinessSource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取数量上限(默认50)
|
||||||
|
/// </summary>
|
||||||
|
public int Limit { get; set; } = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新补偿任务状态输入(供外部处理项目回调)
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateCompensationStatusInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 目标状态(使用 CompensationTaskStatus 常量)
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理结果描述
|
||||||
|
/// </summary>
|
||||||
|
public string? ResultMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新重试次数(可选)
|
||||||
|
/// </summary>
|
||||||
|
public int? RetryCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下次计划执行时间(用于退避重试,可选)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ScheduledAt { get; set; }
|
||||||
|
}
|
||||||
84
QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
Normal file
84
QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 绑定期刊输入DTO
|
||||||
|
/// </summary>
|
||||||
|
public class BindJournalInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊模板Id(扫码解析的期刊定义Id)
|
||||||
|
/// </summary>
|
||||||
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实例化期刊Id(扫码解析的具体期刊实例Id,可选)
|
||||||
|
/// </summary>
|
||||||
|
public long? JournalInstanceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = "Subscribe";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 绑定期刊输出DTO
|
||||||
|
/// </summary>
|
||||||
|
public class BindJournalOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 绑定记录Id
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户Id
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊模板Id
|
||||||
|
/// </summary>
|
||||||
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实例化期刊Id
|
||||||
|
/// </summary>
|
||||||
|
public long? JournalInstanceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联类型
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 绑定时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户期刊关联查询输入DTO
|
||||||
|
/// </summary>
|
||||||
|
public class UserJournalQueryInput : PageQueryModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊模板Id
|
||||||
|
/// </summary>
|
||||||
|
public long? JournalId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实例化期刊Id
|
||||||
|
/// </summary>
|
||||||
|
public long? JournalInstanceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关联类型: Read, Favorite, Subscribe
|
||||||
|
/// </summary>
|
||||||
|
public string? Type { get; set; }
|
||||||
|
}
|
||||||
155
QYZH.InteractiveMagazine.Models/Dto/Pet/PetDto.cs
Normal file
155
QYZH.InteractiveMagazine.Models/Dto/Pet/PetDto.cs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物信息输出
|
||||||
|
/// </summary>
|
||||||
|
public class PetOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主键ID
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户Id
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物昵称
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前进化形态Id
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentEvolutionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPoints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计喂养次数
|
||||||
|
/// </summary>
|
||||||
|
public int FeedingCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物类型: Normal
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态: Inactive, Active, Sleeping
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养宠物输入
|
||||||
|
/// </summary>
|
||||||
|
public class FeedPetInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物Id
|
||||||
|
/// </summary>
|
||||||
|
public long PetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 增加的成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPoints { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养宠物输出
|
||||||
|
/// </summary>
|
||||||
|
public class FeedPetOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物Id
|
||||||
|
/// </summary>
|
||||||
|
public long PetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养前成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthBefore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养后成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthAfter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次成长变化量
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthChange { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否触发进化
|
||||||
|
/// </summary>
|
||||||
|
public bool HasEvolved { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进化后的阶段名称(未进化则为空)
|
||||||
|
/// </summary>
|
||||||
|
public string? EvolvedStageName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养记录输出
|
||||||
|
/// </summary>
|
||||||
|
public class FeedingRecordOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录Id
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物Id
|
||||||
|
/// </summary>
|
||||||
|
public long PetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户Id
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成长值变化量
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthChange { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养前成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthBefore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养后成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthAfter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养类型: Normal, Special
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物喂养补偿消息
|
||||||
|
/// </summary>
|
||||||
|
public class PetFeedingCompensationMessage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户Id
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物Id
|
||||||
|
/// </summary>
|
||||||
|
public long PetId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 需要补偿的成长值
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPoints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到记录Id(关联来源)
|
||||||
|
/// </summary>
|
||||||
|
public long CheckInRecordId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿原因
|
||||||
|
/// </summary>
|
||||||
|
public string Reason { get; set; } = "签到成功但宠物喂养失败";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 消息创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ using Newtonsoft.Json;
|
|||||||
namespace QYZH.InteractiveMagazine.IService.Dto;
|
namespace QYZH.InteractiveMagazine.IService.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信小程序登录输入
|
/// 微信小程序初次登录输入
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WeChatLoginInput
|
public class WeChatLoginInput
|
||||||
{
|
{
|
||||||
@ -12,6 +12,22 @@ public class WeChatLoginInput
|
|||||||
/// 微信登录凭证(wx.login 获取的 code)
|
/// 微信登录凭证(wx.login 获取的 code)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Code { get; set; } = string.Empty;
|
public string Code { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号获取凭证(getPhoneNumber 按钮回调中的 code,可选)
|
||||||
|
/// </summary>
|
||||||
|
public string? PhoneCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序快捷登录输入(通过 OpenId 登录)
|
||||||
|
/// </summary>
|
||||||
|
public class WeChatQuickLoginInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 微信OpenId(初次登录后客户端缓存的 OpenId)
|
||||||
|
/// </summary>
|
||||||
|
public string OpenId { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -225,6 +241,84 @@ public class WxCode2SessionResponse
|
|||||||
public string? ErrMsg { get; set; }
|
public string? ErrMsg { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信获取手机号接口响应
|
||||||
|
/// </summary>
|
||||||
|
public class WxPhoneNumberResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 错误码
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("errcode")]
|
||||||
|
public int ErrCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误信息
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("errmsg")]
|
||||||
|
public string? ErrMsg { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号信息
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("phone_info")]
|
||||||
|
public WxPhoneInfo? PhoneInfo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信手机号信息
|
||||||
|
/// </summary>
|
||||||
|
public class WxPhoneInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户绑定的手机号(国外手机号会有区号)
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("phoneNumber")]
|
||||||
|
public string? PhoneNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 没有区号的手机号
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("purePhoneNumber")]
|
||||||
|
public string? PurePhoneNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 区号
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("countryCode")]
|
||||||
|
public string? CountryCode { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信 access_token 接口响应
|
||||||
|
/// </summary>
|
||||||
|
public class WxAccessTokenResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取到的凭证
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("access_token")]
|
||||||
|
public string? AccessToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 凭证有效时间(秒)
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("expires_in")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误码
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("errcode")]
|
||||||
|
public int ErrCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误信息
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("errmsg")]
|
||||||
|
public string? ErrMsg { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信切换用户输入
|
/// 微信切换用户输入
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -33,6 +33,13 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int PointsAwarded {get;set;}
|
public int PointsAwarded {get;set;}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:本次签到获得成长值(喂养宠物)
|
||||||
|
/// Default:0
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPointsAwarded {get;set;}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desc:连续签到天数
|
/// Desc:连续签到天数
|
||||||
/// Default:
|
/// Default:
|
||||||
|
|||||||
108
QYZH.InteractiveMagazine.Models/Entity/CompensationTask.cs
Normal file
108
QYZH.InteractiveMagazine.Models/Entity/CompensationTask.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Models.Entity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿任务表 — 记录业务执行失败后需要异步重试的操作
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("CompensationTask")]
|
||||||
|
public partial class CompensationTask : SqlSugarBaseEntity
|
||||||
|
{
|
||||||
|
public CompensationTask() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:任务类型,决定补偿处理逻辑
|
||||||
|
/// PetFeeding — 宠物喂养补偿
|
||||||
|
/// UserPoints — 用户积分补偿
|
||||||
|
/// UserGrowth — 用户成长值补偿
|
||||||
|
/// SendNotification — 发送通知补偿
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public string TaskType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:业务来源(如 CheckIn、Purchase、Activity)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public string BusinessSource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:关联业务记录Id(如签到记录Id、订单Id)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
public string? BusinessId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:关联用户Id
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:处理参数(JSON格式,不同TaskType对应不同结构)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public string Payload { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:原始异常消息
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:异常来源(如 CheckInService.CheckInAsync)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorSource { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:已重试次数
|
||||||
|
/// Default:0
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:最大重试次数
|
||||||
|
/// Default:3
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public int MaxRetries { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:处理状态 Pending / Processing / Success / Failed / Cancelled
|
||||||
|
/// Default:Pending
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
public new string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:最后处理时间
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ProcessedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:下次计划执行时间(用于退避重试)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ScheduledAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:处理结果描述(成功/失败原因)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
public string? ResultMessage { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,8 +55,8 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
public string Type {get;set;}
|
public string Type {get;set;}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desc:状态: Active, Sleeping
|
/// Desc:状态: Inactive, Active, Sleeping
|
||||||
/// Default:Active
|
/// Default:Inactive
|
||||||
/// Nullable:False
|
/// Nullable:False
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Status {get;set;}
|
public string Status {get;set;}
|
||||||
|
|||||||
56
QYZH.InteractiveMagazine.Models/Entity/UserJournal.cs
Normal file
56
QYZH.InteractiveMagazine.Models/Entity/UserJournal.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Models.Entity
|
||||||
|
{
|
||||||
|
///<summary>
|
||||||
|
///用户与期刊关联表
|
||||||
|
///</summary>
|
||||||
|
[SugarTable("UserJournal")]
|
||||||
|
public partial class UserJournal : SqlSugarBaseEntity
|
||||||
|
{
|
||||||
|
public UserJournal()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:用户Id
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "UserId")]
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:期刊模板Id(对应Journal表的期刊定义)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "JournalId")]
|
||||||
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:实例化期刊Id(扫码获取的具体期刊实例,可为空表示绑定到期刊模板本身)
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "JournalInstanceId", IsNullable = true)]
|
||||||
|
public long? JournalInstanceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:关联类型: Read(已读), Favorite(收藏), Subscribe(订阅)
|
||||||
|
/// Default:Read
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "Type")]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:状态: Active(正常), Inactive(失效)
|
||||||
|
/// Default:Active
|
||||||
|
/// Nullable:False
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "Status")]
|
||||||
|
public new string Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
277
QYZH.InteractiveMagazine.Service/CheckInService.cs
Normal file
277
QYZH.InteractiveMagazine.Service/CheckInService.cs
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using QYZH.InteractiveMagazine.Repository;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Service;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class CheckInService(
|
||||||
|
BaseRepository<CheckInRecord> checkInRecordRepository,
|
||||||
|
IPetService petService,
|
||||||
|
ICompensationTaskService compensationTaskService,
|
||||||
|
ILogger<CheckInService> logger)
|
||||||
|
: BaseRepository<CheckInRecord>, ICheckInService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 默认签到奖励积分(无配置时的兜底值)
|
||||||
|
/// </summary>
|
||||||
|
private const int DefaultRewardPoints = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户签到
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CheckInOutput> CheckInAsync(long userId)
|
||||||
|
{
|
||||||
|
logger.LogInformation("用户签到,UserId: {UserId}", userId);
|
||||||
|
|
||||||
|
var today = DateTime.Now.Date;
|
||||||
|
|
||||||
|
// 1. 检查今日是否已签到
|
||||||
|
var alreadyCheckedIn = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||||
|
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= today && r.CheckInDate < today.AddDays(1))
|
||||||
|
.AnyAsync();
|
||||||
|
|
||||||
|
if (alreadyCheckedIn)
|
||||||
|
{
|
||||||
|
throw new BusinessException("今日已签到,请明天再来", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 计算连续签到天数
|
||||||
|
var consecutiveDays = await CalculateConsecutiveDaysAsync(userId, today);
|
||||||
|
|
||||||
|
// 3. 查询签到配置,计算奖励
|
||||||
|
var (pointsReward, growthReward) = await CalculateRewardsAsync(consecutiveDays);
|
||||||
|
|
||||||
|
// 4. 查询用户信息
|
||||||
|
var user = await checkInRecordRepository.Context.Queryable<Users>()
|
||||||
|
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException("用户不存在", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 查询用户宠物(如果有)
|
||||||
|
var pet = await checkInRecordRepository.Context.Queryable<Pet>()
|
||||||
|
.Where(p => p.UserId == userId && !p.IsDeleted)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
// 6. 事务执行签到相关写操作
|
||||||
|
var result = new CheckInOutput();
|
||||||
|
|
||||||
|
await checkInRecordRepository.UseTranAsync(async () =>
|
||||||
|
{
|
||||||
|
// 6a. 创建签到记录
|
||||||
|
var checkInRecord = new CheckInRecord
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
CheckInDate = today,
|
||||||
|
PointsAwarded = pointsReward,
|
||||||
|
GrowthPointsAwarded = growthReward,
|
||||||
|
ConsecutiveDays = consecutiveDays,
|
||||||
|
Type = "Normal",
|
||||||
|
Status = "Success"
|
||||||
|
};
|
||||||
|
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
|
||||||
|
checkInRecord.Id = recordId;
|
||||||
|
|
||||||
|
// 6b. 更新用户积分余额
|
||||||
|
var newPointsBalance = user.Points + pointsReward;
|
||||||
|
var newGrowthBalance = user.GrowthPoints + growthReward;
|
||||||
|
|
||||||
|
await checkInRecordRepository.Context.Updateable<Users>()
|
||||||
|
.SetColumns(u => u.Points == newPointsBalance)
|
||||||
|
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
|
||||||
|
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
// 6c. 创建积分变动记录
|
||||||
|
var pointsRecord = new PointsRecord
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ChangeAmount = pointsReward,
|
||||||
|
BalanceAfter = newPointsBalance,
|
||||||
|
ChangeType = "SignIn",
|
||||||
|
RelatedId = recordId,
|
||||||
|
Description = $"签到奖励(连续{consecutiveDays}天)",
|
||||||
|
Type = "Income",
|
||||||
|
Status = "Success"
|
||||||
|
};
|
||||||
|
await checkInRecordRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
||||||
|
|
||||||
|
// 构建返回结果
|
||||||
|
result.RecordId = (long)recordId;
|
||||||
|
result.CheckInDate = today;
|
||||||
|
result.ConsecutiveDays = consecutiveDays;
|
||||||
|
result.PointsAwarded = pointsReward;
|
||||||
|
result.GrowthPointsAwarded = growthReward;
|
||||||
|
result.PointsBalance = newPointsBalance;
|
||||||
|
result.GrowthPointsBalance = newGrowthBalance;
|
||||||
|
result.HasPet = pet != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. 如果用户有活跃宠物,调用 PetService 喂养(含进化检查),独立事务
|
||||||
|
if (pet != null && pet.Status == "Active" && growthReward > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var feedResult = await petService.FeedPetAsync(userId, new FeedPetInput
|
||||||
|
{
|
||||||
|
PetId = pet.Id,
|
||||||
|
GrowthPoints = growthReward
|
||||||
|
});
|
||||||
|
|
||||||
|
result.HasEvolved = feedResult.HasEvolved;
|
||||||
|
result.EvolvedStageName = feedResult.EvolvedStageName;
|
||||||
|
|
||||||
|
logger.LogInformation("签到成长值已喂养宠物,PetId: {PetId}, 进化: {HasEvolved}",
|
||||||
|
pet.Id, feedResult.HasEvolved);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "签到后喂养宠物失败,PetId: {PetId},将创建补偿任务", pet.Id);
|
||||||
|
|
||||||
|
await compensationTaskService.CreateTaskAsync(new CreateCompensationTaskInput
|
||||||
|
{
|
||||||
|
TaskType = CompensationTaskType.PetFeeding,
|
||||||
|
BusinessSource = "CheckIn",
|
||||||
|
BusinessId = result.RecordId.ToString(),
|
||||||
|
UserId = userId,
|
||||||
|
Payload = new { PetId = pet.Id, GrowthPoints = growthReward },
|
||||||
|
ErrorMessage = ex.Message,
|
||||||
|
ErrorSource = "CheckInService.CheckInAsync → PetService.FeedPetAsync",
|
||||||
|
MaxRetries = 3
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("用户签到成功,UserId: {UserId}, 连续{Days}天, 积分+{Points}, 成长值+{Growth}",
|
||||||
|
userId, consecutiveDays, pointsReward, growthReward);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户签到信息
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CheckInInfoOutput> GetCheckInInfoAsync(long userId)
|
||||||
|
{
|
||||||
|
logger.LogInformation("获取签到信息,UserId: {UserId}", userId);
|
||||||
|
|
||||||
|
var today = DateTime.Now.Date;
|
||||||
|
|
||||||
|
// 今日是否已签到
|
||||||
|
var hasCheckedInToday = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||||
|
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= today && r.CheckInDate < today.AddDays(1))
|
||||||
|
.AnyAsync();
|
||||||
|
|
||||||
|
// 累计签到天数
|
||||||
|
var totalCheckInDays = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||||
|
.Where(r => r.UserId == userId && !r.IsDeleted)
|
||||||
|
.CountAsync();
|
||||||
|
|
||||||
|
// 最近一次签到记录(用于获取连续天数)
|
||||||
|
var lastRecord = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||||
|
.Where(r => r.UserId == userId && !r.IsDeleted)
|
||||||
|
.OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
// 判断连续天数:如果最后一次签到是今天或昨天,则连续天数延续
|
||||||
|
var consecutiveDays = 0;
|
||||||
|
if (lastRecord != null)
|
||||||
|
{
|
||||||
|
var lastDate = lastRecord.CheckInDate.Date;
|
||||||
|
if (lastDate == today || lastDate == today.AddDays(-1))
|
||||||
|
{
|
||||||
|
consecutiveDays = lastRecord.ConsecutiveDays;
|
||||||
|
if (lastDate == today)
|
||||||
|
{
|
||||||
|
// 今天已签到,连续天数就是今天的值
|
||||||
|
}
|
||||||
|
// 如果是昨天,则连续天数保持(今天还没签到)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户余额
|
||||||
|
var user = await checkInRecordRepository.Context.Queryable<Users>()
|
||||||
|
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
// 最近 30 天签到记录
|
||||||
|
var thirtyDaysAgo = today.AddDays(-29);
|
||||||
|
var recentRecords = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||||
|
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= thirtyDaysAgo)
|
||||||
|
.OrderBy(r => r.CheckInDate, SqlSugar.OrderByType.Desc)
|
||||||
|
.Select(r => new CheckInRecordOutput
|
||||||
|
{
|
||||||
|
Id = (long)r.Id,
|
||||||
|
CheckInDate = r.CheckInDate,
|
||||||
|
ConsecutiveDays = r.ConsecutiveDays,
|
||||||
|
PointsAwarded = r.PointsAwarded,
|
||||||
|
GrowthPointsAwarded = r.GrowthPointsAwarded,
|
||||||
|
Type = r.Type,
|
||||||
|
Status = r.Status
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new CheckInInfoOutput
|
||||||
|
{
|
||||||
|
HasCheckedInToday = hasCheckedInToday,
|
||||||
|
ConsecutiveDays = consecutiveDays,
|
||||||
|
TotalCheckInDays = totalCheckInDays,
|
||||||
|
PointsBalance = user?.Points ?? 0,
|
||||||
|
GrowthPointsBalance = user?.GrowthPoints ?? 0,
|
||||||
|
RecentRecords = recentRecords
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算连续签到天数
|
||||||
|
/// </summary>
|
||||||
|
private async Task<int> CalculateConsecutiveDaysAsync(long userId, DateTime today)
|
||||||
|
{
|
||||||
|
var yesterday = today.AddDays(-1);
|
||||||
|
|
||||||
|
var yesterdayRecord = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
||||||
|
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= yesterday && r.CheckInDate < today)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
// 昨天有签到记录,连续天数 +1;否则从 1 开始
|
||||||
|
return yesterdayRecord != null ? yesterdayRecord.ConsecutiveDays + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据连续签到天数计算奖励(积分 + 成长值)
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(int PointsReward, int GrowthReward)> CalculateRewardsAsync(int consecutiveDays)
|
||||||
|
{
|
||||||
|
// 查询签到配置(按 DayNumber 升序)
|
||||||
|
var configs = await checkInRecordRepository.Context.Queryable<CheckInConfig>()
|
||||||
|
.Where(c => c.Status == "Active" && !c.IsDeleted)
|
||||||
|
.OrderBy(c => c.DayNumber)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (configs.Count == 0)
|
||||||
|
{
|
||||||
|
// 无配置时使用默认值
|
||||||
|
return (DefaultRewardPoints, DefaultRewardPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到匹配的奖励档位:取 DayNumber <= 连续天数 的最大档位
|
||||||
|
var matchedConfig = configs.LastOrDefault(c => c.DayNumber <= consecutiveDays)
|
||||||
|
?? configs.First();
|
||||||
|
|
||||||
|
var totalPoints = matchedConfig.RewardPoints + matchedConfig.BonusPoints;
|
||||||
|
|
||||||
|
// 成长值与积分相同(签到同时获得积分和成长值)
|
||||||
|
return (totalPoints, totalPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
173
QYZH.InteractiveMagazine.Service/CompensationTaskService.cs
Normal file
173
QYZH.InteractiveMagazine.Service/CompensationTaskService.cs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using QYZH.InteractiveMagazine.Repository;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Service;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 补偿任务服务实现(仅负责记录,处理逻辑由外部 Hangfire 项目完成)
|
||||||
|
/// </summary>
|
||||||
|
public class CompensationTaskService(
|
||||||
|
BaseRepository<CompensationTask> taskRepository,
|
||||||
|
ILogger<CompensationTaskService> logger)
|
||||||
|
: BaseRepository<CompensationTask>, ICompensationTaskService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建补偿任务 — 记录失败操作,供后续补偿处理
|
||||||
|
/// </summary>
|
||||||
|
public async Task<long> CreateTaskAsync(CreateCompensationTaskInput input)
|
||||||
|
{
|
||||||
|
logger.LogWarning(
|
||||||
|
"创建补偿任务,TaskType: {TaskType}, BusinessSource: {BusinessSource}, UserId: {UserId}, ErrorSource: {ErrorSource}, Error: {ErrorMessage}",
|
||||||
|
input.TaskType, input.BusinessSource, input.UserId, input.ErrorSource, input.ErrorMessage);
|
||||||
|
|
||||||
|
var payloadJson = input.Payload is string str ? str : JsonConvert.SerializeObject(input.Payload);
|
||||||
|
|
||||||
|
var task = new CompensationTask
|
||||||
|
{
|
||||||
|
TaskType = input.TaskType,
|
||||||
|
BusinessSource = input.BusinessSource,
|
||||||
|
BusinessId = input.BusinessId,
|
||||||
|
UserId = input.UserId,
|
||||||
|
Payload = payloadJson,
|
||||||
|
ErrorMessage = input.ErrorMessage,
|
||||||
|
ErrorSource = input.ErrorSource,
|
||||||
|
RetryCount = 0,
|
||||||
|
MaxRetries = input.MaxRetries > 0 ? input.MaxRetries : 3,
|
||||||
|
Status = CompensationTaskStatus.Pending,
|
||||||
|
ScheduledAt = DateTime.Now,
|
||||||
|
IsDeleted = false,
|
||||||
|
CreatedBy = "System",
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedBy = "System",
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await taskRepository.InsertReturnEntityAsync(task);
|
||||||
|
|
||||||
|
logger.LogInformation("补偿任务创建成功,TaskId: {TaskId}", result.Id);
|
||||||
|
|
||||||
|
return result.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取待处理的补偿任务列表
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<CompensationTaskOutput>> GetPendingTasksAsync(int limit = 50)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
|
||||||
|
var tasks = await taskRepository.Queryable()
|
||||||
|
.Where(t => (t.Status == CompensationTaskStatus.Pending || t.Status == CompensationTaskStatus.Processing)
|
||||||
|
&& !t.IsDeleted
|
||||||
|
&& (t.ScheduledAt == null || t.ScheduledAt <= now))
|
||||||
|
.OrderBy(t => t.CreatedAt)
|
||||||
|
.Take(limit)
|
||||||
|
.Select(t => new CompensationTaskOutput
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
TaskType = 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 = t.Status,
|
||||||
|
ProcessedAt = t.ProcessedAt,
|
||||||
|
ScheduledAt = t.ScheduledAt,
|
||||||
|
ResultMessage = t.ResultMessage,
|
||||||
|
CreatedAt = t.CreatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据业务来源和类型查询补偿任务(用于外部项目按条件拉取)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<CompensationTaskOutput>> GetTasksAsync(GetCompensationTasksInput input)
|
||||||
|
{
|
||||||
|
var query = taskRepository.Queryable()
|
||||||
|
.Where(t => !t.IsDeleted);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.Status))
|
||||||
|
query = query.Where(t => t.Status == input.Status);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.TaskType))
|
||||||
|
query = query.Where(t => t.TaskType == input.TaskType);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(input.BusinessSource))
|
||||||
|
query = query.Where(t => t.BusinessSource == input.BusinessSource);
|
||||||
|
|
||||||
|
var tasks = await query
|
||||||
|
.OrderBy(t => t.CreatedAt)
|
||||||
|
.Take(input.Limit > 0 ? input.Limit : 50)
|
||||||
|
.Select(t => new CompensationTaskOutput
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
TaskType = 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 = t.Status,
|
||||||
|
ProcessedAt = t.ProcessedAt,
|
||||||
|
ScheduledAt = t.ScheduledAt,
|
||||||
|
ResultMessage = t.ResultMessage,
|
||||||
|
CreatedAt = t.CreatedAt
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新补偿任务状态(供外部处理项目回调更新结果)
|
||||||
|
/// </summary>
|
||||||
|
public async Task UpdateTaskStatusAsync(long taskId, UpdateCompensationStatusInput input)
|
||||||
|
{
|
||||||
|
var update = taskRepository.Context.Updateable<CompensationTask>()
|
||||||
|
.SetColumns(t => t.Status == input.Status)
|
||||||
|
.SetColumns(t => t.ResultMessage == input.ResultMessage)
|
||||||
|
.SetColumns(t => t.ProcessedAt == DateTime.Now)
|
||||||
|
.SetColumns(t => t.UpdatedAt == DateTime.Now);
|
||||||
|
|
||||||
|
// 如果外部传入了重试相关字段,一并更新
|
||||||
|
if (input.RetryCount.HasValue)
|
||||||
|
update = update.SetColumns(t => t.RetryCount == input.RetryCount.Value);
|
||||||
|
|
||||||
|
if (input.ScheduledAt.HasValue)
|
||||||
|
update = update.SetColumns(t => t.ScheduledAt == input.ScheduledAt.Value);
|
||||||
|
|
||||||
|
await update.Where(t => t.Id == taskId && !t.IsDeleted)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("补偿任务状态更新,TaskId: {TaskId}, Status: {Status}", taskId, input.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消补偿任务
|
||||||
|
/// </summary>
|
||||||
|
public async Task CancelTaskAsync(long taskId, string reason)
|
||||||
|
{
|
||||||
|
await taskRepository.Context.Updateable<CompensationTask>()
|
||||||
|
.SetColumns(t => t.Status == CompensationTaskStatus.Cancelled)
|
||||||
|
.SetColumns(t => t.ResultMessage == reason)
|
||||||
|
.SetColumns(t => t.UpdatedAt == DateTime.Now)
|
||||||
|
.Where(t => t.Id == taskId && !t.IsDeleted)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("补偿任务已取消,TaskId: {TaskId}, Reason: {Reason}", taskId, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
282
QYZH.InteractiveMagazine.Service/PetService.cs
Normal file
282
QYZH.InteractiveMagazine.Service/PetService.cs
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using QYZH.InteractiveMagazine.Repository;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Service;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 宠物服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class PetService(
|
||||||
|
BaseRepository<Pet> petRepository,
|
||||||
|
BaseRepository<PetFeedingRecord> feedingRecordRepository,
|
||||||
|
BaseRepository<PetEvolution> petEvolutionRepository,
|
||||||
|
ILogger<PetService> logger)
|
||||||
|
: BaseRepository<Pet>, IPetService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户宠物信息
|
||||||
|
/// </summary>
|
||||||
|
public async Task<PetOutput?> GetPetByUserIdAsync(long userId)
|
||||||
|
{
|
||||||
|
logger.LogInformation("获取用户宠物信息,UserId: {UserId}", userId);
|
||||||
|
|
||||||
|
var pet = await petRepository.Queryable()
|
||||||
|
.Where(p => p.UserId == userId)
|
||||||
|
.Select(p => new PetOutput
|
||||||
|
{
|
||||||
|
Id = p.Id,
|
||||||
|
UserId = p.UserId,
|
||||||
|
Name = p.Name,
|
||||||
|
CurrentEvolutionId = p.CurrentEvolutionId,
|
||||||
|
GrowthPoints = p.GrowthPoints,
|
||||||
|
FeedingCount = p.FeedingCount,
|
||||||
|
Type = p.Type,
|
||||||
|
Status = p.Status,
|
||||||
|
CreatedAt = p.CreatedAt
|
||||||
|
})
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
return pet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为用户创建默认宠物(最低形态、成长值为0、未激活状态)
|
||||||
|
/// </summary>
|
||||||
|
public async Task CreateDefaultPetAsync(long userId)
|
||||||
|
{
|
||||||
|
logger.LogInformation("为用户创建默认宠物,UserId: {UserId}", userId);
|
||||||
|
|
||||||
|
// 检查用户是否已有宠物
|
||||||
|
var exists = petRepository.Context.Queryable<Pet>()
|
||||||
|
.Any(p => p.UserId == userId);
|
||||||
|
if (exists)
|
||||||
|
{
|
||||||
|
logger.LogWarning("用户已存在宠物,跳过创建,UserId: {UserId}", userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pet = new Pet
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Name = "小精灵",
|
||||||
|
CurrentEvolutionId = 1,
|
||||||
|
GrowthPoints = 0,
|
||||||
|
FeedingCount = 0,
|
||||||
|
Type = "Normal",
|
||||||
|
Status = "Inactive",
|
||||||
|
IsDeleted = false,
|
||||||
|
CreatedBy = userId.ToString(),
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedBy = userId.ToString(),
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await petRepository.InsertAsync(pet);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
logger.LogError("创建默认宠物失败,UserId: {UserId}", userId);
|
||||||
|
throw new Exception("创建宠物失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("用户默认宠物创建成功,UserId: {UserId}, PetId: {PetId}", userId, pet.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 激活宠物(将状态从 Inactive 改为 Active)
|
||||||
|
/// </summary>
|
||||||
|
public async Task ActivatePetAsync(long userId)
|
||||||
|
{
|
||||||
|
logger.LogInformation("激活用户宠物,UserId: {UserId}", userId);
|
||||||
|
|
||||||
|
var pet = await petRepository.Queryable()
|
||||||
|
.Where(p => p.UserId == userId)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (pet == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("用户宠物不存在,无法激活,UserId: {UserId}", userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pet.Status != "Inactive")
|
||||||
|
{
|
||||||
|
logger.LogInformation("用户宠物已非未激活状态,跳过激活,UserId: {UserId}, Status: {Status}", userId, pet.Status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await petRepository.UpdateAsync(
|
||||||
|
p => new Pet { Status = "Active" },
|
||||||
|
p => p.UserId == userId);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
logger.LogError("激活宠物失败,UserId: {UserId}", userId);
|
||||||
|
throw new Exception("激活宠物失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("用户宠物激活成功,UserId: {UserId}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养宠物(增加成长值 + 记录喂养记录 + 触发进化检查)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<FeedPetOutput> FeedPetAsync(long userId, FeedPetInput input)
|
||||||
|
{
|
||||||
|
logger.LogInformation("喂养宠物,UserId: {UserId}, PetId: {PetId}, GrowthPoints: {GrowthPoints}",
|
||||||
|
userId, input.PetId, input.GrowthPoints);
|
||||||
|
|
||||||
|
if (input.PetId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("宠物Id不能为空", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.GrowthPoints <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("成长值必须大于0", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询宠物
|
||||||
|
var pet = await petRepository.GetByIdAsync(input.PetId);
|
||||||
|
if (pet == null || pet.IsDeleted)
|
||||||
|
{
|
||||||
|
logger.LogWarning("喂养失败,宠物不存在,PetId: {PetId}", input.PetId);
|
||||||
|
throw new BusinessException("宠物不存在", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验宠物归属
|
||||||
|
if (pet.UserId != userId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("喂养失败,无权操作该宠物,UserId: {UserId}, PetUserId: {PetUserId}", userId, pet.UserId);
|
||||||
|
throw new BusinessException("无权操作该宠物", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验宠物状态
|
||||||
|
if (pet.Status != "Active")
|
||||||
|
{
|
||||||
|
logger.LogWarning("喂养失败,宠物未激活,PetId: {PetId}, Status: {Status}", input.PetId, pet.Status);
|
||||||
|
throw new BusinessException("宠物未激活,无法喂养", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
var growthBefore = pet.GrowthPoints;
|
||||||
|
var growthAfter = growthBefore + input.GrowthPoints;
|
||||||
|
var hasEvolved = false;
|
||||||
|
string? evolvedStageName = null;
|
||||||
|
|
||||||
|
// 事务保证一致性
|
||||||
|
await UseTranAsync(async () =>
|
||||||
|
{
|
||||||
|
// 累加成长值和喂养次数
|
||||||
|
var updateResult = await petRepository.Context.Updateable<Pet>()
|
||||||
|
.SetColumns(p => p.GrowthPoints == growthAfter)
|
||||||
|
.SetColumns(p => p.FeedingCount == p.FeedingCount + 1)
|
||||||
|
.SetColumns(p => p.UpdatedAt == DateTime.Now)
|
||||||
|
.SetColumns(p => p.UpdatedBy == userId.ToString())
|
||||||
|
.Where(p => p.Id == input.PetId)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
if (updateResult <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("更新宠物成长值失败", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进化检查:查找下一阶段进化形态
|
||||||
|
var nextEvolution = await petEvolutionRepository.Queryable()
|
||||||
|
.Where(e => e.PreviousEvolutionId == pet.CurrentEvolutionId
|
||||||
|
&& e.RequiredGrowth <= growthAfter
|
||||||
|
&& e.Status == "Active")
|
||||||
|
.OrderBy(e => e.RequiredGrowth, OrderByType.Desc)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
if (nextEvolution != null)
|
||||||
|
{
|
||||||
|
// 触发进化
|
||||||
|
var evolveResult = await petRepository.Context.Updateable<Pet>()
|
||||||
|
.SetColumns(p => p.CurrentEvolutionId == nextEvolution.Id)
|
||||||
|
.SetColumns(p => p.UpdatedAt == DateTime.Now)
|
||||||
|
.SetColumns(p => p.UpdatedBy == userId.ToString())
|
||||||
|
.Where(p => p.Id == input.PetId)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
if (evolveResult > 0)
|
||||||
|
{
|
||||||
|
hasEvolved = true;
|
||||||
|
evolvedStageName = nextEvolution.StageName;
|
||||||
|
logger.LogInformation("宠物进化成功,PetId: {PetId}, 新形态: {StageName} (Level {StageLevel})",
|
||||||
|
input.PetId, nextEvolution.StageName, nextEvolution.StageLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入喂养记录
|
||||||
|
var record = new PetFeedingRecord
|
||||||
|
{
|
||||||
|
PetId = input.PetId,
|
||||||
|
UserId = userId,
|
||||||
|
PointsUsed = 0, // 预留:后期可扩展为消耗积分喂养
|
||||||
|
GrowthChange = input.GrowthPoints,
|
||||||
|
GrowthBefore = growthBefore,
|
||||||
|
GrowthAfter = growthAfter,
|
||||||
|
Type = "Normal",
|
||||||
|
Status = "Success",
|
||||||
|
IsDeleted = false,
|
||||||
|
CreatedBy = userId.ToString(),
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedBy = userId.ToString(),
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
var insertResult = await feedingRecordRepository.InsertAsync(record);
|
||||||
|
if (!insertResult)
|
||||||
|
{
|
||||||
|
throw new BusinessException("写入喂养记录失败", 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.LogInformation("喂养宠物成功,PetId: {PetId}, 成长值: {Before} -> {After}, 进化: {HasEvolved}",
|
||||||
|
input.PetId, growthBefore, growthAfter, hasEvolved);
|
||||||
|
|
||||||
|
return new FeedPetOutput
|
||||||
|
{
|
||||||
|
PetId = input.PetId,
|
||||||
|
GrowthBefore = growthBefore,
|
||||||
|
GrowthAfter = growthAfter,
|
||||||
|
GrowthChange = input.GrowthPoints,
|
||||||
|
HasEvolved = hasEvolved,
|
||||||
|
EvolvedStageName = evolvedStageName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取宠物喂养记录列表
|
||||||
|
/// </summary>
|
||||||
|
public async Task<PageListModel<FeedingRecordOutput>> GetFeedingRecordsAsync(long userId, long petId, PageQueryModel pageQuery)
|
||||||
|
{
|
||||||
|
logger.LogInformation("查询喂养记录,UserId: {UserId}, PetId: {PetId}, PageIndex: {PageIndex}, PageSize: {PageSize}",
|
||||||
|
userId, petId, pageQuery.PageIndex, pageQuery.PageSize);
|
||||||
|
|
||||||
|
RefAsync<int> totalNumber = 0;
|
||||||
|
var records = await feedingRecordRepository.Queryable()
|
||||||
|
.Where(r => r.UserId == userId && r.PetId == petId)
|
||||||
|
.OrderByDescending(r => r.CreatedAt)
|
||||||
|
.Select(r => new FeedingRecordOutput
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
PetId = r.PetId,
|
||||||
|
UserId = r.UserId,
|
||||||
|
GrowthChange = r.GrowthChange,
|
||||||
|
GrowthBefore = r.GrowthBefore,
|
||||||
|
GrowthAfter = r.GrowthAfter,
|
||||||
|
Type = r.Type,
|
||||||
|
Status = r.Status,
|
||||||
|
CreatedAt = r.CreatedAt
|
||||||
|
}, true)
|
||||||
|
.ToPageListAsync(pageQuery.PageIndex, pageQuery.PageSize, totalNumber);
|
||||||
|
|
||||||
|
return new PageListModel<FeedingRecordOutput>(records, pageQuery.PageIndex, pageQuery.PageSize, totalNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
208
QYZH.InteractiveMagazine.Service/UserJournalService.cs
Normal file
208
QYZH.InteractiveMagazine.Service/UserJournalService.cs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using QYZH.InteractiveMagazine.Repository;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Service;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户期刊关联服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class UserJournalService(
|
||||||
|
BaseRepository<UserJournal> userJournalRepository,
|
||||||
|
BaseRepository<Users> usersRepository,
|
||||||
|
BaseRepository<Journal> journalRepository,
|
||||||
|
ILogger<UserJournalService> logger,
|
||||||
|
IPetService petService)
|
||||||
|
: BaseRepository<UserJournal>, IUserJournalService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户绑定期刊(扫码绑定)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<BindJournalOutput> BindJournalAsync(long userId, BindJournalInput input)
|
||||||
|
{
|
||||||
|
logger.LogInformation("用户绑定期刊,UserId: {UserId}, JournalId: {JournalId}, JournalInstanceId: {JournalInstanceId}, Type: {Type}",
|
||||||
|
userId, input.JournalId, input.JournalInstanceId, input.Type);
|
||||||
|
|
||||||
|
// 校验参数
|
||||||
|
if (input.JournalId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("期刊Id不能为空", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验用户是否存在
|
||||||
|
var user = await usersRepository.GetByIdAsync(userId);
|
||||||
|
if (user == null || user.IsDeleted)
|
||||||
|
{
|
||||||
|
logger.LogWarning("绑定期刊失败,用户不存在,UserId: {UserId}", userId);
|
||||||
|
throw new BusinessException("用户不存在", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验期刊是否存在
|
||||||
|
var journal = await journalRepository.GetByIdAsync(input.JournalId);
|
||||||
|
if (journal == null || journal.IsDeleted)
|
||||||
|
{
|
||||||
|
logger.LogWarning("绑定期刊失败,期刊不存在,JournalId: {JournalId}", input.JournalId);
|
||||||
|
throw new BusinessException("期刊不存在", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验期刊状态
|
||||||
|
if (journal.Status != "Published")
|
||||||
|
{
|
||||||
|
logger.LogWarning("绑定期刊失败,期刊未发布,JournalId: {JournalId}, Status: {Status}", input.JournalId, journal.Status);
|
||||||
|
throw new BusinessException("该期刊暂未发布,无法绑定", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验实例化期刊是否存在(如果传入了 JournalInstanceId)
|
||||||
|
if (input.JournalInstanceId.HasValue && input.JournalInstanceId.Value > 0)
|
||||||
|
{
|
||||||
|
var instance = await journalRepository.GetByIdAsync(input.JournalInstanceId.Value);
|
||||||
|
if (instance == null || instance.IsDeleted)
|
||||||
|
{
|
||||||
|
logger.LogWarning("绑定期刊失败,实例化期刊不存在,JournalInstanceId: {JournalInstanceId}", input.JournalInstanceId);
|
||||||
|
throw new BusinessException("实例化期刊不存在", 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防重复绑定:同一用户 + 期刊 + 实例 + 类型
|
||||||
|
var isExist = userJournalRepository.Any(uj =>
|
||||||
|
uj.UserId == userId &&
|
||||||
|
uj.JournalId == input.JournalId &&
|
||||||
|
uj.JournalInstanceId == input.JournalInstanceId &&
|
||||||
|
uj.Type == input.Type &&
|
||||||
|
!uj.IsDeleted);
|
||||||
|
|
||||||
|
if (isExist)
|
||||||
|
{
|
||||||
|
logger.LogWarning("重复绑定期刊,UserId: {UserId}, JournalId: {JournalId}, Type: {Type}", userId, input.JournalId, input.Type);
|
||||||
|
throw new BusinessException("您已绑定过该期刊,无需重复操作", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为首次绑定期刊(用于激活宠物)
|
||||||
|
var isFirstBind = !userJournalRepository.Context.Queryable<UserJournal>()
|
||||||
|
.Any(uj => uj.UserId == userId);
|
||||||
|
|
||||||
|
// 创建绑定记录
|
||||||
|
var userJournal = new UserJournal
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
JournalId = input.JournalId,
|
||||||
|
JournalInstanceId = input.JournalInstanceId,
|
||||||
|
Type = input.Type,
|
||||||
|
Status = "Active",
|
||||||
|
IsDeleted = false,
|
||||||
|
CreatedBy = userId.ToString(),
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
UpdatedBy = userId.ToString(),
|
||||||
|
UpdatedAt = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await userJournalRepository.InsertAsync(userJournal);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
logger.LogError("绑定期刊失败,写入数据库失败,UserId: {UserId}, JournalId: {JournalId}", userId, input.JournalId);
|
||||||
|
throw new BusinessException("绑定期刊失败,请稍后重试", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("用户绑定期刊成功,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, userJournal.Id);
|
||||||
|
|
||||||
|
// 首次绑定期刊时激活宠物
|
||||||
|
if (isFirstBind)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await petService.ActivatePetAsync(userId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "首次绑定期刊激活宠物失败,UserId: {UserId}", userId);
|
||||||
|
// 宠物激活失败不阻断绑定流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BindJournalOutput
|
||||||
|
{
|
||||||
|
Id = userJournal.Id,
|
||||||
|
UserId = userJournal.UserId,
|
||||||
|
JournalId = userJournal.JournalId,
|
||||||
|
JournalInstanceId = userJournal.JournalInstanceId,
|
||||||
|
Type = userJournal.Type,
|
||||||
|
Status = userJournal.Status,
|
||||||
|
CreatedAt = userJournal.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取用户的期刊绑定列表
|
||||||
|
/// </summary>
|
||||||
|
public async Task<PageListModel<BindJournalOutput>> GetUserJournalsAsync(long userId, UserJournalQueryInput input)
|
||||||
|
{
|
||||||
|
logger.LogInformation("查询用户期刊绑定列表,UserId: {UserId}, PageIndex: {PageIndex}, PageSize: {PageSize}",
|
||||||
|
userId, 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 pageResult = await userJournalRepository.Queryable()
|
||||||
|
.Where(uj => uj.UserId == userId)
|
||||||
|
.WhereIF(input.JournalId.HasValue, uj => uj.JournalId == input.JournalId.Value)
|
||||||
|
.WhereIF(input.JournalInstanceId.HasValue, uj => uj.JournalInstanceId == input.JournalInstanceId.Value)
|
||||||
|
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type == input.Type)
|
||||||
|
.OrderByDescending(uj => uj.CreatedAt)
|
||||||
|
.Select(uj => new BindJournalOutput
|
||||||
|
{
|
||||||
|
Id = uj.Id,
|
||||||
|
UserId = uj.UserId,
|
||||||
|
JournalId = uj.JournalId,
|
||||||
|
JournalInstanceId = uj.JournalInstanceId,
|
||||||
|
Type = uj.Type,
|
||||||
|
Status = uj.Status,
|
||||||
|
CreatedAt = uj.CreatedAt
|
||||||
|
}, true)
|
||||||
|
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||||
|
|
||||||
|
return new PageListModel<BindJournalOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消期刊绑定
|
||||||
|
/// </summary>
|
||||||
|
public async Task UnbindJournalAsync(long userId, long id)
|
||||||
|
{
|
||||||
|
logger.LogInformation("取消期刊绑定,UserId: {UserId}, Id: {Id}", userId, id);
|
||||||
|
|
||||||
|
var userJournal = await userJournalRepository.GetByIdAsync(id);
|
||||||
|
if (userJournal == null || userJournal.IsDeleted)
|
||||||
|
{
|
||||||
|
logger.LogWarning("取消绑定失败,记录不存在,Id: {Id}", id);
|
||||||
|
throw new BusinessException("绑定记录不存在", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验归属权:只能取消自己的绑定
|
||||||
|
if (userJournal.UserId != userId)
|
||||||
|
{
|
||||||
|
logger.LogWarning("取消绑定失败,无权操作,UserId: {UserId}, RecordUserId: {RecordUserId}", userId, userJournal.UserId);
|
||||||
|
throw new BusinessException("无权取消该绑定", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await userJournalRepository.DeleteByIdAsync(id);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
logger.LogError("取消绑定失败,Id: {Id}", id);
|
||||||
|
throw new BusinessException("取消绑定失败,请稍后重试", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("取消期刊绑定成功,UserId: {UserId}, Id: {Id}", userId, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,27 +15,26 @@ namespace QYZH.InteractiveMagazine.Service;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信小程序认证服务实现
|
/// 微信小程序认证服务实现
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfiguration configuration, ILogger<WeChatAuthService> logger) : BaseRepository<Users>, IWeChatAuthService
|
public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfiguration configuration, ILogger<WeChatAuthService> logger, IPetService petService) : BaseRepository<Users>, IWeChatAuthService
|
||||||
{
|
{
|
||||||
private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||||
|
private const string AccessTokenCacheKey = "InteractiveMagazine:WeChat:AccessToken";
|
||||||
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
|
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
|
||||||
|
private const string GetAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
|
||||||
|
private const string GetPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={0}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信小程序一键登录
|
/// 微信小程序登录(首次创建用户,非首次直接登录)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">登录输入(含微信 code)</param>
|
|
||||||
/// <returns>登录结果(含 Token 和该 OpenId 下的用户列表)</returns>
|
|
||||||
public async Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input)
|
public async Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input)
|
||||||
{
|
{
|
||||||
logger.LogInformation("微信小程序登录尝试");
|
logger.LogInformation("微信小程序登录");
|
||||||
|
|
||||||
// 参数校验
|
|
||||||
if (string.IsNullOrWhiteSpace(input.Code))
|
if (string.IsNullOrWhiteSpace(input.Code))
|
||||||
{
|
{
|
||||||
throw new BusinessException("微信登录凭证 code 不能为空", 400);
|
throw new BusinessException("微信登录凭证 code 不能为空", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取微信配置
|
|
||||||
var weChatSettings = GetWeChatSettings();
|
var weChatSettings = GetWeChatSettings();
|
||||||
|
|
||||||
// 调用微信 code2session 接口
|
// 调用微信 code2session 接口
|
||||||
@ -56,16 +55,26 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
|||||||
|
|
||||||
if (users.Count == 0)
|
if (users.Count == 0)
|
||||||
{
|
{
|
||||||
// 首次登录,创建新用户
|
// 首次登录,获取手机号(如果传入了 PhoneCode)
|
||||||
|
string? phone = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
|
||||||
|
{
|
||||||
|
phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
|
||||||
|
logger.LogInformation("获取手机号成功,OpenId: {OpenId}, Phone: {Phone}", wxResponse.OpenId, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新用户
|
||||||
var newUser = new Users
|
var newUser = new Users
|
||||||
{
|
{
|
||||||
Name = $"wx_{wxResponse.OpenId[^8..]}",
|
Name = $"wx_{wxResponse.OpenId[^8..]}",
|
||||||
OpenId = wxResponse.OpenId,
|
OpenId = wxResponse.OpenId,
|
||||||
UnionId = wxResponse.UnionId,
|
UnionId = wxResponse.UnionId,
|
||||||
|
Phone = phone,
|
||||||
Type = "Normal",
|
Type = "Normal",
|
||||||
Status = "Active",
|
Status = "Active",
|
||||||
GrowthPoints = 0,
|
GrowthPoints = 0,
|
||||||
Points = 0
|
Points = 0,
|
||||||
|
IsLastOnline = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var insertResult = await usersRepository.Insertable(newUser).ExecuteReturnIdentityAsync();
|
var insertResult = await usersRepository.Insertable(newUser).ExecuteReturnIdentityAsync();
|
||||||
@ -78,29 +87,73 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
|||||||
newUser.Id = insertResult;
|
newUser.Id = insertResult;
|
||||||
users.Add(newUser);
|
users.Add(newUser);
|
||||||
logger.LogInformation("微信新用户创建成功,UserId: {UserId}, OpenId: {OpenId}", newUser.Id, wxResponse.OpenId);
|
logger.LogInformation("微信新用户创建成功,UserId: {UserId}, OpenId: {OpenId}", newUser.Id, wxResponse.OpenId);
|
||||||
|
|
||||||
|
// 为新用户创建默认宠物(未激活状态)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await petService.CreateDefaultPetAsync(newUser.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "新用户创建默认宠物失败,UserId: {UserId}", newUser.Id);
|
||||||
|
// 宠物创建失败不阻断注册流程
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// 非首次登录,如果传入了 PhoneCode 则更新该 OpenId 下所有用户的手机号
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
|
||||||
|
{
|
||||||
|
var phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
|
||||||
|
if (!string.IsNullOrWhiteSpace(phone))
|
||||||
|
{
|
||||||
|
await usersRepository.Context.Updateable<Users>()
|
||||||
|
.SetColumns(u => u.Phone == phone)
|
||||||
|
.Where(u => u.OpenId == wxResponse.OpenId && !u.IsDeleted)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
foreach (var u in users)
|
||||||
|
{
|
||||||
|
u.Phone = phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("更新 OpenId: {OpenId} 下所有用户手机号成功,Phone: {Phone}", wxResponse.OpenId, phone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("微信登录成功,OpenId: {OpenId} 下存在 {Count} 个用户", wxResponse.OpenId, users.Count);
|
logger.LogInformation("微信登录成功,OpenId: {OpenId} 下存在 {Count} 个用户", wxResponse.OpenId, users.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用第一个用户生成 JWT Token
|
// 构建登录输出
|
||||||
var primaryUser = users.First();
|
return await BuildLoginOutputAsync(wxResponse.OpenId, users);
|
||||||
var jwtSettings = GetJwtSettings();
|
}
|
||||||
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
|
|
||||||
|
|
||||||
// 缓存 Token 到 Redis
|
/// <summary>
|
||||||
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
/// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input)
|
||||||
|
{
|
||||||
|
logger.LogInformation("微信快捷登录,OpenId: {OpenId}", input.OpenId);
|
||||||
|
|
||||||
// 映射用户列表
|
if (string.IsNullOrWhiteSpace(input.OpenId))
|
||||||
var userOutputs = users.Select(MapUserToOutput).ToList();
|
|
||||||
|
|
||||||
return new WeChatLoginOutput
|
|
||||||
{
|
{
|
||||||
Token = token,
|
throw new BusinessException("OpenId 不能为空", 400);
|
||||||
OpenId = wxResponse.OpenId,
|
}
|
||||||
Users = userOutputs
|
|
||||||
};
|
// 查询该 OpenId 下的所有用户
|
||||||
|
var users = await usersRepository.Context.Queryable<Users>()
|
||||||
|
.Where(u => u.OpenId == input.OpenId && !u.IsDeleted)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (users.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("快捷登录失败,OpenId: {OpenId} 下无用户", input.OpenId);
|
||||||
|
throw new BusinessException("未找到该微信账号关联的用户,请先完成注册", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("快捷登录成功,OpenId: {OpenId} 下存在 {Count} 个用户", input.OpenId, users.Count);
|
||||||
|
|
||||||
|
return await BuildLoginOutputAsync(input.OpenId, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -193,6 +246,90 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建登录输出(生成 Token + 映射用户列表)
|
||||||
|
/// </summary>
|
||||||
|
private async Task<WeChatLoginOutput> BuildLoginOutputAsync(string openId, List<Users> users)
|
||||||
|
{
|
||||||
|
// 优先使用 IsLastOnline 的用户,否则取第一个
|
||||||
|
var primaryUser = users.FirstOrDefault(u => u.IsLastOnline) ?? users.First();
|
||||||
|
|
||||||
|
var jwtSettings = GetJwtSettings();
|
||||||
|
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
|
||||||
|
|
||||||
|
// 缓存 Token 到 Redis
|
||||||
|
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||||
|
|
||||||
|
var userOutputs = users.Select(MapUserToOutput).ToList();
|
||||||
|
|
||||||
|
return new WeChatLoginOutput
|
||||||
|
{
|
||||||
|
Token = token,
|
||||||
|
OpenId = openId,
|
||||||
|
Users = userOutputs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取微信 access_token(带 Redis 缓存)
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string> GetAccessTokenAsync(WeChatSettings settings)
|
||||||
|
{
|
||||||
|
// 先从 Redis 缓存获取
|
||||||
|
var cachedToken = await RedisHelper.StringGetAsync(AccessTokenCacheKey);
|
||||||
|
if (!string.IsNullOrWhiteSpace(cachedToken))
|
||||||
|
{
|
||||||
|
return cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中,调用微信接口获取
|
||||||
|
var url = string.Format(GetAccessTokenUrl, settings.AppId, settings.AppSecret);
|
||||||
|
var response = await HttpHelper.GetAsync<WxAccessTokenResponse>(url);
|
||||||
|
|
||||||
|
if (response == null || response.ErrCode != 0 || string.IsNullOrWhiteSpace(response.AccessToken))
|
||||||
|
{
|
||||||
|
var errMsg = response?.ErrMsg ?? "未知错误";
|
||||||
|
logger.LogError("获取微信 access_token 失败,errcode: {ErrCode}, errmsg: {ErrMsg}", response?.ErrCode, errMsg);
|
||||||
|
throw new BusinessException("微信服务请求失败,请稍后重试", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存 access_token,提前 5 分钟过期(微信默认 7200 秒)
|
||||||
|
var expiresIn = response.ExpiresIn > 300 ? response.ExpiresIn - 300 : response.ExpiresIn;
|
||||||
|
await RedisHelper.StringSetAsync(AccessTokenCacheKey, response.AccessToken, TimeSpan.FromSeconds(expiresIn));
|
||||||
|
|
||||||
|
logger.LogInformation("获取微信 access_token 成功,有效期: {ExpiresIn} 秒", expiresIn);
|
||||||
|
return response.AccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 phone_code 获取微信用户手机号
|
||||||
|
/// </summary>
|
||||||
|
private async Task<string?> GetPhoneNumberAsync(WeChatSettings settings, string phoneCode)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var accessToken = await GetAccessTokenAsync(settings);
|
||||||
|
var url = string.Format(GetPhoneNumberUrl, accessToken);
|
||||||
|
var response = await HttpHelper.PostAsync<WxPhoneNumberResponse>(url, new { code = phoneCode });
|
||||||
|
|
||||||
|
if (response == null || response.ErrCode != 0 || response.PhoneInfo == null)
|
||||||
|
{
|
||||||
|
var errMsg = response?.ErrMsg ?? "未知错误";
|
||||||
|
logger.LogWarning("获取手机号失败,errcode: {ErrCode}, errmsg: {ErrMsg}", response?.ErrCode, errMsg);
|
||||||
|
// 获取手机号失败不阻断登录流程,仅记录日志
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.PhoneInfo.PurePhoneNumber ?? response.PhoneInfo.PhoneNumber;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "调用微信获取手机号接口异常");
|
||||||
|
// 获取手机号失败不阻断登录流程
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户实体映射为输出 DTO
|
/// 用户实体映射为输出 DTO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 签到控制器
|
||||||
|
/// </summary>
|
||||||
|
public class CheckInController : WeChatBaseController
|
||||||
|
{
|
||||||
|
private readonly ICheckInService _checkInService;
|
||||||
|
private readonly ILogger<CheckInController> _logger;
|
||||||
|
|
||||||
|
public CheckInController(ICheckInService checkInService, ILogger<CheckInController> logger)
|
||||||
|
{
|
||||||
|
_checkInService = checkInService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户签到
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>签到结果(含奖励详情和余额)</returns>
|
||||||
|
[HttpPost("checkIn")]
|
||||||
|
public async Task<BaseResponse<CheckInOutput>> CheckInAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<CheckInOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _checkInService.CheckInAsync(userId.Value);
|
||||||
|
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>
|
||||||
|
/// <returns>签到信息</returns>
|
||||||
|
[HttpGet("info")]
|
||||||
|
public async Task<BaseResponse<CheckInInfoOutput>> GetCheckInInfoAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<CheckInInfoOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _checkInService.GetCheckInInfoAsync(userId.Value);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "获取签到信息业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<CheckInInfoOutput>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取签到信息系统异常");
|
||||||
|
return BaseResponse<CheckInInfoOutput>.Fail("获取签到信息失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序期刊管理控制器
|
||||||
|
/// </summary>
|
||||||
|
public class JournalController : WeChatBaseController
|
||||||
|
{
|
||||||
|
private readonly IUserJournalService _userJournalService;
|
||||||
|
private readonly ILogger<JournalController> _logger;
|
||||||
|
|
||||||
|
public JournalController(IUserJournalService userJournalService, ILogger<JournalController> logger)
|
||||||
|
{
|
||||||
|
_userJournalService = userJournalService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户扫码绑定期刊
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">绑定输入(JournalId、JournalInstanceId 从扫码内容解析,Type 默认 Subscribe)</param>
|
||||||
|
/// <returns>绑定结果</returns>
|
||||||
|
[HttpPost("bind")]
|
||||||
|
public async Task<BaseResponse<BindJournalOutput>> BindAsync([FromBody] BindJournalInput input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<BindJournalOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userJournalService.BindJournalAsync(userId.Value, input);
|
||||||
|
return Success(result, "绑定期刊成功");
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "绑定期刊业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<BindJournalOutput>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "绑定期刊系统异常,参数:{Input}", input);
|
||||||
|
return BaseResponse<BindJournalOutput>.Fail("绑定期刊失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户的期刊绑定列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">查询条件(期刊Id、实例Id、关联类型)</param>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
[HttpPost("list")]
|
||||||
|
public async Task<BaseResponse<PageListModel<BindJournalOutput>>> GetListAsync([FromBody] UserJournalQueryInput input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<PageListModel<BindJournalOutput>>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userJournalService.GetUserJournalsAsync(userId.Value, input);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "查询期刊绑定列表业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<PageListModel<BindJournalOutput>>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "查询期刊绑定列表系统异常");
|
||||||
|
return BaseResponse<PageListModel<BindJournalOutput>>.Fail("查询期刊绑定列表失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QYZH.InteractiveMagazine.IService;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序宠物管理控制器
|
||||||
|
/// </summary>
|
||||||
|
public class PetController : WeChatBaseController
|
||||||
|
{
|
||||||
|
private readonly IPetService _petService;
|
||||||
|
private readonly ILogger<PetController> _logger;
|
||||||
|
|
||||||
|
public PetController(IPetService petService, ILogger<PetController> logger)
|
||||||
|
{
|
||||||
|
_petService = petService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户的宠物信息
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("mine")]
|
||||||
|
public async Task<BaseResponse<PetOutput>> GetMyPetAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<PetOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var pet = await _petService.GetPetByUserIdAsync(userId.Value);
|
||||||
|
if (pet == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<PetOutput>.Fail("未找到宠物信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Success(pet);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "获取宠物信息业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<PetOutput>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取宠物信息系统异常");
|
||||||
|
return BaseResponse<PetOutput>.Fail("获取宠物信息失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 喂养宠物(增加成长值,触发进化检查)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("feed")]
|
||||||
|
public async Task<BaseResponse<FeedPetOutput>> FeedPetAsync([FromBody] FeedPetInput input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<FeedPetOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _petService.FeedPetAsync(userId.Value, input);
|
||||||
|
return Success(result, "喂养成功");
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "喂养宠物业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<FeedPetOutput>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "喂养宠物系统异常,参数:{@Input}", input);
|
||||||
|
return BaseResponse<FeedPetOutput>.Fail("喂养宠物失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取宠物喂养记录列表
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("records/{petId}")]
|
||||||
|
public async Task<BaseResponse<PageListModel<FeedingRecordOutput>>> GetFeedingRecordsAsync(long petId, [FromQuery] PageQueryModel pageQuery)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return BaseResponse<PageListModel<FeedingRecordOutput>>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _petService.GetFeedingRecordsAsync(userId.Value, petId, pageQuery);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "查询喂养记录业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<PageListModel<FeedingRecordOutput>>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "查询喂养记录系统异常");
|
||||||
|
return BaseResponse<PageListModel<FeedingRecordOutput>>.Fail("查询喂养记录失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,10 +22,10 @@ public class WeChatAuthController : WeChatBaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信小程序一键登录
|
/// 微信小程序登录(首次创建用户,非首次直接登录)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">登录输入(含微信 code)</param>
|
/// <param name="input">登录输入(含微信 code 和可选的手机号 code)</param>
|
||||||
/// <returns>登录结果(含 Token 和用户信息)</returns>
|
/// <returns>登录结果(含 Token 和用户列表)</returns>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<BaseResponse<WeChatLoginOutput>> LoginAsync([FromBody] WeChatLoginInput input)
|
public async Task<BaseResponse<WeChatLoginOutput>> LoginAsync([FromBody] WeChatLoginInput input)
|
||||||
@ -47,6 +47,32 @@ public class WeChatAuthController : WeChatBaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">快捷登录输入(含 OpenId)</param>
|
||||||
|
/// <returns>登录结果(含 Token 和用户列表)</returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("quickLogin")]
|
||||||
|
public async Task<BaseResponse<WeChatLoginOutput>> QuickLoginAsync([FromBody] WeChatQuickLoginInput input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _weChatAuthService.QuickLoginAsync(input);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "微信快捷登录业务异常: {Message}", ex.Message);
|
||||||
|
return BaseResponse<WeChatLoginOutput>.Fail(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "微信快捷登录系统异常");
|
||||||
|
return BaseResponse<WeChatLoginOutput>.Fail("快捷登录失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 切换用户(同一 OpenId 下切换身份)
|
/// 切换用户(同一 OpenId 下切换身份)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("wechat/api/[controller]")]
|
||||||
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Wechat))]
|
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Wechat))]
|
||||||
public abstract class WeChatBaseController : ControllerBase
|
public abstract class WeChatBaseController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user