Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/UserJournalService.cs
glz cc2276fe27 refactor(user-journal): 重构用户期刊关联逻辑,移除枚举依赖并新增二维码功能
1.  删除不再使用的UserJournalTypeEnum枚举
2.  将UserJournal实体的Type字段改为int类型并移除枚举依赖
3.  移除BindJournal相关接口的类型参数校验和赋值逻辑
4.  新增二维码生成和上传功能,为用户期刊绑定QrCodeUrl字段
5.  修复UserAnswerTaskController的用户ID获取逻辑
6.  调整UserJournalQrCodeOutput的字段映射,替换QrCodeContent为QrCodeUrl
2026-07-01 17:59:35 +08:00

432 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using SqlSugar;
using System.Text.Json;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// 用户期刊关联服务实现
/// </summary>
public class UserJournalService(
BaseRepository<UserJournal> userJournalRepository,
BaseRepository<Users> usersRepository,
BaseRepository<Journal> journalRepository,
ILogger<UserJournalService> logger,
IRabbitMQService rabbitMqService,
OssService ossService,
IPetService petService)
: BaseRepository<UserJournal>, IUserJournalService
{
private const string JournalExchange = "ex.journal";
private const string BindJournalQueue = "mq.journal.bindUser";
private const string BindJournalRoutingKey = "rk.journal.bindUser";
/// <summary>
/// 用户绑定期刊(扫码绑定)
/// </summary>
public async Task<BindJournalOutput> BindJournalAsync(long userId, BindJournalInput input)
{
// 校验参数
if (input.JournalId <= 0|| input.Id <= 0)
{
throw new BusinessException("参数错误,未获取到期刊", ResultCode.BAD_REQUEST);
}
// 校验用户是否存在
var user = await usersRepository.GetByIdAsync(userId);
if (user == null || user.IsDeleted)
{
logger.LogWarning("绑定期刊失败用户不存在UserId: {UserId}", userId);
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
}
// 校验期刊是否存在
var journal = await journalRepository.GetByIdAsync(input.JournalId);
if (journal == null || journal.IsDeleted)
{
logger.LogWarning("绑定期刊失败期刊不存在JournalId: {JournalId}", input.JournalId);
throw new BusinessException("期刊不存在", ResultCode.NOT_FOUND);
}
// 校验期刊状态
if (journal.Status != (int)JournalStatusEnum.Published)
{
logger.LogWarning("绑定期刊失败期刊未发布JournalId: {JournalId}, Status: {Status}", input.JournalId, journal.Status);
throw new BusinessException("该期刊暂未发布,无法绑定", ResultCode.UNPROCESSABLE_ENTITY);
}
var userJournal = await userJournalRepository.Queryable()
.Where(uj => uj.Id == input.Id && uj.JournalId == input.JournalId && !uj.IsDeleted)
.FirstAsync();
if (userJournal == null)
{
logger.LogWarning("绑定期刊失败二维码记录不存在UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, input.Id);
throw new BusinessException("二维码不存在或已失效", ResultCode.NOT_FOUND);
}
if (userJournal.Status != (int)UserJournalStatusEnum.Active)
{
throw new BusinessException("二维码已失效", ResultCode.UNPROCESSABLE_ENTITY);
}
if (userJournal.UserId.HasValue && userJournal.UserId.Value > 0)
{
logger.LogWarning("重复绑定期刊UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, input.Id);
throw new BusinessException("该期刊已被绑定", ResultCode.BAD_REQUEST);
}
var isFirstBind = !userJournalRepository.Context.Queryable<UserJournal>()
.Any(uj => uj.UserId == userId);
// 创建绑定记录
var updateCount = await userJournalRepository.Updateable()
.SetColumns(uj => uj.UserId == userId)
.SetColumns(uj => uj.UpdatedBy == userId.ToString())
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
.Where(uj => uj.Id == input.Id && !uj.IsDeleted && (uj.UserId == null || uj.UserId == 0))
.ExecuteCommandAsync();
if (updateCount <= 0)
{
logger.LogError("绑定期刊失败写入数据库失败UserId: {UserId}, JournalId: {JournalId}", userId, input.JournalId);
throw new BusinessException("绑定期刊失败,请稍后重试", ResultCode.GLOBAL_ERROR);
}
logger.LogInformation("用户绑定期刊成功UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, userJournal.Id);
userJournal.UserId = userId;
userJournal.UpdatedBy = userId.ToString();
userJournal.UpdatedAt = DateTime.Now;
await SendBindJournalMessageAsync(user, journal);
// 首次绑定期刊时激活宠物
if (isFirstBind)
{
try
{
await petService.ActivatePetAsync(userId);
}
catch (Exception ex)
{
logger.LogError(ex, "首次绑定期刊激活宠物失败UserId: {UserId}", userId);
// 宠物激活失败不阻断绑定流程
}
}
return new BindJournalOutput
{
Id = userJournal.Id,
UserId = userId,
JournalId = userJournal.JournalId,
Type = userJournal.Type.ToString(),
Status = userJournal.Status.ToString(),
CreatedAt = userJournal.CreatedAt
};
}
/// <summary>
/// 生成期刊二维码记录
/// </summary>
public async Task<UserJournalQrCodeOutput> CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId)
{
if (input.JournalId <= 0)
{
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
}
var journal = await journalRepository.GetByIdAsync(input.JournalId);
if (journal == null || journal.IsDeleted)
{
throw new BusinessException("期刊不存在", ResultCode.NOT_FOUND);
}
if (journal.Status != (int)JournalStatusEnum.Published)
{
throw new BusinessException("该期刊暂未发布,无法生成二维码", ResultCode.UNPROCESSABLE_ENTITY);
}
var record = new UserJournal
{
UserId = null,
JournalId = input.JournalId,
Type = 0,
Status = (int)UserJournalStatusEnum.Active,
IsDeleted = false,
CreatedBy = operatorId.ToString(),
CreatedAt = DateTime.Now,
UpdatedBy = operatorId.ToString(),
UpdatedAt = DateTime.Now
};
var qrCodeContent = BuildQrCodeContent(record.JournalId, record.Id);
var qrCodeKey = $"journal/qrcode/{record.JournalId}/{record.Id}.png";
using var qrCodeStream = new MemoryStream(QrCodeHelper.GeneratePng(qrCodeContent));
var uploadedKey = ossService.PutObject(qrCodeKey, qrCodeStream);
if (string.IsNullOrWhiteSpace(uploadedKey))
{
throw new BusinessException("二维码图片上传失败,请稍后重试", ResultCode.GLOBAL_ERROR);
}
record.QrCodeUrl = uploadedKey;
var result = await userJournalRepository.InsertAsync(record);
if (!result)
{
throw new BusinessException("生成二维码失败,请稍后重试", ResultCode.GLOBAL_ERROR);
}
return MapQrCodeOutput(record, journal, null);
}
/// <summary>
/// 分页查询期刊二维码记录
/// </summary>
public async Task<PageListModel<UserJournalQrCodeOutput>> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input)
{
if (input.PageIndex <= 0)
{
throw new BusinessException("页码必须大于0", ResultCode.BAD_REQUEST);
}
if (input.PageSize <= 0 || input.PageSize > 100)
{
throw new BusinessException("每页条数必须在1-100之间", ResultCode.BAD_REQUEST);
}
var query = userJournalRepository.Queryable()
.Where(uj => !uj.IsDeleted)
.WhereIF(input.JournalId.HasValue, uj => uj.JournalId == input.JournalId!.Value)
.WhereIF(input.UserId.HasValue, uj => uj.UserId == input.UserId!.Value)
.WhereIF(input.Status.HasValue, uj => uj.Status == input.Status!.Value)
.WhereIF(input.IsBound == true, uj => uj.UserId != null && uj.UserId > 0)
.WhereIF(input.IsBound == false, uj => uj.UserId == null || uj.UserId == 0)
.OrderByDescending(uj => uj.CreatedAt);
RefAsync<int> totalNumber = 0;
var records = await query.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
var outputs = await BuildQrCodeOutputsAsync(records);
return new PageListModel<UserJournalQrCodeOutput>(outputs, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 获取期刊二维码详情
/// </summary>
public async Task<UserJournalQrCodeOutput> GetQrCodeDetailAsync(long id)
{
var record = await userJournalRepository.GetByIdAsync(id);
if (record == null || record.IsDeleted)
{
throw new BusinessException("二维码记录不存在", ResultCode.NOT_FOUND);
}
var outputs = await BuildQrCodeOutputsAsync([record]);
return outputs.First();
}
/// <summary>
/// 删除未绑定的期刊二维码记录
/// </summary>
public async Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, long operatorId)
{
if (input.Ids == null || input.Ids.Count == 0)
{
throw new BusinessException("请选择要删除的二维码", ResultCode.BAD_REQUEST);
}
var ids = input.Ids.Distinct().ToList();
var records = await userJournalRepository.Queryable()
.Where(uj => ids.Contains(uj.Id) && !uj.IsDeleted)
.ToListAsync();
if (records.Count != ids.Count)
{
throw new BusinessException("二维码记录不存在", ResultCode.NOT_FOUND);
}
if (records.Any(uj => uj.UserId.HasValue && uj.UserId.Value > 0))
{
throw new BusinessException("已绑定用户的二维码不能删除", ResultCode.UNPROCESSABLE_ENTITY);
}
var updateCount = await userJournalRepository.Updateable()
.SetColumns(uj => uj.IsDeleted == true)
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Inactive)
.SetColumns(uj => uj.UpdatedBy == operatorId.ToString())
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
.Where(uj => ids.Contains(uj.Id) && !uj.IsDeleted && (uj.UserId == null || uj.UserId == 0))
.ExecuteCommandAsync();
return updateCount == ids.Count;
}
private async Task<List<UserJournalQrCodeOutput>> BuildQrCodeOutputsAsync(List<UserJournal> records)
{
if (records.Count == 0)
{
return [];
}
var journalIds = records.Select(r => r.JournalId).Distinct().ToList();
var journals = await journalRepository.Queryable()
.Where(j => journalIds.Contains(j.Id))
.ToListAsync();
var journalDict = journals.ToDictionary(j => j.Id);
var userIds = records
.Where(r => r.UserId.HasValue && r.UserId.Value > 0)
.Select(r => r.UserId!.Value)
.Distinct()
.ToList();
List<Users> users = userIds.Count == 0
? []
: await usersRepository.Queryable()
.Where(u => userIds.Contains(u.Id))
.ToListAsync();
var userDict = users.ToDictionary(u => u.Id);
return records.Select(record =>
{
journalDict.TryGetValue(record.JournalId, out var journal);
Users? user = null;
if (record.UserId.HasValue)
{
userDict.TryGetValue(record.UserId.Value, out user);
}
return MapQrCodeOutput(record, journal, user);
}).ToList();
}
private static UserJournalQrCodeOutput MapQrCodeOutput(UserJournal record, Journal? journal, Users? user)
{
return new UserJournalQrCodeOutput
{
Id = record.Id,
JournalId = record.JournalId,
JournalName = journal?.Name ?? journal?.Title ?? string.Empty,
UserId = record.UserId,
UserName = user?.Name,
Type = record.Type.ToString(),
Status = record.Status.ToString(),
IsBound = record.UserId.HasValue && record.UserId.Value > 0,
QrCodeUrl = DomainHelper.OssFullUrl(record.QrCodeUrl ?? string.Empty),
CreatedAt = record.CreatedAt,
BoundAt = record.UserId.HasValue && record.UserId.Value > 0 ? record.UpdatedAt : null
};
}
private static string BuildQrCodeContent(long journalId, long id)
{
return JsonSerializer.Serialize(new { JournalId = journalId, Id = id });
}
private async Task SendBindJournalMessageAsync(Users user, Journal journal)
{
try
{
var messageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
{
Exchange = JournalExchange,
Queue = BindJournalQueue,
RoutingKey = BindJournalRoutingKey,
Data = new BindJournalMessage
{
UserId = user.Id,
JournalId = journal.Id,
StartTime = journal.StartTime,
EndTime = journal.EndTime,
UploadDomain = user.UploadDomain
}
});
if (!messageSent)
{
logger.LogError("发送绑定期刊消息失败UserId: {UserId}, JournalId: {JournalId}", user.Id, journal.Id);
}
}
catch (Exception ex)
{
logger.LogError(ex, "发送绑定期刊消息异常UserId: {UserId}, JournalId: {JournalId}", user.Id, journal.Id);
}
}
/// <summary>
/// 获取用户的期刊绑定列表
/// </summary>
public async Task<PageListModel<BindJournalOutput>> GetUserJournalsAsync(long userId, UserJournalQueryInput input)
{
logger.LogInformation("查询用户期刊绑定列表UserId: {UserId}, PageIndex: {PageIndex}, PageSize: {PageSize}",
userId, input.PageIndex, input.PageSize);
if (input.PageIndex <= 0)
{
throw new BusinessException("页码必须大于0", ResultCode.BAD_REQUEST);
}
if (input.PageSize <= 0 || input.PageSize > 100)
{
throw new BusinessException("每页条数必须在1-100之间", ResultCode.BAD_REQUEST);
}
RefAsync<int> totalNumber = 0;
var pageResult = await userJournalRepository.Queryable()
.Where(uj => uj.UserId == userId)
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type.ToString() == input.Type)
.OrderByDescending(uj => uj.CreatedAt)
.Select(uj => new BindJournalOutput
{
Id = uj.Id,
UserId = uj.UserId ?? 0,
JournalId = uj.JournalId,
Type = uj.Type.ToString(),
Status = uj.Status.ToString(),
CreatedAt = uj.CreatedAt
}, true)
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
return new PageListModel<BindJournalOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 取消期刊绑定
/// </summary>
public async Task UnbindJournalAsync(long userId, long id)
{
logger.LogInformation("取消期刊绑定UserId: {UserId}, Id: {Id}", userId, id);
var userJournal = await userJournalRepository.GetByIdAsync(id);
if (userJournal == null || userJournal.IsDeleted)
{
logger.LogWarning("取消绑定失败记录不存在Id: {Id}", id);
throw new BusinessException("绑定记录不存在", ResultCode.NOT_FOUND);
}
// 校验归属权:只能取消自己的绑定
if (userJournal.UserId != userId)
{
logger.LogWarning("取消绑定失败无权操作UserId: {UserId}, RecordUserId: {RecordUserId}", userId, userJournal.UserId);
throw new BusinessException("无权取消该绑定", ResultCode.FORBIDDEN);
}
var result = await userJournalRepository.DeleteByIdAsync(id);
if (!result)
{
logger.LogError("取消绑定失败Id: {Id}", id);
throw new BusinessException("取消绑定失败,请稍后重试", ResultCode.GLOBAL_ERROR);
}
logger.LogInformation("取消期刊绑定成功UserId: {UserId}, Id: {Id}", userId, id);
}
}