Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/UserJournalService.cs
glz 72c7a38f1d refactor(UserJournal): 优化期刊绑定逻辑并完善校验
1. 重构用户查询逻辑,增加未删除过滤
2. 将枚举常量提取复用,统一状态判断
3. 新增数据库事务包裹绑定操作
4. 增加重复绑定同一期刊的校验逻辑
5. 统一绑定时间变量复用
6. 完善更新条件,增加期刊ID和状态校验
2026-07-02 11:25:24 +08:00

560 lines
22 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";
private const string QrCodeGenerateQueue = "mq.journal.qrcode.generate";
private const string QrCodeGenerateRoutingKey = "rk.journal.qrcode.generate";
private const int MaxBatchQrCodeCount = 500;
/// <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 activeStatus = (int)UserJournalStatusEnum.Active;
var boundAt = DateTime.Now;
var user = await usersRepository.Queryable()
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstAsync();
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 != activeStatus)
{
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 = false;
await userJournalRepository.UseTranAsync(async () =>
{
var boundJournalIds = await userJournalRepository.Queryable()
.Where(uj => uj.UserId == userId && !uj.IsDeleted && uj.Status == activeStatus)
.Select(uj => uj.JournalId)
.ToListAsync();
if (boundJournalIds.Contains(input.JournalId))
{
logger.LogWarning("用户重复绑定同一期刊UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, input.Id);
throw new BusinessException("该用户已绑定过该期刊", ResultCode.BAD_REQUEST);
}
isFirstBind = boundJournalIds.Count == 0;
boundAt = DateTime.Now;
// 创建绑定记录
var updateCount = await userJournalRepository.Updateable()
.SetColumns(uj => uj.UserId == userId)
.SetColumns(uj => uj.UpdatedBy == userId.ToString())
.SetColumns(uj => uj.UpdatedAt == boundAt)
.Where(uj => uj.Id == input.Id
&& uj.JournalId == input.JournalId
&& !uj.IsDeleted
&& uj.Status == activeStatus
&& (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 = boundAt;
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, string operatorName)
{
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 = operatorName,
CreatedAt = DateTime.Now,
UpdatedBy = operatorName,
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<CreateUserJournalQrCodeOutput> CreateQrCodesAsync(CreateUserJournalQrCodeInput input, string operatorName)
{
if (input.JournalId <= 0)
{
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
}
if (input.Count <= 0 || input.Count > MaxBatchQrCodeCount)
{
throw new BusinessException($"生成数量必须在1-{MaxBatchQrCodeCount}之间", 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 now = DateTime.Now;
var records = Enumerable.Range(0, input.Count)
.Select(_ => new UserJournal
{
UserId = null,
JournalId = input.JournalId,
Type = 0,
Status = (int)UserJournalStatusEnum.Generating,
IsDeleted = false,
CreatedBy = operatorName,
CreatedAt = now,
UpdatedBy = operatorName,
UpdatedAt = now
})
.ToList();
var insertCount = await userJournalRepository.Context.Insertable(records).ExecuteCommandAsync();
if (insertCount <= 0)
{
throw new BusinessException("提交二维码生成任务失败,请稍后重试", ResultCode.GLOBAL_ERROR);
}
var recordIds = records.Select(r => r.Id).ToList();
var messageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
{
Exchange = JournalExchange,
Queue = QrCodeGenerateQueue,
RoutingKey = QrCodeGenerateRoutingKey,
Data = new GenerateUserJournalQrCodeMessage
{
RecordIds = recordIds,
OperatorName = operatorName
}
});
if (!messageSent)
{
await userJournalRepository.Updateable()
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Failed)
.SetColumns(uj => uj.UpdatedBy == operatorName)
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
.Where(uj => recordIds.Contains(uj.Id) && !uj.IsDeleted && uj.Status == (int)UserJournalStatusEnum.Generating)
.ExecuteCommandAsync();
logger.LogError("发送期刊二维码生成消息失败RecordIds: {RecordIds}", string.Join(",", recordIds));
throw new BusinessException("二维码生成任务提交失败,请稍后重试", ResultCode.GLOBAL_ERROR);
}
return new CreateUserJournalQrCodeOutput
{
JournalId = input.JournalId,
RequestedCount = input.Count,
AcceptedCount = insertCount,
RecordIds = recordIds,
IsAsync = true,
Message = "二维码生成任务已提交,请稍后查询未绑定二维码列表"
};
}
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, string operatorName)
{
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 == operatorName)
.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;
}
public async Task<List<UserJournalQrCodeOutput>> GetUnboundQrCodesByJournalIdAsync(long journalId)
{
if (journalId <= 0)
{
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
}
var records = await userJournalRepository.Queryable()
.Where(uj => uj.JournalId == journalId && !uj.IsDeleted)
.Where(uj => uj.UserId == null || uj.UserId == 0)
.Where(uj => uj.Status == (int)UserJournalStatusEnum.Active)
.Where(uj => !string.IsNullOrEmpty(uj.QrCodeUrl))
.OrderByDescending(uj => uj.CreatedAt)
.ToListAsync();
return await BuildQrCodeOutputsAsync(records);
}
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);
}
}