feat: 新增签到、宠物、期刊绑定、补偿任务等业务模块,优化微信登录流程

本次提交完成了多个核心业务模块的开发与优化:
1.  宠物模块:新增宠物实体、服务接口与实现,支持创建默认宠物、激活、喂养、进化以及喂养记录查询
2.  签到模块:新增签到实体、服务接口、控制器以及相关DTO,支持用户签到和签到信息查询,新增成长值奖励字段
3.  期刊绑定模块:新增用户期刊关联实体、服务接口与控制器,支持扫码绑定期刊、解绑和查询绑定列表
4.  补偿任务模块:新增补偿任务实体、服务接口与实现,用于处理业务失败后的异步重试补偿
5.  优化微信登录流程:拆分登录与快捷登录接口,支持手机号获取,新增首次登录自动创建默认宠物逻辑
6.  调整基础路由与实体状态:修改微信控制器路由前缀,更新宠物状态枚举与默认值
This commit is contained in:
glz
2026-06-04 15:54:30 +08:00
parent 018d7e1733
commit 903ccd3073
25 changed files with 2411 additions and 32 deletions

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

View File

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

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

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

View File

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

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

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

View File

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

View File

@ -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("获取签到信息失败,请稍后重试");
}
}
}

View File

@ -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("查询期刊绑定列表失败,请稍后重试");
}
}
}

View File

@ -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("查询喂养记录失败,请稍后重试");
}
}
}

View File

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

View File

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