From 8291b823620e326041a29dffabd2645073ae32c0 Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Fri, 5 Jun 2026 17:15:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=AE=A0=E7=89=A9?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E9=87=8D=E6=9E=84=E4=B8=8E=E7=A4=BE=E5=8C=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交包含多项核心更新: 1. 重构宠物模块数据结构:拆分皮肤图片为独立表,优化Pet、PetEvolution实体,调整字段类型与冗余字段 2. 新增宠物皮肤图片管理表,支持多进化阶段多图片展示 3. 完善宠物DTO,新增当前形态名称、皮肤信息与图片序列返回 4. 新增社区功能模块: - 微信端社区Feed流、点赞/取消点赞接口 - 后台社区消息管理接口与服务实现 5. 优化商城与背包模块,替换皮肤图片获取逻辑为从新表读取预览图 6. 重构勋章服务,新增规则校验逻辑 7. 调整命名规范,修复原有控制器命名问题 --- .../ICommunityMessageService.cs | 44 +++ .../IWeChatCommunityService.cs | 31 ++ .../Dto/Bag/UserBagDto.cs | 6 +- .../Dto/Community/CommunityDto.cs | 288 +++++++++++++++++ .../Dto/Mall/WxMallDto.cs | 6 +- .../Dto/Pet/PetDto.cs | 43 ++- QYZH.InteractiveMagazine.Models/Entity/Pet.cs | 111 ++++--- .../Entity/PetEvolution.cs | 132 ++++---- .../Entity/PetSkin.cs | 21 +- .../Entity/PetSkinImage.cs | 48 +++ .../CommunityMessageService.cs | 219 +++++++++++++ .../MedalService.cs | 146 +++++++++ .../PetService.cs | 79 ++++- .../WeChatCommunityService.cs | 296 ++++++++++++++++++ .../WxMallService.cs | 138 ++++++-- .../Controllers/CommunityMessageController.cs | 163 ++++++++++ .../Controllers/WeChat/CommunityController.cs | 129 ++++++++ ...tMedalController.cs => MedalController.cs} | 2 +- SqlMigrations/pet_module_optimization.sql | 33 ++ 19 files changed, 1753 insertions(+), 182 deletions(-) create mode 100644 QYZH.InteractiveMagazine.IService/ICommunityMessageService.cs create mode 100644 QYZH.InteractiveMagazine.IService/IWeChatCommunityService.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Community/CommunityDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Entity/PetSkinImage.cs create mode 100644 QYZH.InteractiveMagazine.Service/CommunityMessageService.cs create mode 100644 QYZH.InteractiveMagazine.Service/WeChatCommunityService.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs rename QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/{WeChatMedalController.cs => MedalController.cs} (96%) create mode 100644 SqlMigrations/pet_module_optimization.sql diff --git a/QYZH.InteractiveMagazine.IService/ICommunityMessageService.cs b/QYZH.InteractiveMagazine.IService/ICommunityMessageService.cs new file mode 100644 index 0000000..8a2b896 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/ICommunityMessageService.cs @@ -0,0 +1,44 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 社区消息后台管理服务接口 +/// +public interface ICommunityMessageService : IBaseService +{ + /// + /// 分页查询消息列表 + /// + Task> GetListAsync(AdminMessageQueryInput input); + + /// + /// 查看消息详情 + /// + Task GetDetailAsync(long id); + + /// + /// 软删除消息 + /// + Task DeleteAsync(long id); + + /// + /// 冻结/解冻消息 + /// + /// 消息ID + /// 状态: 1=解冻(通过), 2=冻结 + Task FreezeAsync(long id, int status); + + /// + /// 设置/取消精选 + /// + /// 消息ID + /// 0=取消, 1=精选 + Task SetFeaturedAsync(long id, int isFeatured); + + /// + /// 设置排序权重 + /// + Task SetSortOrderAsync(long id, int sortOrder); +} diff --git a/QYZH.InteractiveMagazine.IService/IWeChatCommunityService.cs b/QYZH.InteractiveMagazine.IService/IWeChatCommunityService.cs new file mode 100644 index 0000000..5d339c7 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IWeChatCommunityService.cs @@ -0,0 +1,31 @@ +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 社区微信端服务接口 +/// +public interface IWeChatCommunityService +{ + /// + /// 获取社区Feed流(翻页) + /// + /// 当前用户ID + /// 游标(上一页最后一条消息的ID,首次传null) + Task GetFeedAsync(long userId, long? cursor); + + /// + /// 下拉刷新(返回最新列表) + /// + Task RefreshFeedAsync(long userId); + + /// + /// 点赞 + /// + Task LikeAsync(long userId, WxLikeInput input); + + /// + /// 取消点赞 + /// + Task UnlikeAsync(long userId, long messageId); +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Bag/UserBagDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Bag/UserBagDto.cs index d23cdab..3e678fc 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Bag/UserBagDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Bag/UserBagDto.cs @@ -41,8 +41,12 @@ public class BagSkinBrief { public long SkinId { get; set; } public string SkinName { get; set; } = string.Empty; - public string? SkinImage { get; set; } public string Rarity { get; set; } = string.Empty; + + /// + /// 皮肤预览图(取第一张图片) + /// + public string? PreviewImage { get; set; } } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/Community/CommunityDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Community/CommunityDto.cs new file mode 100644 index 0000000..dda8f57 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Community/CommunityDto.cs @@ -0,0 +1,288 @@ +namespace QYZH.InteractiveMagazine.Models.Dto; + +#region 后台管理 DTO + +/// +/// 社区消息分页查询输入(后台) +/// +public class AdminMessageQueryInput : PageQueryModel +{ + /// + /// 期刊ID + /// + public long? JournalId { get; set; } + + /// + /// 消息类型: Article, Quote, Announcement + /// + public string? Type { get; set; } + + /// + /// 状态: 0=审核中, 1=已通过, 2=已冻结 + /// + public int? Status { get; set; } + + /// + /// 是否精选: 0=否, 1=是 + /// + public int? IsFeatured { get; set; } + + /// + /// 是否启用 + /// + public bool? IsActive { get; set; } + + /// + /// 内容关键词(模糊查询) + /// + public string? Keyword { get; set; } + + /// + /// 用户ID + /// + public long? UserId { get; set; } +} + +/// +/// 社区消息详情输出(后台) +/// +public class AdminMessageDetailOutput +{ + /// + /// 主键ID + /// + public long Id { get; set; } + + /// + /// 期刊ID + /// + public long JournalId { get; set; } + + /// + /// 用户ID + /// + public long UserId { get; set; } + + /// + /// 用户实例期刊ID + /// + public long UserJournalId { get; set; } + + /// + /// 期刊任务ID + /// + public long JournalTaskId { get; set; } + + /// + /// 用户期刊任务回答ID + /// + public long JournalTaskAnswerId { get; set; } + + /// + /// 消息内容 + /// + public string Content { get; set; } = string.Empty; + + /// + /// 配图 + /// + public string? ImageUrl { get; set; } + + /// + /// 排序权重 + /// + public int SortOrder { get; set; } + + /// + /// 是否启用 + /// + public bool IsActive { get; set; } + + /// + /// 消息类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 点赞数 + /// + public int LikeCount { get; set; } + + /// + /// 是否精选 + /// + public int IsFeatured { get; set; } + + /// + /// 状态: 0=审核中, 1=已通过, 2=已冻结 + /// + public int Status { get; set; } + + /// + /// 创建人 + /// + public string? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 更新人 + /// + public string? UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdatedAt { get; set; } +} + +/// +/// 设置精选入参 +/// +public class AdminSetFeaturedInput +{ + /// + /// 是否精选: 0=否, 1=是 + /// + public int IsFeatured { get; set; } +} + +/// +/// 设置排序权重入参 +/// +public class AdminSetSortOrderInput +{ + /// + /// 排序权重 + /// + public int SortOrder { get; set; } +} + +/// +/// 冻结/解冻入参 +/// +public class AdminFreezeInput +{ + /// + /// 状态: 1=解冻(通过), 2=冻结 + /// + public int Status { get; set; } +} + +#endregion + +#region 微信端 DTO + +/// +/// 微信端社区Feed消息输出 +/// +public class WxFeedMessageOutput +{ + /// + /// 消息ID + /// + public long Id { get; set; } + + /// + /// 期刊ID + /// + public long JournalId { get; set; } + + /// + /// 作者用户ID + /// + public long UserId { get; set; } + + /// + /// 消息内容 + /// + public string Content { get; set; } = string.Empty; + + /// + /// 配图 + /// + public string? ImageUrl { get; set; } + + /// + /// 消息类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 点赞数 + /// + public int LikeCount { get; set; } + + /// + /// 是否精选 + /// + public int IsFeatured { get; set; } + + /// + /// 当前用户是否已点赞 + /// + public bool IsLiked { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } +} + +/// +/// 微信端社区Feed输出(含游标) +/// +public class WxFeedOutput +{ + /// + /// 消息列表 + /// + public List Messages { get; set; } = new(); + + /// + /// 下一页游标(为null表示没有更多数据) + /// + public long? NextCursor { get; set; } + + /// + /// 是否还有更多数据 + /// + public bool HasMore { get; set; } +} + +/// +/// 微信端点赞入参 +/// +public class WxLikeInput +{ + /// + /// 消息ID + /// + public long MessageId { get; set; } +} + +/// +/// 微信端点赞输出 +/// +public class WxLikeOutput +{ + /// + /// 消息ID + /// + public long MessageId { get; set; } + + /// + /// 当前点赞数 + /// + public int LikeCount { get; set; } + + /// + /// 是否已点赞 + /// + public bool IsLiked { get; set; } +} + +#endregion diff --git a/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs index c5cb70f..92f6c9f 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Mall/WxMallDto.cs @@ -30,9 +30,13 @@ public class PetSkinBrief { public long SkinId { get; set; } public string SkinName { get; set; } = string.Empty; - public string? SkinImage { get; set; } public string? Description { get; set; } public string Rarity { get; set; } = string.Empty; + + /// + /// 皮肤预览图(取当前进化阶段第一张图片) + /// + public string? PreviewImage { get; set; } } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/Pet/PetDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Pet/PetDto.cs index d82dd3e..a935ab0 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Pet/PetDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Pet/PetDto.cs @@ -23,7 +23,12 @@ public class PetOutput /// /// 当前进化形态Id /// - public int CurrentEvolutionId { get; set; } + public long CurrentEvolutionId { get; set; } + + /// + /// 当前进化阶段名称 + /// + public string? EvolutionStageName { get; set; } /// /// 当前成长值 @@ -35,6 +40,21 @@ public class PetOutput /// public int FeedingCount { get; set; } + /// + /// 当前皮肤Id + /// + public long CurrentSkinId { get; set; } + + /// + /// 当前皮肤名称 + /// + public string? CurrentSkinName { get; set; } + + /// + /// 当前形态+当前皮肤 的图片列表(按SortOrder排序) + /// + public List CurrentImages { get; set; } = new(); + /// /// 宠物类型: Normal /// @@ -51,6 +71,27 @@ public class PetOutput public DateTime CreatedAt { get; set; } } +/// +/// 皮肤图片输出 +/// +public class SkinImageOutput +{ + /// + /// 图片Id + /// + public long Id { get; set; } + + /// + /// 图片地址 + /// + public string ImageUrl { get; set; } = string.Empty; + + /// + /// 图片顺序 + /// + public int SortOrder { get; set; } +} + /// /// 喂养宠物输入 /// diff --git a/QYZH.InteractiveMagazine.Models/Entity/Pet.cs b/QYZH.InteractiveMagazine.Models/Entity/Pet.cs index 55a8881..8beb35c 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/Pet.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/Pet.cs @@ -8,64 +8,85 @@ namespace QYZH.InteractiveMagazine.Models.Entity [SugarTable("Pet")] public partial class Pet : SqlSugarBaseEntity { - public Pet(){ + public Pet() + { - } + } /// /// Desc:用户Id /// Default: /// Nullable:False /// - public long UserId {get;set;} + public long UserId { get; set; } - /// - /// Desc:宠物昵称 - /// Default: - /// Nullable:True - /// - public string Name {get;set;} + /// + /// Desc:宠物昵称 + /// Default: + /// Nullable:True + /// + public string Name { get; set; } - /// - /// Desc:当前进化形态Id - /// Default: - /// Nullable:False - /// - public int CurrentEvolutionId {get;set;} + /// + /// Desc:当前进化形态Id + /// Default: + /// Nullable:False + /// + public long CurrentEvolutionId { get; set; } - /// - /// Desc:当前成长值 - /// Default:0 - /// Nullable:False - /// - public int GrowthPoints {get;set;} + /// + /// Desc:当前成长值 + /// Default:0 + /// Nullable:False + /// + public int GrowthPoints { get; set; } - /// - /// Desc:累计喂养次数 - /// Default:0 - /// Nullable:False - /// - public int FeedingCount {get;set;} + /// + /// Desc:累计喂养次数 + /// Default:0 + /// Nullable:False + /// + public int FeedingCount { get; set; } - /// - /// Desc:当前皮肤Id(0为默认皮肤) - /// Default:0 - /// Nullable:False - /// - public long CurrentSkinId {get;set;} + /// + /// Desc:当前皮肤Id(0为默认皮肤) + /// Default:0 + /// Nullable:False + /// + public long CurrentSkinId { get; set; } - /// - /// Desc:宠物类型 - /// Default:Normal - /// Nullable:False - /// - public string Type {get;set;} + /// + /// Desc:宠物类型 + /// Default:Normal + /// Nullable:False + /// + public string Type { get; set; } - /// - /// Desc:状态: Inactive, Active, Sleeping - /// Default:Inactive - /// Nullable:False - /// - public string Status {get;set;} + /// + /// Desc:状态: Inactive, Active, Sleeping + /// Default:Inactive + /// Nullable:False + /// + public new string Status { get; set; } + + /// + /// 理解力 + /// + public int Comprehension { get; set; } + + /// + /// 判断力 + /// + public int Judgment { get; set; } + + /// + /// 表达力 + /// + public int Expression { get; set; } + + /// + /// 说服力 + /// + public int Persuasiveness { get; set; } } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/PetEvolution.cs b/QYZH.InteractiveMagazine.Models/Entity/PetEvolution.cs index be6fd5d..3873010 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/PetEvolution.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/PetEvolution.cs @@ -8,86 +8,76 @@ namespace QYZH.InteractiveMagazine.Models.Entity [SugarTable("PetEvolution")] public partial class PetEvolution : SqlSugarBaseEntity { - public PetEvolution(){ + public PetEvolution() { } + /// + /// Desc:阶段名称 + /// Default: + /// Nullable:False + /// + public string StageName { get; set; } - } + /// + /// Desc:阶段等级 + /// Default: + /// Nullable:False + /// + public int StageLevel { get; set; } - /// - /// Desc:阶段名称 - /// Default: - /// Nullable:False - /// - public string StageName {get;set;} + /// + /// Desc:进化所需成长值 + /// Default: + /// Nullable:False + /// + public int RequiredGrowth { get; set; } - /// - /// Desc:阶段等级 - /// Default: - /// Nullable:False - /// - public int StageLevel {get;set;} + /// + /// Desc:前一形态Id(null表示初始形态) + /// Default: + /// Nullable:True + /// + public long? PreviousEvolutionId { get; set; } - /// - /// Desc:进化所需成长值 - /// Default: - /// Nullable:False - /// - public int RequiredGrowth {get;set;} + /// + /// Desc:基础力量 + /// Default:0 + /// Nullable:False + /// + public int BaseStrength { get; set; } - /// - /// Desc:形态图片 - /// Default: - /// Nullable:True - /// - public string ImageUrl {get;set;} + /// + /// Desc:基础敏捷 + /// Default:0 + /// Nullable:False + /// + public int BaseAgility { get; set; } - /// - /// Desc:前一形态Id - /// Default: - /// Nullable:True - /// - public int? PreviousEvolutionId {get;set;} + /// + /// Desc:基础智力 + /// Default:0 + /// Nullable:False + /// + public int BaseIntelligence { get; set; } - /// - /// Desc:基础力量 - /// Default:0 - /// Nullable:False - /// - public int BaseStrength {get;set;} + /// + /// Desc:基础魅力 + /// Default:0 + /// Nullable:False + /// + public int BaseCharm { get; set; } - /// - /// Desc:基础敏捷 - /// Default:0 - /// Nullable:False - /// - public int BaseAgility {get;set;} + /// + /// Desc:进化类型: Normal, Special + /// Default:Normal + /// Nullable:False + /// + public string Type { get; set; } - /// - /// Desc:基础智力 - /// Default:0 - /// Nullable:False - /// - public int BaseIntelligence {get;set;} - - /// - /// Desc:基础魅力 - /// Default:0 - /// Nullable:False - /// - public int BaseCharm {get;set;} - - /// - /// Desc:进化类型: Normal, Special - /// Default:Normal - /// Nullable:False - /// - public string Type {get;set;} - - /// - /// Desc:状态: Active, Inactive - /// Default:Active - /// Nullable:False - /// - public string Status {get;set;} + /// + /// Desc:状态: Active, Inactive + /// Default:Active + /// Nullable:False + /// + public new string Status { get; set; } } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/PetSkin.cs b/QYZH.InteractiveMagazine.Models/Entity/PetSkin.cs index 9450908..bcbea9a 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/PetSkin.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/PetSkin.cs @@ -17,13 +17,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// public string Name { get; set; } - /// - /// Desc:皮肤图片 - /// Default: - /// Nullable:True - /// - public string ImageUrl { get; set; } - /// /// Desc:描述/特效说明 /// Default: @@ -31,13 +24,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// public string Description { get; set; } - /// - /// Desc:关联商品Id(用于兑换) - /// Default: - /// Nullable:False - /// - public long ProductId { get; set; } - /// /// Desc:稀有度: Normal, Rare, Epic, Legendary /// Default:Normal @@ -51,5 +37,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Nullable:False /// public int SortOrder { get; set; } + + /// + /// Desc:皮肤类型: Normal, Limited, Event + /// Default:Normal + /// Nullable:False + /// + public string Type { get; set; } } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/PetSkinImage.cs b/QYZH.InteractiveMagazine.Models/Entity/PetSkinImage.cs new file mode 100644 index 0000000..e5d9714 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/PetSkinImage.cs @@ -0,0 +1,48 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity +{ + /// + /// 宠物皮肤图片表(每个皮肤在每个进化阶段下有一组有序图片,用于动态表示) + /// + [SugarTable("PetSkinImage")] + public partial class PetSkinImage : SqlSugarBaseEntity + { + public PetSkinImage() { } + + /// + /// Desc:皮肤Id,关联PetSkin.Id(0表示默认皮肤的图片) + /// Default: + /// Nullable:False + /// + public long SkinId { get; set; } + + /// + /// Desc:进化阶段Id,关联PetEvolution.Id + /// Default: + /// Nullable:False + /// + public long EvolutionStageId { get; set; } + + /// + /// Desc:图片地址 + /// Default: + /// Nullable:False + /// + public string ImageUrl { get; set; } + + /// + /// Desc:图片顺序(动画帧序号) + /// Default:0 + /// Nullable:False + /// + public int SortOrder { get; set; } + + /// + /// Desc:图片类型: Normal, Special + /// Default:Normal + /// Nullable:False + /// + public string Type { get; set; } + } +} diff --git a/QYZH.InteractiveMagazine.Service/CommunityMessageService.cs b/QYZH.InteractiveMagazine.Service/CommunityMessageService.cs new file mode 100644 index 0000000..591eb97 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/CommunityMessageService.cs @@ -0,0 +1,219 @@ +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; + +/// +/// 社区消息后台管理服务实现 +/// +public class CommunityMessageService(BaseRepository messageRepository, ILogger logger) + : BaseRepository, ICommunityMessageService +{ + + /// + /// 分页查询消息列表 + /// + public async Task> GetListAsync(AdminMessageQueryInput input) + { + logger.LogInformation("正在查询社区消息列表,页码: {PageIndex}, 每页条数: {PageSize}", input.PageIndex, input.PageSize); + + if (input.PageIndex <= 0) throw new BusinessException("页码必须大于0", 400); + if (input.PageSize <= 0 || input.PageSize > 100) throw new BusinessException("每页条数必须在1-100之间", 400); + + RefAsync totalNumber = 0; + var pageResult = await messageRepository.Queryable() + .WhereIF(input.JournalId.HasValue, m => m.JournalId == input.JournalId.Value) + .WhereIF(!string.IsNullOrWhiteSpace(input.Type), m => m.Type == input.Type) + .WhereIF(input.Status.HasValue, m => m.Status == input.Status.Value) + .WhereIF(input.IsFeatured.HasValue, m => m.IsFeatured == input.IsFeatured.Value) + .WhereIF(input.IsActive.HasValue, m => m.IsActive == input.IsActive.Value) + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), m => m.Content.Contains(input.Keyword)) + .WhereIF(input.UserId.HasValue, m => m.UserId == input.UserId.Value) + .OrderByDescending(m => m.IsFeatured) + .OrderByDescending(m => m.CreatedAt) + .Select(m => new AdminMessageDetailOutput + { + Id = m.Id, + JournalId = m.JournalId, + UserId = m.UserId, + UserJournalId = m.UserJournalId, + JournalTaskId = m.JournalTaskId, + JournalTaskAnswerId = m.JournalTaskAnswerId, + Content = m.Content, + ImageUrl = m.ImageUrl, + SortOrder = m.SortOrder, + IsActive = m.IsActive, + Type = m.Type, + LikeCount = m.LikeCount, + IsFeatured = m.IsFeatured, + Status = m.Status, + CreatedBy = m.CreatedBy, + CreatedAt = m.CreatedAt, + UpdatedBy = m.UpdatedBy, + UpdatedAt = m.UpdatedAt + }, true) + .ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); + + return new PageListModel(pageResult, input.PageIndex, input.PageSize, totalNumber); + } + + /// + /// 查看消息详情 + /// + public async Task GetDetailAsync(long id) + { + logger.LogInformation("正在获取社区消息详情,ID: {Id}", id); + + var message = await messageRepository.GetByIdAsync(id); + if (message == null) + { + logger.LogWarning("未找到社区消息,ID: {Id}", id); + throw new BusinessException("消息不存在", 404); + } + + return new AdminMessageDetailOutput + { + Id = message.Id, + JournalId = message.JournalId, + UserId = message.UserId, + UserJournalId = message.UserJournalId, + JournalTaskId = message.JournalTaskId, + JournalTaskAnswerId = message.JournalTaskAnswerId, + Content = message.Content, + ImageUrl = message.ImageUrl, + SortOrder = message.SortOrder, + IsActive = message.IsActive, + Type = message.Type, + LikeCount = message.LikeCount, + IsFeatured = message.IsFeatured, + Status = message.Status, + CreatedBy = message.CreatedBy, + CreatedAt = message.CreatedAt, + UpdatedBy = message.UpdatedBy, + UpdatedAt = message.UpdatedAt + }; + } + + /// + /// 软删除消息 + /// + public async Task DeleteAsync(long id) + { + logger.LogInformation("正在删除社区消息,ID: {Id}", id); + + var message = await messageRepository.GetByIdAsync(id); + if (message == null) + { + logger.LogWarning("未找到要删除的社区消息,ID: {Id}", id); + throw new BusinessException("消息不存在", 404); + } + + var result = await messageRepository.DeleteByIdAsync(id); + if (!result) + { + logger.LogError("社区消息删除失败,ID: {Id}", id); + throw new BusinessException("删除消息失败", 500); + } + + logger.LogInformation("社区消息删除成功,ID: {Id}", id); + } + + /// + /// 冻结/解冻消息 + /// + public async Task FreezeAsync(long id, int status) + { + logger.LogInformation("正在更新社区消息冻结状态,ID: {Id}, Status: {Status}", id, status); + + if (status != 1 && status != 2) + { + throw new BusinessException("状态值无效,只能为1(解冻/通过)或2(冻结)", 400); + } + + var message = await messageRepository.GetByIdAsync(id); + if (message == null) + { + logger.LogWarning("未找到社区消息,ID: {Id}", id); + throw new BusinessException("消息不存在", 404); + } + + message.Status = status; + message.UpdatedBy = "System"; + message.UpdatedAt = DateTime.Now; + + var result = await messageRepository.UpdateAsync(message); + if (!result) + { + logger.LogError("社区消息冻结状态更新失败,ID: {Id}", id); + throw new BusinessException("更新冻结状态失败", 500); + } + + logger.LogInformation("社区消息冻结状态更新成功,ID: {Id}, Status: {Status}", id, status); + } + + /// + /// 设置/取消精选 + /// + public async Task SetFeaturedAsync(long id, int isFeatured) + { + logger.LogInformation("正在设置社区消息精选状态,ID: {Id}, IsFeatured: {IsFeatured}", id, isFeatured); + + if (isFeatured != 0 && isFeatured != 1) + { + throw new BusinessException("精选值无效,只能为0(取消)或1(精选)", 400); + } + + var message = await messageRepository.GetByIdAsync(id); + if (message == null) + { + logger.LogWarning("未找到社区消息,ID: {Id}", id); + throw new BusinessException("消息不存在", 404); + } + + message.IsFeatured = isFeatured; + message.UpdatedBy = "System"; + message.UpdatedAt = DateTime.Now; + + var result = await messageRepository.UpdateAsync(message); + if (!result) + { + logger.LogError("社区消息精选设置失败,ID: {Id}", id); + throw new BusinessException("设置精选失败", 500); + } + + logger.LogInformation("社区消息精选设置成功,ID: {Id}, IsFeatured: {IsFeatured}", id, isFeatured); + } + + /// + /// 设置排序权重 + /// + public async Task SetSortOrderAsync(long id, int sortOrder) + { + logger.LogInformation("正在设置社区消息排序权重,ID: {Id}, SortOrder: {SortOrder}", id, sortOrder); + + var message = await messageRepository.GetByIdAsync(id); + if (message == null) + { + logger.LogWarning("未找到社区消息,ID: {Id}", id); + throw new BusinessException("消息不存在", 404); + } + + message.SortOrder = sortOrder; + message.UpdatedBy = "System"; + message.UpdatedAt = DateTime.Now; + + var result = await messageRepository.UpdateAsync(message); + if (!result) + { + logger.LogError("社区消息排序权重设置失败,ID: {Id}", id); + throw new BusinessException("设置排序权重失败", 500); + } + + logger.LogInformation("社区消息排序权重设置成功,ID: {Id}, SortOrder: {SortOrder}", id, sortOrder); + } +} diff --git a/QYZH.InteractiveMagazine.Service/MedalService.cs b/QYZH.InteractiveMagazine.Service/MedalService.cs index f1074d6..782bbf9 100644 --- a/QYZH.InteractiveMagazine.Service/MedalService.cs +++ b/QYZH.InteractiveMagazine.Service/MedalService.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; @@ -380,6 +381,14 @@ public class MedalService(BaseRepository medalRepository, ILogger() .Where(um => um.UserId == userId && um.MedalId == (int)input.MedalId && um.Status == "Awarded") .FirstAsync(); @@ -498,5 +507,142 @@ public class MedalService(BaseRepository medalRepository, ILogger + /// 校验勋章规则是否满足 + /// + /// 勋章ID + /// 用户ID + /// (是否通过, 失败原因) + private async Task<(bool passed, string? failReason)> ValidateMedalRulesAsync(long medalId, long userId) + { + var rules = await Context.Queryable() + .Where(r => r.MedalId == medalId && !r.IsDeleted) + .OrderBy(r => r.RuleOrder) + .ToListAsync(); + + if (!rules.Any()) + { + return (true, null); + } + + var failReasons = new List(); + + foreach (var rule in rules) + { + var rulePassed = await EvaluateRuleAsync(rule, userId); + + if (!rulePassed) + { + var reason = !string.IsNullOrWhiteSpace(rule.Description) + ? rule.Description + : $"未满足条件: {rule.TargetField} {rule.Operator} {rule.ThresholdValue}"; + failReasons.Add(reason); + } + + var logicOp = rule.LogicOperator?.ToUpper() ?? "AND"; + + if (logicOp == "AND" && !rulePassed) + { + return (false, $"未满足获得条件:{string.Join(";", failReasons)}"); + } + + if (logicOp == "OR" && rulePassed) + { + return (true, null); + } + } + + // AND 模式下所有规则都通过才到这里 + // OR 模式下所有规则都没通过才到这里 + var firstLogicOp = rules.First().LogicOperator?.ToUpper() ?? "AND"; + if (firstLogicOp == "AND") + { + return (true, null); + } + else + { + return (false, $"未满足获得条件:{string.Join(";", failReasons)}"); + } + } + + /// + /// 评估单条规则是否满足 + /// + /// 规则配置 + /// 用户ID + /// 规则是否满足 + private async Task EvaluateRuleAsync(MedalRule rule, long userId) + { + try + { + var sql = $"SELECT COUNT(1) FROM [{rule.TargetTable}] WHERE [{rule.TargetField}] {rule.Operator} @ThresholdValue AND IsDeleted = 0 AND UserId = @UserId"; + var parameters = new List + { + new SugarParameter("@ThresholdValue", rule.ThresholdValue), + new SugarParameter("@UserId", userId) + }; + + // 处理额外筛选条件 + if (!string.IsNullOrWhiteSpace(rule.FilterCondition)) + { + var filters = JsonSerializer.Deserialize>(rule.FilterCondition); + if (filters != null) + { + var index = 0; + foreach (var kv in filters) + { + var paramName = $"@Filter_{index}"; + sql += $" AND [{kv.Key}] = {paramName}"; + var paramValue = kv.Value.ValueKind switch + { + JsonValueKind.Number => kv.Value.GetInt32() as object, + JsonValueKind.String => kv.Value.GetString() as object, + JsonValueKind.True => true as object, + JsonValueKind.False => false as object, + _ => kv.Value.GetRawText() + }; + parameters.Add(new SugarParameter(paramName, paramValue!)); + index++; + } + } + } + + // 处理时间范围 + var dateRange = GetDateRange(rule); + if (dateRange.HasValue) + { + sql += " AND CreatedAt >= @DateStart AND CreatedAt <= @DateEnd"; + parameters.Add(new SugarParameter("@DateStart", dateRange.Value.start)); + parameters.Add(new SugarParameter("@DateEnd", dateRange.Value.end)); + } + + var count = await Context.Ado.GetIntAsync(sql, parameters.ToArray()); + return count > 0; + } + catch (Exception ex) + { + logger.LogError(ex, "规则评估异常,规则ID: {RuleId}, 目标表: {Table}, 目标字段: {Field}", + rule.Id, rule.TargetTable, rule.TargetField); + return false; + } + } + + /// + /// 根据规则的时间范围类型计算起止时间 + /// + private static (DateTime start, DateTime end)? GetDateRange(MedalRule rule) + { + var now = DateTime.Now; + + return rule.DateRangeType?.ToLower() switch + { + "currentmonth" => (new DateTime(now.Year, now.Month, 1), now), + "currentweek" => (now.AddDays(-(int)now.DayOfWeek), now), + "last7days" => (now.AddDays(-7), now), + "custom" when rule.DateRangeStart.HasValue && rule.DateRangeEnd.HasValue => (rule.DateRangeStart.Value, rule.DateRangeEnd.Value), + _ => null + }; + } + #endregion } diff --git a/QYZH.InteractiveMagazine.Service/PetService.cs b/QYZH.InteractiveMagazine.Service/PetService.cs index aebd326..0e3d1bc 100644 --- a/QYZH.InteractiveMagazine.Service/PetService.cs +++ b/QYZH.InteractiveMagazine.Service/PetService.cs @@ -16,11 +16,13 @@ public class PetService( BaseRepository petRepository, BaseRepository feedingRecordRepository, BaseRepository petEvolutionRepository, + BaseRepository petSkinImageRepository, + BaseRepository petSkinRepository, ILogger logger) : BaseRepository, IPetService { /// - /// 获取用户宠物信息 + /// 获取用户宠物信息(含当前形态名称、皮肤图片序列) /// public async Task GetPetByUserIdAsync(long userId) { @@ -28,21 +30,56 @@ public class PetService( 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; + if (pet == null) return null; + + // 查询当前进化阶段名称 + var evolution = await petEvolutionRepository.Queryable() + .Where(e => e.Id == pet.CurrentEvolutionId) + .FirstAsync(); + + // 查询当前皮肤名称 + string? skinName = null; + if (pet.CurrentSkinId > 0) + { + var skin = await petSkinRepository.Queryable() + .Where(s => s.Id == pet.CurrentSkinId && !s.IsDeleted) + .FirstAsync(); + skinName = skin?.Name; + } + + // 查询当前形态+当前皮肤 的图片序列 + var skinId = pet.CurrentSkinId; // 0 = 默认皮肤 + var images = await petSkinImageRepository.Queryable() + .Where(i => i.SkinId == skinId + && i.EvolutionStageId == pet.CurrentEvolutionId + && !i.IsDeleted) + .OrderBy(i => i.SortOrder) + .Select(i => new SkinImageOutput + { + Id = i.Id, + ImageUrl = i.ImageUrl, + SortOrder = i.SortOrder + }) + .ToListAsync(); + + return new PetOutput + { + Id = pet.Id, + UserId = pet.UserId, + Name = pet.Name, + CurrentEvolutionId = pet.CurrentEvolutionId, + EvolutionStageName = evolution?.StageName, + GrowthPoints = pet.GrowthPoints, + FeedingCount = pet.FeedingCount, + CurrentSkinId = pet.CurrentSkinId, + CurrentSkinName = skinName, + CurrentImages = images, + Type = pet.Type, + Status = pet.Status, + CreatedAt = pet.CreatedAt + }; } /// @@ -61,13 +98,20 @@ public class PetService( return; } + // 查询初始进化形态(PreviousEvolutionId 为 null 的即为初始形态) + var initialEvolution = await petEvolutionRepository.Queryable() + .Where(e => e.PreviousEvolutionId == null && e.Status == "Active") + .OrderBy(e => e.StageLevel) + .FirstAsync(); + var pet = new Pet { UserId = userId, Name = "小精灵", - CurrentEvolutionId = 1, + CurrentEvolutionId = initialEvolution?.Id ?? 0, GrowthPoints = 0, FeedingCount = 0, + CurrentSkinId = 0, Type = "Normal", Status = "Inactive", IsDeleted = false, @@ -84,7 +128,8 @@ public class PetService( throw new Exception("创建宠物失败"); } - logger.LogInformation("用户默认宠物创建成功,UserId: {UserId}, PetId: {PetId}", userId, pet.Id); + logger.LogInformation("用户默认宠物创建成功,UserId: {UserId}, PetId: {PetId}, EvolutionId: {EvolutionId}", + userId, pet.Id, pet.CurrentEvolutionId); } /// @@ -185,7 +230,7 @@ public class PetService( throw new BusinessException("更新宠物成长值失败", 500); } - // 进化检查:查找下一阶段进化形态 + // 进化检查:查找下一阶段进化形态(PreviousEvolutionId 类型为 long?) var nextEvolution = await petEvolutionRepository.Queryable() .Where(e => e.PreviousEvolutionId == pet.CurrentEvolutionId && e.RequiredGrowth <= growthAfter diff --git a/QYZH.InteractiveMagazine.Service/WeChatCommunityService.cs b/QYZH.InteractiveMagazine.Service/WeChatCommunityService.cs new file mode 100644 index 0000000..333c96d --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/WeChatCommunityService.cs @@ -0,0 +1,296 @@ +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; + +/// +/// 社区微信端服务实现 +/// +public class WeChatCommunityService( + BaseRepository messageRepository, + ILogger logger) + : BaseRepository, IWeChatCommunityService +{ + private const int FeaturedCount = 5; + private const int NormalCount = 5; + + /// + /// 获取社区Feed流(翻页) + /// + public async Task GetFeedAsync(long userId, long? cursor) + { + logger.LogInformation("正在获取社区Feed,用户ID: {UserId}, Cursor: {Cursor}", userId, cursor); + + var journalIds = await GetUserJournalIds(userId); + if (!journalIds.Any()) + { + return new WxFeedOutput { Messages = new List(), HasMore = false }; + } + + // 查询精选消息 + var featuredQuery = messageRepository.Queryable() + .Where(m => journalIds.Contains(m.JournalId) + && m.Status == 1 + && m.IsActive + && m.UserId != userId + && m.IsFeatured == 1); + + // 查询普通消息 + var normalQuery = messageRepository.Queryable() + .Where(m => journalIds.Contains(m.JournalId) + && m.Status == 1 + && m.IsActive + && m.UserId != userId + && m.IsFeatured == 0); + + // 游标分页:按ID降序(ID越大越新) + if (cursor.HasValue) + { + featuredQuery = featuredQuery.Where(m => m.Id < cursor.Value); + normalQuery = normalQuery.Where(m => m.Id < cursor.Value); + } + + var featured = await featuredQuery + .OrderByDescending(m => m.SortOrder) + .Take(FeaturedCount) + .ToListAsync(); + + var normal = await normalQuery + .OrderByDescending(m => m.SortOrder) + .Take(NormalCount) + .ToListAsync(); + + // 混排 + var mixed = MixMessages(featured, normal); + + // 查询当前用户点赞状态 + var messageIds = mixed.Select(m => m.Id).ToList(); + var likedIds = await GetLikedMessageIds(userId, messageIds); + + var output = mixed.Select(m => new WxFeedMessageOutput + { + Id = m.Id, + JournalId = m.JournalId, + UserId = m.UserId, + Content = m.Content, + ImageUrl = m.ImageUrl, + Type = m.Type, + LikeCount = m.LikeCount, + IsFeatured = m.IsFeatured, + IsLiked = likedIds.Contains(m.Id), + CreatedAt = m.CreatedAt + }).ToList(); + + var nextCursor = mixed.Any() ? mixed.Min(m => m.Id) : (long?)null; + var hasMore = featured.Count >= FeaturedCount || normal.Count >= NormalCount; + + return new WxFeedOutput + { + Messages = output, + NextCursor = hasMore ? nextCursor : null, + HasMore = hasMore + }; + } + + /// + /// 下拉刷新(重置游标,返回最新列表) + /// + public async Task RefreshFeedAsync(long userId) + { + logger.LogInformation("正在刷新社区Feed,用户ID: {UserId}", userId); + return await GetFeedAsync(userId, null); + } + + /// + /// 点赞 + /// + public async Task LikeAsync(long userId, WxLikeInput input) + { + logger.LogInformation("用户正在点赞,用户ID: {UserId}, 消息ID: {MessageId}", userId, input.MessageId); + + if (input.MessageId <= 0) + { + throw new BusinessException("消息ID无效", 400); + } + + var message = await messageRepository.GetByIdAsync(input.MessageId); + if (message == null) + { + throw new BusinessException("消息不存在", 404); + } + + // 检查是否已点赞 + var existingLike = await Context.Queryable() + .Where(l => l.UserId == userId && l.MessageId == input.MessageId && l.Type == "Like") + .FirstAsync(); + + if (existingLike != null) + { + throw new BusinessException("您已点赞过该消息", 400); + } + + // 插入点赞记录 + var like = new CommunityMessageLike + { + UserId = userId, + MessageId = input.MessageId, + Type = "Like", + CreatedBy = "System", + UpdatedBy = "System", + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now, + IsDeleted = false + }; + + var insertResult = await Context.Insertable(like).ExecuteCommandAsync(); + if (insertResult <= 0) + { + throw new BusinessException("点赞失败", 500); + } + + // 更新点赞数 + await Context.Updateable() + .SetColumns(m => new CommunityMessage + { + LikeCount = m.LikeCount + 1, + UpdatedAt = DateTime.Now + }) + .Where(m => m.Id == input.MessageId) + .ExecuteCommandAsync(); + + logger.LogInformation("点赞成功,用户ID: {UserId}, 消息ID: {MessageId}", userId, input.MessageId); + + return new WxLikeOutput + { + MessageId = input.MessageId, + LikeCount = message.LikeCount + 1, + IsLiked = true + }; + } + + /// + /// 取消点赞 + /// + public async Task UnlikeAsync(long userId, long messageId) + { + logger.LogInformation("用户正在取消点赞,用户ID: {UserId}, 消息ID: {MessageId}", userId, messageId); + + if (messageId <= 0) + { + throw new BusinessException("消息ID无效", 400); + } + + var message = await messageRepository.GetByIdAsync(messageId); + if (message == null) + { + throw new BusinessException("消息不存在", 404); + } + + var existingLike = await Context.Queryable() + .Where(l => l.UserId == userId && l.MessageId == messageId && l.Type == "Like") + .FirstAsync(); + + if (existingLike == null) + { + throw new BusinessException("您尚未点赞过该消息", 400); + } + + // 软删除点赞记录 + await Context.Updateable() + .SetColumns(l => new CommunityMessageLike { IsDeleted = true, UpdatedAt = DateTime.Now }) + .Where(l => l.Id == existingLike.Id) + .ExecuteCommandAsync(); + + // 更新点赞数(不小于0) + var newLikeCount = Math.Max(0, message.LikeCount - 1); + await Context.Updateable() + .SetColumns(m => new CommunityMessage + { + LikeCount = newLikeCount, + UpdatedAt = DateTime.Now + }) + .Where(m => m.Id == messageId) + .ExecuteCommandAsync(); + + logger.LogInformation("取消点赞成功,用户ID: {UserId}, 消息ID: {MessageId}", userId, messageId); + + return new WxLikeOutput + { + MessageId = messageId, + LikeCount = newLikeCount, + IsLiked = false + }; + } + + #region 私有辅助方法 + + /// + /// 获取用户拥有的期刊ID列表 + /// + private async Task> GetUserJournalIds(long userId) + { + return await Context.Queryable() + .Where(uj => uj.UserId == userId && uj.Status == "Active") + .Select(uj => uj.JournalId) + .ToListAsync(); + } + + /// + /// 获取当前用户已点赞的消息ID集合 + /// + private async Task> GetLikedMessageIds(long userId, List messageIds) + { + if (!messageIds.Any()) return new HashSet(); + + var likes = await Context.Queryable() + .Where(l => l.UserId == userId && messageIds.Contains(l.MessageId) && l.Type == "Like" && !l.IsDeleted) + .Select(l => l.MessageId) + .ToListAsync(); + + return likes.ToHashSet(); + } + + /// + /// 混排精选消息和普通消息(按权重随机插入) + /// + private static List MixMessages(List featured, List normal) + { + var result = new List(); + var allMessages = new List<(CommunityMessage msg, bool isFeatured)>(); + + allMessages.AddRange(featured.Select(m => (m, true))); + allMessages.AddRange(normal.Select(m => (m, false))); + + // 按SortOrder加权随机排序:SortOrder越大出现越靠前 + var random = new Random(); + while (allMessages.Any()) + { + var totalWeight = allMessages.Sum(x => Math.Max(1, x.msg.SortOrder)); + var pick = random.Next(0, totalWeight + 1); + var cumulative = 0; + var selectedIndex = 0; + + for (var i = 0; i < allMessages.Count; i++) + { + cumulative += Math.Max(1, allMessages[i].msg.SortOrder); + if (pick <= cumulative) + { + selectedIndex = i; + break; + } + } + + result.Add(allMessages[selectedIndex].msg); + allMessages.RemoveAt(selectedIndex); + } + + return result; + } + + #endregion +} diff --git a/QYZH.InteractiveMagazine.Service/WxMallService.cs b/QYZH.InteractiveMagazine.Service/WxMallService.cs index 8ace22b..e7db82c 100644 --- a/QYZH.InteractiveMagazine.Service/WxMallService.cs +++ b/QYZH.InteractiveMagazine.Service/WxMallService.cs @@ -5,6 +5,7 @@ using QYZH.InteractiveMagazine.Models.Dto.Bag; using QYZH.InteractiveMagazine.Models.Dto.Mall; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Repository; +using System.Text.Json; namespace QYZH.InteractiveMagazine.Service; @@ -17,6 +18,24 @@ public class WxMallService( ILogger logger) : BaseRepository, IWxMallService { + /// + /// 从 MetaData JSON 中提取 SkinId + /// + private static long GetSkinIdFromMetaData(string? metaData) + { + if (string.IsNullOrWhiteSpace(metaData)) return 0; + try + { + using var doc = JsonDocument.Parse(metaData); + if (doc.RootElement.TryGetProperty("SkinId", out var skinIdElement)) + return skinIdElement.GetInt64(); + if (doc.RootElement.TryGetProperty("skinId", out var skinIdLower)) + return skinIdLower.GetInt64(); + } + catch { } + return 0; + } + /// /// 获取商城商品列表(仅上架 + 在售) /// @@ -29,15 +48,36 @@ public class WxMallService( var products = await query.ToListAsync(); - // 批量查询皮肤信息 - var skinProductIds = products.Where(p => p.Type == "PetBg").Select(p => p.Id).ToList(); - var skins = skinProductIds.Count > 0 + // 批量提取 PetBg 商品的 SkinId + var skinProductMap = products + .Where(p => p.Type == "PetBg") + .Select(p => new { ProductId = p.Id, SkinId = GetSkinIdFromMetaData(p.MetaData) }) + .Where(x => x.SkinId > 0) + .ToList(); + + var skinIds = skinProductMap.Select(x => x.SkinId).Distinct().ToList(); + + // 批量查询皮肤 + var skins = skinIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() - .Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted) + .Where(s => skinIds.Contains(s.Id) && !s.IsDeleted) .ToListAsync() : new List(); + // 批量查询皮肤预览图(取每个皮肤的第一张图) + var allPreviewImages = skinIds.Count > 0 + ? await exchangeRecordRepository.Context.Queryable() + .Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted) + .OrderBy(i => i.SortOrder) + .ToListAsync() + : new List(); + + var previewImageMap = allPreviewImages + .GroupBy(i => i.SkinId) + .ToDictionary(g => g.Key, g => g.First().ImageUrl); + // 批量查询用户背包(判断已拥有) + var skinProductIds = skinProductMap.Select(x => x.ProductId).ToList(); var bagItems = skinProductIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() .Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "Available" @@ -47,8 +87,10 @@ public class WxMallService( return products.Select(p => { - var skin = skins.FirstOrDefault(s => s.ProductId == p.Id); + var skinMapping = skinProductMap.FirstOrDefault(x => x.ProductId == p.Id); + var skin = skinMapping != null ? skins.FirstOrDefault(s => s.Id == skinMapping.SkinId) : null; var owned = bagItems.Any(b => b.ItemId == p.Id); + var previewImg = skin != null && previewImageMap.ContainsKey(skin.Id) ? previewImageMap[skin.Id] : null; return new WxProductOutput { @@ -63,9 +105,9 @@ public class WxMallService( { SkinId = skin.Id, SkinName = skin.Name, - SkinImage = skin.ImageUrl, Description = skin.Description, - Rarity = skin.Rarity + Rarity = skin.Rarity, + PreviewImage = previewImg } : null }; }).ToList(); @@ -85,20 +127,30 @@ public class WxMallService( PetSkinBrief? skinBrief = null; if (product.Type == "PetBg") { - var skin = await exchangeRecordRepository.Context.Queryable() - .Where(s => s.ProductId == product.Id && !s.IsDeleted) - .FirstAsync(); - - if (skin != null) + var skinId = GetSkinIdFromMetaData(product.MetaData); + if (skinId > 0) { - skinBrief = new PetSkinBrief + var skin = await exchangeRecordRepository.Context.Queryable() + .Where(s => s.Id == skinId && !s.IsDeleted) + .FirstAsync(); + + if (skin != null) { - SkinId = skin.Id, - SkinName = skin.Name, - SkinImage = skin.ImageUrl, - Description = skin.Description, - Rarity = skin.Rarity - }; + var previewImg = await exchangeRecordRepository.Context.Queryable() + .Where(i => i.SkinId == skinId && !i.IsDeleted) + .OrderBy(i => i.SortOrder) + .Select(i => i.ImageUrl) + .FirstAsync(); + + skinBrief = new PetSkinBrief + { + SkinId = skin.Id, + SkinName = skin.Name, + Description = skin.Description, + Rarity = skin.Rarity, + PreviewImage = previewImg + }; + } } } @@ -298,18 +350,40 @@ public class WxMallService( .Where(p => itemIds.Contains(p.Id) && !p.IsDeleted) .ToListAsync(); - // 批量查询关联皮肤(PetBg 类型) - var skinProductIds = bagItems.Where(b => b.ItemType == "PetBg").Select(b => b.ItemId).Distinct().ToList(); - var skins = skinProductIds.Count > 0 + // 批量提取 PetBg 背包物品的 SkinId(从 MetaData 解析) + var skinIdMap = bagItems + .Where(b => b.ItemType == "PetBg") + .Select(b => new { BagId = b.Id, SkinId = GetSkinIdFromMetaData(b.MetaData) }) + .Where(x => x.SkinId > 0) + .ToList(); + + var skinIds = skinIdMap.Select(x => x.SkinId).Distinct().ToList(); + + // 批量查询皮肤 + var skins = skinIds.Count > 0 ? await exchangeRecordRepository.Context.Queryable() - .Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted) + .Where(s => skinIds.Contains(s.Id) && !s.IsDeleted) .ToListAsync() : new List(); + // 批量查询皮肤预览图 + var allBagPreviewImages = skinIds.Count > 0 + ? await exchangeRecordRepository.Context.Queryable() + .Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted) + .OrderBy(i => i.SortOrder) + .ToListAsync() + : new List(); + + var bagPreviewImageMap = allBagPreviewImages + .GroupBy(i => i.SkinId) + .ToDictionary(g => g.Key, g => g.First().ImageUrl); + return bagItems.Select(b => { var product = products.FirstOrDefault(p => p.Id == b.ItemId); - var skin = skins.FirstOrDefault(s => s.ProductId == b.ItemId); + var skinMapping = skinIdMap.FirstOrDefault(x => x.BagId == b.Id); + var skin = skinMapping != null ? skins.FirstOrDefault(s => s.Id == skinMapping.SkinId) : null; + var previewImg = skin != null && bagPreviewImageMap.ContainsKey(skin.Id) ? bagPreviewImageMap[skin.Id] : null; return new UserBagOutput { @@ -329,8 +403,8 @@ public class WxMallService( { SkinId = skin.Id, SkinName = skin.Name, - SkinImage = skin.ImageUrl, - Rarity = skin.Rarity + Rarity = skin.Rarity, + PreviewImage = previewImg } : null }; }).ToList(); @@ -449,13 +523,15 @@ public class WxMallService( if (skin == null) throw new BusinessException("皮肤不存在", 404); - // 校验背包中是否拥有该皮肤(通过关联商品Id判断) + // 校验背包中是否拥有该皮肤(通过 MetaData 中的 SkinId 判断) var hasSkin = await exchangeRecordRepository.Context.Queryable() - .Where(b => b.UserId == userId && b.ItemId == skin.ProductId - && !b.IsDeleted && b.Status == "Available" && b.Quantity > 0) - .AnyAsync(); + .Where(b => b.UserId == userId && !b.IsDeleted && b.Status == "Available" && b.Quantity > 0 + && b.ItemType == "PetBg") + .ToListAsync(); - if (!hasSkin) + var owned = hasSkin.Any(b => GetSkinIdFromMetaData(b.MetaData) == input.SkinId); + + if (!owned) throw new BusinessException("您尚未拥有该皮肤,请先兑换", 400); // 换肤 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs new file mode 100644 index 0000000..a3fabc4 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs @@ -0,0 +1,163 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 社区消息管理控制器 +/// +[Route("api/[controller]")] +[ApiController] +[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] +public class CommunityMessageController : BaseController +{ + private readonly ICommunityMessageService _communityMessageService; + private readonly ILogger _logger; + + public CommunityMessageController(ICommunityMessageService communityMessageService, ILogger logger) + { + _communityMessageService = communityMessageService; + _logger = logger; + } + + /// + /// 分页查询社区消息列表 + /// + [HttpPost("list")] + public async Task>> GetListAsync([FromBody] AdminMessageQueryInput input) + { + try + { + var result = await _communityMessageService.GetListAsync(input); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "查询社区消息列表业务异常: {Message}", ex.Message); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "查询社区消息列表系统异常,参数:{Input}", input); + return BaseResponse>.Fail("查询社区消息列表失败,请稍后重试"); + } + } + + /// + /// 查看消息详情 + /// + [HttpGet("{id}")] + public async Task> GetDetailAsync(long id) + { + try + { + var result = await _communityMessageService.GetDetailAsync(id); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "获取社区消息详情业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取社区消息详情系统异常,ID:{Id}", id); + return BaseResponse.Fail("获取消息详情失败,请稍后重试"); + } + } + + /// + /// 删除消息 + /// + [HttpDelete("{id}")] + public async Task> DeleteAsync(long id) + { + try + { + await _communityMessageService.DeleteAsync(id); + return Success(new object(), "删除消息成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "删除社区消息业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "删除社区消息系统异常,ID:{Id}", id); + return BaseResponse.Fail("删除消息失败,请稍后重试"); + } + } + + /// + /// 冻结/解冻消息 + /// + [HttpPut("{id}/freeze")] + public async Task> FreezeAsync(long id, [FromBody] AdminFreezeInput input) + { + try + { + await _communityMessageService.FreezeAsync(id, input.Status); + return Success(new object(), input.Status == 2 ? "冻结消息成功" : "解冻消息成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "冻结社区消息业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "冻结社区消息系统异常,ID:{Id}", id); + return BaseResponse.Fail("操作失败,请稍后重试"); + } + } + + /// + /// 设置/取消精选 + /// + [HttpPut("{id}/featured")] + public async Task> SetFeaturedAsync(long id, [FromBody] AdminSetFeaturedInput input) + { + try + { + await _communityMessageService.SetFeaturedAsync(id, input.IsFeatured); + return Success(new object(), input.IsFeatured == 1 ? "设置精选成功" : "取消精选成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "设置社区消息精选业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "设置社区消息精选系统异常,ID:{Id}", id); + return BaseResponse.Fail("操作失败,请稍后重试"); + } + } + + /// + /// 设置排序权重 + /// + [HttpPut("{id}/sort")] + public async Task> SetSortOrderAsync(long id, [FromBody] AdminSetSortOrderInput input) + { + try + { + await _communityMessageService.SetSortOrderAsync(id, input.SortOrder); + return Success(new object(), "设置排序权重成功"); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "设置社区消息排序权重业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "设置社区消息排序权重系统异常,ID:{Id}", id); + return BaseResponse.Fail("操作失败,请稍后重试"); + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs new file mode 100644 index 0000000..7317e26 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs @@ -0,0 +1,129 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; + +/// +/// 小程序社区控制器 +/// +public class CommunityController(IWeChatCommunityService communityService, ILogger logger) : WeChatBaseController +{ + /// + /// 获取社区Feed流(翻页) + /// + /// 游标(上一页最后一条消息ID,首次不传) + [HttpGet("feed")] + public async Task> GetFeed([FromQuery] long? cursor = null) + { + try + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await communityService.GetFeedAsync(userId.Value, cursor); + return Success(result); + } + catch (BusinessException ex) + { + logger.LogWarning(ex, "获取社区Feed业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + logger.LogError(ex, "获取社区Feed系统异常"); + return BaseResponse.Fail("获取社区内容失败,请稍后重试"); + } + } + + /// + /// 下拉刷新社区Feed + /// + [HttpGet("refresh")] + public async Task> RefreshFeed() + { + try + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await communityService.RefreshFeedAsync(userId.Value); + return Success(result); + } + catch (BusinessException ex) + { + logger.LogWarning(ex, "刷新社区Feed业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + logger.LogError(ex, "刷新社区Feed系统异常"); + return BaseResponse.Fail("刷新社区内容失败,请稍后重试"); + } + } + + /// + /// 点赞 + /// + [HttpPost("like")] + public async Task> Like([FromBody] WxLikeInput input) + { + try + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await communityService.LikeAsync(userId.Value, input); + return Success(result, "点赞成功"); + } + catch (BusinessException ex) + { + logger.LogWarning(ex, "点赞业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + logger.LogError(ex, "点赞系统异常,参数:{Input}", input); + return BaseResponse.Fail("点赞失败,请稍后重试"); + } + } + + /// + /// 取消点赞 + /// + [HttpPost("unlike")] + public async Task> Unlike([FromBody] WxLikeInput input) + { + try + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await communityService.UnlikeAsync(userId.Value, input.MessageId); + return Success(result, "已取消点赞"); + } + catch (BusinessException ex) + { + logger.LogWarning(ex, "取消点赞业务异常: {Message}", ex.Message); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + logger.LogError(ex, "取消点赞系统异常,参数:{Input}", input); + return BaseResponse.Fail("取消点赞失败,请稍后重试"); + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatMedalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs similarity index 96% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatMedalController.cs rename to QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs index a7bc536..8ea66f6 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatMedalController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs @@ -8,7 +8,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; /// /// 小程序勋章控制器 /// -public class WeChatMedalController(IMedalService medalService, ILogger logger) : WeChatBaseController +public class MedalController(IMedalService medalService, ILogger logger) : WeChatBaseController { /// /// 获取所有勋章列表(含当前用户拥有状态) diff --git a/SqlMigrations/pet_module_optimization.sql b/SqlMigrations/pet_module_optimization.sql new file mode 100644 index 0000000..4424693 --- /dev/null +++ b/SqlMigrations/pet_module_optimization.sql @@ -0,0 +1,33 @@ +-- ======================================== +-- 宠物模块数据结构优化迁移脚本 +-- 执行前请备份相关表数据 +-- ======================================== + +-- 1. 修改 Pet 表:CurrentEvolutionId int → bigint +ALTER TABLE Pet MODIFY COLUMN CurrentEvolutionId BIGINT NOT NULL DEFAULT 0; + +-- 2. 修改 PetEvolution 表:PreviousEvolutionId int → bigint, 删除 ImageUrl +ALTER TABLE PetEvolution MODIFY COLUMN PreviousEvolutionId BIGINT NULL; +ALTER TABLE PetEvolution DROP COLUMN ImageUrl; + +-- 3. 修改 PetSkin 表:删除 ImageUrl 和 ProductId +ALTER TABLE PetSkin DROP COLUMN ImageUrl; +ALTER TABLE PetSkin DROP COLUMN ProductId; + +-- 4. 新建 PetSkinImage 表(每个皮肤在每个进化阶段下有一组有序图片) +CREATE TABLE IF NOT EXISTS PetSkinImage ( + Id BIGINT NOT NULL PRIMARY KEY COMMENT '主键(雪花ID)', + SkinId BIGINT NOT NULL COMMENT '皮肤Id,关联PetSkin.Id(0表示默认皮肤)', + EvolutionStageId BIGINT NOT NULL COMMENT '进化阶段Id,关联PetEvolution.Id', + ImageUrl VARCHAR(500) NOT NULL COMMENT '图片地址', + SortOrder INT NOT NULL DEFAULT 0 COMMENT '图片顺序(动画帧序号)', + Type VARCHAR(50) NOT NULL DEFAULT 'Normal' COMMENT '图片类型: Normal, Special', + Status INT NOT NULL DEFAULT 0 COMMENT '基础状态', + IsDeleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删除', + CreatedBy VARCHAR(100) NULL COMMENT '创建人', + CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UpdatedBy VARCHAR(100) NULL COMMENT '更新人', + UpdatedAt DATETIME NULL COMMENT '更新时间', + INDEX idx_skin_evolution (SkinId, EvolutionStageId), + INDEX idx_sort (SkinId, EvolutionStageId, SortOrder) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物皮肤图片表';