feat: 完成宠物模块重构与社区功能开发
本次提交包含多项核心更新:
1. 重构宠物模块数据结构:拆分皮肤图片为独立表,优化Pet、PetEvolution实体,调整字段类型与冗余字段
2. 新增宠物皮肤图片管理表,支持多进化阶段多图片展示
3. 完善宠物DTO,新增当前形态名称、皮肤信息与图片序列返回
4. 新增社区功能模块:
- 微信端社区Feed流、点赞/取消点赞接口
- 后台社区消息管理接口与服务实现
5. 优化商城与背包模块,替换皮肤图片获取逻辑为从新表读取预览图
6. 重构勋章服务,新增规则校验逻辑
7. 调整命名规范,修复原有控制器命名问题
This commit is contained in:
219
QYZH.InteractiveMagazine.Service/CommunityMessageService.cs
Normal file
219
QYZH.InteractiveMagazine.Service/CommunityMessageService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
296
QYZH.InteractiveMagazine.Service/WeChatCommunityService.cs
Normal file
296
QYZH.InteractiveMagazine.Service/WeChatCommunityService.cs
Normal 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
|
||||
}
|
||||
@ -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);
|
||||
|
||||
// 换肤
|
||||
|
||||
Reference in New Issue
Block a user