feat: 完成宠物模块重构与社区功能开发

本次提交包含多项核心更新:
1.  重构宠物模块数据结构:拆分皮肤图片为独立表,优化Pet、PetEvolution实体,调整字段类型与冗余字段
2.  新增宠物皮肤图片管理表,支持多进化阶段多图片展示
3.  完善宠物DTO,新增当前形态名称、皮肤信息与图片序列返回
4.  新增社区功能模块:
    - 微信端社区Feed流、点赞/取消点赞接口
    - 后台社区消息管理接口与服务实现
5.  优化商城与背包模块,替换皮肤图片获取逻辑为从新表读取预览图
6.  重构勋章服务,新增规则校验逻辑
7.  调整命名规范,修复原有控制器命名问题
This commit is contained in:
glz
2026-06-05 17:15:30 +08:00
parent eab84747a5
commit 8291b82362
19 changed files with 1753 additions and 182 deletions

View File

@ -0,0 +1,44 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 社区消息后台管理服务接口
/// </summary>
public interface ICommunityMessageService : IBaseService<CommunityMessage>
{
/// <summary>
/// 分页查询消息列表
/// </summary>
Task<PageListModel<AdminMessageDetailOutput>> GetListAsync(AdminMessageQueryInput input);
/// <summary>
/// 查看消息详情
/// </summary>
Task<AdminMessageDetailOutput> GetDetailAsync(long id);
/// <summary>
/// 软删除消息
/// </summary>
Task DeleteAsync(long id);
/// <summary>
/// 冻结/解冻消息
/// </summary>
/// <param name="id">消息ID</param>
/// <param name="status">状态: 1=解冻(通过), 2=冻结</param>
Task FreezeAsync(long id, int status);
/// <summary>
/// 设置/取消精选
/// </summary>
/// <param name="id">消息ID</param>
/// <param name="isFeatured">0=取消, 1=精选</param>
Task SetFeaturedAsync(long id, int isFeatured);
/// <summary>
/// 设置排序权重
/// </summary>
Task SetSortOrderAsync(long id, int sortOrder);
}

View File

@ -0,0 +1,31 @@
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 社区微信端服务接口
/// </summary>
public interface IWeChatCommunityService
{
/// <summary>
/// 获取社区Feed流翻页
/// </summary>
/// <param name="userId">当前用户ID</param>
/// <param name="cursor">游标上一页最后一条消息的ID首次传null</param>
Task<WxFeedOutput> GetFeedAsync(long userId, long? cursor);
/// <summary>
/// 下拉刷新(返回最新列表)
/// </summary>
Task<WxFeedOutput> RefreshFeedAsync(long userId);
/// <summary>
/// 点赞
/// </summary>
Task<WxLikeOutput> LikeAsync(long userId, WxLikeInput input);
/// <summary>
/// 取消点赞
/// </summary>
Task<WxLikeOutput> UnlikeAsync(long userId, long messageId);
}

View File

@ -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;
/// <summary>
/// 皮肤预览图(取第一张图片)
/// </summary>
public string? PreviewImage { get; set; }
}
/// <summary>

View File

@ -0,0 +1,288 @@
namespace QYZH.InteractiveMagazine.Models.Dto;
#region DTO
/// <summary>
/// 社区消息分页查询输入(后台)
/// </summary>
public class AdminMessageQueryInput : PageQueryModel
{
/// <summary>
/// 期刊ID
/// </summary>
public long? JournalId { get; set; }
/// <summary>
/// 消息类型: Article, Quote, Announcement
/// </summary>
public string? Type { get; set; }
/// <summary>
/// 状态: 0=审核中, 1=已通过, 2=已冻结
/// </summary>
public int? Status { get; set; }
/// <summary>
/// 是否精选: 0=否, 1=是
/// </summary>
public int? IsFeatured { get; set; }
/// <summary>
/// 是否启用
/// </summary>
public bool? IsActive { get; set; }
/// <summary>
/// 内容关键词(模糊查询)
/// </summary>
public string? Keyword { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public long? UserId { get; set; }
}
/// <summary>
/// 社区消息详情输出(后台)
/// </summary>
public class AdminMessageDetailOutput
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 期刊ID
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 用户实例期刊ID
/// </summary>
public long UserJournalId { get; set; }
/// <summary>
/// 期刊任务ID
/// </summary>
public long JournalTaskId { get; set; }
/// <summary>
/// 用户期刊任务回答ID
/// </summary>
public long JournalTaskAnswerId { get; set; }
/// <summary>
/// 消息内容
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// 配图
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 排序权重
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 是否启用
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// 消息类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 点赞数
/// </summary>
public int LikeCount { get; set; }
/// <summary>
/// 是否精选
/// </summary>
public int IsFeatured { get; set; }
/// <summary>
/// 状态: 0=审核中, 1=已通过, 2=已冻结
/// </summary>
public int Status { get; set; }
/// <summary>
/// 创建人
/// </summary>
public string? CreatedBy { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新人
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// 设置精选入参
/// </summary>
public class AdminSetFeaturedInput
{
/// <summary>
/// 是否精选: 0=否, 1=是
/// </summary>
public int IsFeatured { get; set; }
}
/// <summary>
/// 设置排序权重入参
/// </summary>
public class AdminSetSortOrderInput
{
/// <summary>
/// 排序权重
/// </summary>
public int SortOrder { get; set; }
}
/// <summary>
/// 冻结/解冻入参
/// </summary>
public class AdminFreezeInput
{
/// <summary>
/// 状态: 1=解冻(通过), 2=冻结
/// </summary>
public int Status { get; set; }
}
#endregion
#region DTO
/// <summary>
/// 微信端社区Feed消息输出
/// </summary>
public class WxFeedMessageOutput
{
/// <summary>
/// 消息ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 期刊ID
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 作者用户ID
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 消息内容
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// 配图
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 消息类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 点赞数
/// </summary>
public int LikeCount { get; set; }
/// <summary>
/// 是否精选
/// </summary>
public int IsFeatured { get; set; }
/// <summary>
/// 当前用户是否已点赞
/// </summary>
public bool IsLiked { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// 微信端社区Feed输出含游标
/// </summary>
public class WxFeedOutput
{
/// <summary>
/// 消息列表
/// </summary>
public List<WxFeedMessageOutput> Messages { get; set; } = new();
/// <summary>
/// 下一页游标为null表示没有更多数据
/// </summary>
public long? NextCursor { get; set; }
/// <summary>
/// 是否还有更多数据
/// </summary>
public bool HasMore { get; set; }
}
/// <summary>
/// 微信端点赞入参
/// </summary>
public class WxLikeInput
{
/// <summary>
/// 消息ID
/// </summary>
public long MessageId { get; set; }
}
/// <summary>
/// 微信端点赞输出
/// </summary>
public class WxLikeOutput
{
/// <summary>
/// 消息ID
/// </summary>
public long MessageId { get; set; }
/// <summary>
/// 当前点赞数
/// </summary>
public int LikeCount { get; set; }
/// <summary>
/// 是否已点赞
/// </summary>
public bool IsLiked { get; set; }
}
#endregion

View File

@ -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;
/// <summary>
/// 皮肤预览图(取当前进化阶段第一张图片)
/// </summary>
public string? PreviewImage { get; set; }
}
/// <summary>

View File

@ -23,7 +23,12 @@ public class PetOutput
/// <summary>
/// 当前进化形态Id
/// </summary>
public int CurrentEvolutionId { get; set; }
public long CurrentEvolutionId { get; set; }
/// <summary>
/// 当前进化阶段名称
/// </summary>
public string? EvolutionStageName { get; set; }
/// <summary>
/// 当前成长值
@ -35,6 +40,21 @@ public class PetOutput
/// </summary>
public int FeedingCount { get; set; }
/// <summary>
/// 当前皮肤Id
/// </summary>
public long CurrentSkinId { get; set; }
/// <summary>
/// 当前皮肤名称
/// </summary>
public string? CurrentSkinName { get; set; }
/// <summary>
/// 当前形态+当前皮肤 的图片列表按SortOrder排序
/// </summary>
public List<SkinImageOutput> CurrentImages { get; set; } = new();
/// <summary>
/// 宠物类型: Normal
/// </summary>
@ -51,6 +71,27 @@ public class PetOutput
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// 皮肤图片输出
/// </summary>
public class SkinImageOutput
{
/// <summary>
/// 图片Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 图片地址
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// 图片顺序
/// </summary>
public int SortOrder { get; set; }
}
/// <summary>
/// 喂养宠物输入
/// </summary>

View File

@ -8,64 +8,85 @@ namespace QYZH.InteractiveMagazine.Models.Entity
[SugarTable("Pet")]
public partial class Pet : SqlSugarBaseEntity
{
public Pet(){
public Pet()
{
}
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
public long UserId { get; set; }
/// <summary>
/// Desc:宠物昵称
/// Default:
/// Nullable:True
/// </summary>
public string Name {get;set;}
/// <summary>
/// Desc:宠物昵称
/// Default:
/// Nullable:True
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:当前进化形态Id
/// Default:
/// Nullable:False
/// </summary>
public int CurrentEvolutionId {get;set;}
/// <summary>
/// Desc:当前进化形态Id
/// Default:
/// Nullable:False
/// </summary>
public long CurrentEvolutionId { get; set; }
/// <summary>
/// Desc:当前成长值
/// Default:0
/// Nullable:False
/// </summary>
public int GrowthPoints {get;set;}
/// <summary>
/// Desc:当前成长值
/// Default:0
/// Nullable:False
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// Desc:累计喂养次数
/// Default:0
/// Nullable:False
/// </summary>
public int FeedingCount {get;set;}
/// <summary>
/// Desc:累计喂养次数
/// Default:0
/// Nullable:False
/// </summary>
public int FeedingCount { get; set; }
/// <summary>
/// Desc:当前皮肤Id0为默认皮肤
/// Default:0
/// Nullable:False
/// </summary>
public long CurrentSkinId {get;set;}
/// <summary>
/// Desc:当前皮肤Id0为默认皮肤
/// Default:0
/// Nullable:False
/// </summary>
public long CurrentSkinId { get; set; }
/// <summary>
/// Desc:宠物类型
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:宠物类型
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type { get; set; }
/// <summary>
/// Desc:状态: Inactive, Active, Sleeping
/// Default:Inactive
/// Nullable:False
/// </summary>
public string Status {get;set;}
/// <summary>
/// Desc:状态: Inactive, Active, Sleeping
/// Default:Inactive
/// Nullable:False
/// </summary>
public new string Status { get; set; }
/// <summary>
/// 理解力
/// </summary>
public int Comprehension { get; set; }
/// <summary>
/// 判断力
/// </summary>
public int Judgment { get; set; }
/// <summary>
/// 表达力
/// </summary>
public int Expression { get; set; }
/// <summary>
/// 说服力
/// </summary>
public int Persuasiveness { get; set; }
}
}

View File

@ -8,86 +8,76 @@ namespace QYZH.InteractiveMagazine.Models.Entity
[SugarTable("PetEvolution")]
public partial class PetEvolution : SqlSugarBaseEntity
{
public PetEvolution(){
public PetEvolution() { }
/// <summary>
/// Desc:阶段名称
/// Default:
/// Nullable:False
/// </summary>
public string StageName { get; set; }
}
/// <summary>
/// Desc:阶段等级
/// Default:
/// Nullable:False
/// </summary>
public int StageLevel { get; set; }
/// <summary>
/// Desc:阶段名称
/// Default:
/// Nullable:False
/// </summary>
public string StageName {get;set;}
/// <summary>
/// Desc:进化所需成长值
/// Default:
/// Nullable:False
/// </summary>
public int RequiredGrowth { get; set; }
/// <summary>
/// Desc:阶段等级
/// Default:
/// Nullable:False
/// </summary>
public int StageLevel {get;set;}
/// <summary>
/// Desc:前一形态Idnull表示初始形态
/// Default:
/// Nullable:True
/// </summary>
public long? PreviousEvolutionId { get; set; }
/// <summary>
/// Desc:进化所需成长值
/// Default:
/// Nullable:False
/// </summary>
public int RequiredGrowth {get;set;}
/// <summary>
/// Desc:基础力量
/// Default:0
/// Nullable:False
/// </summary>
public int BaseStrength { get; set; }
/// <summary>
/// Desc:形态图片
/// Default:
/// Nullable:True
/// </summary>
public string ImageUrl {get;set;}
/// <summary>
/// Desc:基础敏捷
/// Default:0
/// Nullable:False
/// </summary>
public int BaseAgility { get; set; }
/// <summary>
/// Desc:前一形态Id
/// Default:
/// Nullable:True
/// </summary>
public int? PreviousEvolutionId {get;set;}
/// <summary>
/// Desc:基础智力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseIntelligence { get; set; }
/// <summary>
/// Desc:基础力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseStrength {get;set;}
/// <summary>
/// Desc:基础
/// Default:0
/// Nullable:False
/// </summary>
public int BaseCharm { get; set; }
/// <summary>
/// Desc:基础敏捷
/// Default:0
/// Nullable:False
/// </summary>
public int BaseAgility {get;set;}
/// <summary>
/// Desc:进化类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type { get; set; }
/// <summary>
/// Desc:基础智力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseIntelligence {get;set;}
/// <summary>
/// Desc:基础魅力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseCharm {get;set;}
/// <summary>
/// Desc:进化类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public new string Status { get; set; }
}
}

View File

@ -17,13 +17,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:皮肤图片
/// Default:
/// Nullable:True
/// </summary>
public string ImageUrl { get; set; }
/// <summary>
/// Desc:描述/特效说明
/// Default:
@ -31,13 +24,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public string Description { get; set; }
/// <summary>
/// Desc:关联商品Id用于兑换
/// Default:
/// Nullable:False
/// </summary>
public long ProductId { get; set; }
/// <summary>
/// Desc:稀有度: Normal, Rare, Epic, Legendary
/// Default:Normal
@ -51,5 +37,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Nullable:False
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// Desc:皮肤类型: Normal, Limited, Event
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type { get; set; }
}
}

View File

@ -0,0 +1,48 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
/// <summary>
/// 宠物皮肤图片表(每个皮肤在每个进化阶段下有一组有序图片,用于动态表示)
/// </summary>
[SugarTable("PetSkinImage")]
public partial class PetSkinImage : SqlSugarBaseEntity
{
public PetSkinImage() { }
/// <summary>
/// Desc:皮肤Id关联PetSkin.Id0表示默认皮肤的图片
/// Default:
/// Nullable:False
/// </summary>
public long SkinId { get; set; }
/// <summary>
/// Desc:进化阶段Id关联PetEvolution.Id
/// Default:
/// Nullable:False
/// </summary>
public long EvolutionStageId { get; set; }
/// <summary>
/// Desc:图片地址
/// Default:
/// Nullable:False
/// </summary>
public string ImageUrl { get; set; }
/// <summary>
/// Desc:图片顺序(动画帧序号)
/// Default:0
/// Nullable:False
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// Desc:图片类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type { get; set; }
}
}

View File

@ -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;
/// <summary>
/// 社区消息后台管理服务实现
/// </summary>
public class CommunityMessageService(BaseRepository<CommunityMessage> messageRepository, ILogger<CommunityMessageService> logger)
: BaseRepository<CommunityMessage>, ICommunityMessageService
{
/// <summary>
/// 分页查询消息列表
/// </summary>
public async Task<PageListModel<AdminMessageDetailOutput>> 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<int> 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<AdminMessageDetailOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 查看消息详情
/// </summary>
public async Task<AdminMessageDetailOutput> 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
};
}
/// <summary>
/// 软删除消息
/// </summary>
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);
}
/// <summary>
/// 冻结/解冻消息
/// </summary>
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);
}
/// <summary>
/// 设置/取消精选
/// </summary>
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);
}
/// <summary>
/// 设置排序权重
/// </summary>
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);
}
}

View File

@ -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<Medal> medalRepository, ILogger<MedalSe
throw new BusinessException("该勋章当前不可获得", 400);
}
// 规则校验
var (passed, failReason) = await ValidateMedalRulesAsync(medal.Id, userId);
if (!passed)
{
logger.LogWarning("勋章规则校验未通过用户ID: {UserId}, 勋章ID: {MedalId}, 原因: {Reason}", userId, input.MedalId, failReason);
throw new BusinessException(failReason!, 400);
}
var existingUserMedal = await Context.Queryable<UserMedal>()
.Where(um => um.UserId == userId && um.MedalId == (int)input.MedalId && um.Status == "Awarded")
.FirstAsync();
@ -498,5 +507,142 @@ public class MedalService(BaseRepository<Medal> medalRepository, ILogger<MedalSe
};
}
/// <summary>
/// 校验勋章规则是否满足
/// </summary>
/// <param name="medalId">勋章ID</param>
/// <param name="userId">用户ID</param>
/// <returns>(是否通过, 失败原因)</returns>
private async Task<(bool passed, string? failReason)> ValidateMedalRulesAsync(long medalId, long userId)
{
var rules = await Context.Queryable<MedalRule>()
.Where(r => r.MedalId == medalId && !r.IsDeleted)
.OrderBy(r => r.RuleOrder)
.ToListAsync();
if (!rules.Any())
{
return (true, null);
}
var failReasons = new List<string>();
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)}");
}
}
/// <summary>
/// 评估单条规则是否满足
/// </summary>
/// <param name="rule">规则配置</param>
/// <param name="userId">用户ID</param>
/// <returns>规则是否满足</returns>
private async Task<bool> 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<SugarParameter>
{
new SugarParameter("@ThresholdValue", rule.ThresholdValue),
new SugarParameter("@UserId", userId)
};
// 处理额外筛选条件
if (!string.IsNullOrWhiteSpace(rule.FilterCondition))
{
var filters = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(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;
}
}
/// <summary>
/// 根据规则的时间范围类型计算起止时间
/// </summary>
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
}

View File

@ -16,11 +16,13 @@ public class PetService(
BaseRepository<Pet> petRepository,
BaseRepository<PetFeedingRecord> feedingRecordRepository,
BaseRepository<PetEvolution> petEvolutionRepository,
BaseRepository<PetSkinImage> petSkinImageRepository,
BaseRepository<PetSkin> petSkinRepository,
ILogger<PetService> logger)
: BaseRepository<Pet>, IPetService
{
/// <summary>
/// 获取用户宠物信息
/// 获取用户宠物信息(含当前形态名称、皮肤图片序列)
/// </summary>
public async Task<PetOutput?> 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
};
}
/// <summary>
@ -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);
}
/// <summary>
@ -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

View File

@ -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;
/// <summary>
/// 社区微信端服务实现
/// </summary>
public class WeChatCommunityService(
BaseRepository<CommunityMessage> messageRepository,
ILogger<WeChatCommunityService> logger)
: BaseRepository<CommunityMessage>, IWeChatCommunityService
{
private const int FeaturedCount = 5;
private const int NormalCount = 5;
/// <summary>
/// 获取社区Feed流翻页
/// </summary>
public async Task<WxFeedOutput> 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<WxFeedMessageOutput>(), 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
};
}
/// <summary>
/// 下拉刷新(重置游标,返回最新列表)
/// </summary>
public async Task<WxFeedOutput> RefreshFeedAsync(long userId)
{
logger.LogInformation("正在刷新社区Feed用户ID: {UserId}", userId);
return await GetFeedAsync(userId, null);
}
/// <summary>
/// 点赞
/// </summary>
public async Task<WxLikeOutput> 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<CommunityMessageLike>()
.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<CommunityMessage>()
.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
};
}
/// <summary>
/// 取消点赞
/// </summary>
public async Task<WxLikeOutput> 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<CommunityMessageLike>()
.Where(l => l.UserId == userId && l.MessageId == messageId && l.Type == "Like")
.FirstAsync();
if (existingLike == null)
{
throw new BusinessException("您尚未点赞过该消息", 400);
}
// 软删除点赞记录
await Context.Updateable<CommunityMessageLike>()
.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<CommunityMessage>()
.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
/// <summary>
/// 获取用户拥有的期刊ID列表
/// </summary>
private async Task<List<long>> GetUserJournalIds(long userId)
{
return await Context.Queryable<UserJournal>()
.Where(uj => uj.UserId == userId && uj.Status == "Active")
.Select(uj => uj.JournalId)
.ToListAsync();
}
/// <summary>
/// 获取当前用户已点赞的消息ID集合
/// </summary>
private async Task<HashSet<long>> GetLikedMessageIds(long userId, List<long> messageIds)
{
if (!messageIds.Any()) return new HashSet<long>();
var likes = await Context.Queryable<CommunityMessageLike>()
.Where(l => l.UserId == userId && messageIds.Contains(l.MessageId) && l.Type == "Like" && !l.IsDeleted)
.Select(l => l.MessageId)
.ToListAsync();
return likes.ToHashSet();
}
/// <summary>
/// 混排精选消息和普通消息(按权重随机插入)
/// </summary>
private static List<CommunityMessage> MixMessages(List<CommunityMessage> featured, List<CommunityMessage> normal)
{
var result = new List<CommunityMessage>();
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
}

View File

@ -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<WxMallService> logger)
: BaseRepository<ExchangeRecord>, IWxMallService
{
/// <summary>
/// 从 MetaData JSON 中提取 SkinId
/// </summary>
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;
}
/// <summary>
/// 获取商城商品列表(仅上架 + 在售)
/// </summary>
@ -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<PetSkin>()
.Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted)
.Where(s => skinIds.Contains(s.Id) && !s.IsDeleted)
.ToListAsync()
: new List<PetSkin>();
// 批量查询皮肤预览图(取每个皮肤的第一张图)
var allPreviewImages = skinIds.Count > 0
? await exchangeRecordRepository.Context.Queryable<PetSkinImage>()
.Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted)
.OrderBy(i => i.SortOrder)
.ToListAsync()
: new List<PetSkinImage>();
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<UserBag>()
.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<PetSkin>()
.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<PetSkin>()
.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<PetSkinImage>()
.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<PetSkin>()
.Where(s => skinProductIds.Contains(s.ProductId) && !s.IsDeleted)
.Where(s => skinIds.Contains(s.Id) && !s.IsDeleted)
.ToListAsync()
: new List<PetSkin>();
// 批量查询皮肤预览图
var allBagPreviewImages = skinIds.Count > 0
? await exchangeRecordRepository.Context.Queryable<PetSkinImage>()
.Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted)
.OrderBy(i => i.SortOrder)
.ToListAsync()
: new List<PetSkinImage>();
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<UserBag>()
.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);
// 换肤

View File

@ -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;
/// <summary>
/// 社区消息管理控制器
/// </summary>
[Route("api/[controller]")]
[ApiController]
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
public class CommunityMessageController : BaseController
{
private readonly ICommunityMessageService _communityMessageService;
private readonly ILogger<CommunityMessageController> _logger;
public CommunityMessageController(ICommunityMessageService communityMessageService, ILogger<CommunityMessageController> logger)
{
_communityMessageService = communityMessageService;
_logger = logger;
}
/// <summary>
/// 分页查询社区消息列表
/// </summary>
[HttpPost("list")]
public async Task<BaseResponse<PageListModel<AdminMessageDetailOutput>>> 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<PageListModel<AdminMessageDetailOutput>>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "查询社区消息列表系统异常,参数:{Input}", input);
return BaseResponse<PageListModel<AdminMessageDetailOutput>>.Fail("查询社区消息列表失败,请稍后重试");
}
}
/// <summary>
/// 查看消息详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<AdminMessageDetailOutput>> GetDetailAsync(long id)
{
try
{
var result = await _communityMessageService.GetDetailAsync(id);
return Success(result);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "获取社区消息详情业务异常: {Message}", ex.Message);
return BaseResponse<AdminMessageDetailOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取社区消息详情系统异常ID{Id}", id);
return BaseResponse<AdminMessageDetailOutput>.Fail("获取消息详情失败,请稍后重试");
}
}
/// <summary>
/// 删除消息
/// </summary>
[HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id)
{
try
{
await _communityMessageService.DeleteAsync(id);
return Success(new object(), "删除消息成功");
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "删除社区消息业务异常: {Message}", ex.Message);
return BaseResponse<object>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "删除社区消息系统异常ID{Id}", id);
return BaseResponse<object>.Fail("删除消息失败,请稍后重试");
}
}
/// <summary>
/// 冻结/解冻消息
/// </summary>
[HttpPut("{id}/freeze")]
public async Task<BaseResponse<object>> 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<object>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "冻结社区消息系统异常ID{Id}", id);
return BaseResponse<object>.Fail("操作失败,请稍后重试");
}
}
/// <summary>
/// 设置/取消精选
/// </summary>
[HttpPut("{id}/featured")]
public async Task<BaseResponse<object>> 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<object>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "设置社区消息精选系统异常ID{Id}", id);
return BaseResponse<object>.Fail("操作失败,请稍后重试");
}
}
/// <summary>
/// 设置排序权重
/// </summary>
[HttpPut("{id}/sort")]
public async Task<BaseResponse<object>> 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<object>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "设置社区消息排序权重系统异常ID{Id}", id);
return BaseResponse<object>.Fail("操作失败,请稍后重试");
}
}
}

View File

@ -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;
/// <summary>
/// 小程序社区控制器
/// </summary>
public class CommunityController(IWeChatCommunityService communityService, ILogger<CommunityController> logger) : WeChatBaseController
{
/// <summary>
/// 获取社区Feed流翻页
/// </summary>
/// <param name="cursor">游标上一页最后一条消息ID首次不传</param>
[HttpGet("feed")]
public async Task<BaseResponse<WxFeedOutput>> GetFeed([FromQuery] long? cursor = null)
{
try
{
var userId = GetCurrentUserId();
if (userId == null)
{
return BaseResponse<WxFeedOutput>.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<WxFeedOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
logger.LogError(ex, "获取社区Feed系统异常");
return BaseResponse<WxFeedOutput>.Fail("获取社区内容失败,请稍后重试");
}
}
/// <summary>
/// 下拉刷新社区Feed
/// </summary>
[HttpGet("refresh")]
public async Task<BaseResponse<WxFeedOutput>> RefreshFeed()
{
try
{
var userId = GetCurrentUserId();
if (userId == null)
{
return BaseResponse<WxFeedOutput>.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<WxFeedOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
logger.LogError(ex, "刷新社区Feed系统异常");
return BaseResponse<WxFeedOutput>.Fail("刷新社区内容失败,请稍后重试");
}
}
/// <summary>
/// 点赞
/// </summary>
[HttpPost("like")]
public async Task<BaseResponse<WxLikeOutput>> Like([FromBody] WxLikeInput input)
{
try
{
var userId = GetCurrentUserId();
if (userId == null)
{
return BaseResponse<WxLikeOutput>.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<WxLikeOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
logger.LogError(ex, "点赞系统异常,参数:{Input}", input);
return BaseResponse<WxLikeOutput>.Fail("点赞失败,请稍后重试");
}
}
/// <summary>
/// 取消点赞
/// </summary>
[HttpPost("unlike")]
public async Task<BaseResponse<WxLikeOutput>> Unlike([FromBody] WxLikeInput input)
{
try
{
var userId = GetCurrentUserId();
if (userId == null)
{
return BaseResponse<WxLikeOutput>.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<WxLikeOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
logger.LogError(ex, "取消点赞系统异常,参数:{Input}", input);
return BaseResponse<WxLikeOutput>.Fail("取消点赞失败,请稍后重试");
}
}
}

View File

@ -8,7 +8,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat;
/// <summary>
/// 小程序勋章控制器
/// </summary>
public class WeChatMedalController(IMedalService medalService, ILogger<WeChatMedalController> logger) : WeChatBaseController
public class MedalController(IMedalService medalService, ILogger<MedalController> logger) : WeChatBaseController
{
/// <summary>
/// 获取所有勋章列表(含当前用户拥有状态)

View File

@ -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.Id0表示默认皮肤',
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='宠物皮肤图片表';