Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/UserJournalService.cs
glz 88e5fd23be refactor(common&service): adjust id range and remove unused qr code method
1. 调整RandomIdHelper的默认ID生成范围从16位改为13位
2. 从IUserJournalService和UserJournalService中移除废弃的单例生成二维码方法
3. 为相关DTO添加JSON数字序列化/反序列化配置,处理前后端数字类型兼容问题
4. 修改二维码内容序列化逻辑,将数字转为字符串避免精度丢失
2026-07-02 16:16:24 +08:00

553 lines
21 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;
private const int MaxRandomIdGenerateRetryCount = 5;
/// <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<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 randomIds = await GenerateUniqueQrCodeIdsAsync(input.Count);
var records = randomIds
.Select(id => new UserJournal
{
Id = id,
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.ToString(), Id = id.ToString() });
}
private async Task<long> GenerateUniqueQrCodeIdAsync()
{
for (var i = 0; i < MaxRandomIdGenerateRetryCount; i++)
{
var id = RandomIdHelper.GenerateLongId();
var exists = await userJournalRepository.Queryable()
.AnyAsync(uj => uj.Id == id);
if (!exists)
{
return id;
}
}
throw new BusinessException("生成二维码ID失败请稍后重试", ResultCode.GLOBAL_ERROR);
}
private async Task<List<long>> GenerateUniqueQrCodeIdsAsync(int count)
{
var ids = new HashSet<long>();
for (var i = 0; i < MaxRandomIdGenerateRetryCount && ids.Count < count; i++)
{
while (ids.Count < count)
{
ids.Add(RandomIdHelper.GenerateLongId());
}
var candidateIds = ids.ToList();
var existingIds = await userJournalRepository.Queryable()
.Where(uj => candidateIds.Contains(uj.Id))
.Select(uj => uj.Id)
.ToListAsync();
if (existingIds.Count == 0)
{
return candidateIds;
}
ids.ExceptWith(existingIds);
}
throw new BusinessException("生成二维码ID失败请稍后重试", ResultCode.GLOBAL_ERROR);
}
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)
.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);
}
}