diff --git a/QYZH.InteractiveMagazine.IService/IUserJournalService.cs b/QYZH.InteractiveMagazine.IService/IUserJournalService.cs index 1a91763..38102c6 100644 --- a/QYZH.InteractiveMagazine.IService/IUserJournalService.cs +++ b/QYZH.InteractiveMagazine.IService/IUserJournalService.cs @@ -30,4 +30,34 @@ public interface IUserJournalService : IBaseService /// 用户Id /// 绑定记录Id Task UnbindJournalAsync(long userId, long id); + + /// + /// 生成期刊二维码记录 + /// + /// 生成输入 + /// 操作人Id + /// 二维码记录 + Task CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId); + + /// + /// 分页查询期刊二维码记录 + /// + /// 查询条件 + /// 分页结果 + Task> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input); + + /// + /// 获取期刊二维码详情 + /// + /// 二维码记录Id + /// 二维码记录 + Task GetQrCodeDetailAsync(long id); + + /// + /// 删除未绑定的期刊二维码记录 + /// + /// 删除输入 + /// 操作人Id + /// 是否成功 + Task DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, long operatorId); } diff --git a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs index 1321366..f1f7310 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs @@ -68,11 +68,6 @@ public static class JwtHelper return $"{AdminTokenKeyPrefix}:{userId}"; } - public static string BuildAdminTokenKey(long userId) - { - return BuildAdminTokenKey(userId.ToString()); - } - public static string BuildWeChatTokenKey(string wxUserId, string userId) { return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}"; diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs index 7862518..6660c26 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs @@ -110,3 +110,117 @@ public class UserJournalQueryInput : PageQueryModel /// public string? Type { get; set; } } + +/// +/// 生成期刊二维码输入DTO +/// +public class CreateUserJournalQrCodeInput +{ + /// + /// 期刊Id + /// + public long JournalId { get; set; } + + /// + /// 关联类型: Read, Favorite, Subscribe,默认 Subscribe + /// + public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString(); +} + +/// +/// 期刊二维码查询输入DTO +/// +public class UserJournalQrCodeQueryInput : PageQueryModel +{ + /// + /// 期刊Id + /// + public long? JournalId { get; set; } + + /// + /// 用户Id + /// + public long? UserId { get; set; } + + /// + /// 是否已绑定用户 + /// + public bool? IsBound { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} + +/// +/// 期刊二维码输出DTO +/// +public class UserJournalQrCodeOutput +{ + /// + /// 二维码记录Id + /// + public long Id { get; set; } + + /// + /// 期刊Id + /// + public long JournalId { get; set; } + + /// + /// 期刊名称 + /// + public string JournalName { get; set; } = string.Empty; + + /// + /// 用户Id + /// + public long? UserId { get; set; } + + /// + /// 用户昵称 + /// + public string? UserName { get; set; } + + /// + /// 关联类型 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 状态 + /// + public string Status { get; set; } = string.Empty; + + /// + /// 是否已绑定用户 + /// + public bool IsBound { get; set; } + + /// + /// 二维码内容 + /// + public string QrCodeContent { get; set; } = string.Empty; + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 绑定时间 + /// + public DateTime? BoundAt { get; set; } +} + +/// +/// 删除期刊二维码输入DTO +/// +public class DeleteUserJournalQrCodeInput +{ + /// + /// 二维码记录Id列表 + /// + public List Ids { get; set; } = []; +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/UserJournal.cs b/QYZH.InteractiveMagazine.Models/Entity/UserJournal.cs index e1de98a..611bce8 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/UserJournal.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/UserJournal.cs @@ -20,7 +20,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Nullable:False /// [SugarColumn(ColumnName = "UserId")] - public long UserId { get; set; } + public long? UserId { get; set; } /// /// Desc:期刊模板Id(对应Journal表的期刊定义) diff --git a/QYZH.InteractiveMagazine.Service/UserJournalService.cs b/QYZH.InteractiveMagazine.Service/UserJournalService.cs index 2a16225..a03af33 100644 --- a/QYZH.InteractiveMagazine.Service/UserJournalService.cs +++ b/QYZH.InteractiveMagazine.Service/UserJournalService.cs @@ -7,6 +7,7 @@ using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Repository; using SqlSugar; +using System.Text.Json; namespace QYZH.InteractiveMagazine.Service; @@ -63,42 +64,57 @@ public class UserJournalService( } // 防重复绑定:同一用户 + 期刊 + 实例 + 类型 - var isExist = userJournalRepository.Any(uj => - uj.Id == input.Id); - - if (isExist) + if (!Enum.TryParse(input.Type, true, out var bindType)) { - logger.LogWarning("重复绑定期刊,UserId: {UserId}, JournalId: {JournalId}, Type: {Type}", userId, input.JournalId, input.Type); + throw new BusinessException("关联类型不正确", ResultCode.BAD_REQUEST); + } + + 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() .Any(uj => uj.UserId == userId); // 创建绑定记录 - var userJournal = new UserJournal - { - UserId = userId, - JournalId = input.JournalId, - Id = input.Id, - Type = Enum.Parse(input.Type, true), - Status = (int)UserJournalStatusEnum.Active, - IsDeleted = false, - CreatedBy = userId.ToString(), - CreatedAt = DateTime.Now, - UpdatedBy = userId.ToString(), - UpdatedAt = DateTime.Now - }; + var updateCount = await userJournalRepository.Updateable() + .SetColumns(uj => uj.UserId == userId) + .SetColumns(uj => uj.Type == bindType) + .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(); - var result = await userJournalRepository.InsertAsync(userJournal); - if (!result) + 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.Type = bindType; + userJournal.UpdatedBy = userId.ToString(); + userJournal.UpdatedAt = DateTime.Now; + await SendBindJournalMessageAsync(user, journal); // 首次绑定期刊时激活宠物 @@ -118,7 +134,7 @@ public class UserJournalService( return new BindJournalOutput { Id = userJournal.Id, - UserId = userJournal.UserId, + UserId = userId, JournalId = userJournal.JournalId, Type = userJournal.Type.ToString(), Status = userJournal.Status.ToString(), @@ -126,6 +142,197 @@ public class UserJournalService( }; } + /// + /// 生成期刊二维码记录 + /// + public async Task CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId) + { + if (input.JournalId <= 0) + { + throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST); + } + + if (!Enum.TryParse(input.Type, true, out var type)) + { + throw new BusinessException("关联类型不正确", 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 = type, + Status = (int)UserJournalStatusEnum.Active, + IsDeleted = false, + CreatedBy = operatorId.ToString(), + CreatedAt = DateTime.Now, + UpdatedBy = operatorId.ToString(), + UpdatedAt = DateTime.Now + }; + + var result = await userJournalRepository.InsertAsync(record); + if (!result) + { + throw new BusinessException("生成二维码失败,请稍后重试", ResultCode.GLOBAL_ERROR); + } + + return MapQrCodeOutput(record, journal, null); + } + + /// + /// 分页查询期刊二维码记录 + /// + public async Task> 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 totalNumber = 0; + var records = await query.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); + var outputs = await BuildQrCodeOutputsAsync(records); + + return new PageListModel(outputs, input.PageIndex, input.PageSize, totalNumber); + } + + /// + /// 获取期刊二维码详情 + /// + public async Task 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(); + } + + /// + /// 删除未绑定的期刊二维码记录 + /// + public async Task 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> BuildQrCodeOutputsAsync(List 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 = 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, + QrCodeContent = BuildQrCodeContent(record.JournalId, record.Id), + 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 @@ -182,7 +389,7 @@ public class UserJournalService( .Select(uj => new BindJournalOutput { Id = uj.Id, - UserId = uj.UserId, + UserId = uj.UserId ?? 0, JournalId = uj.JournalId, Type = uj.Type.ToString(), Status = uj.Status.ToString(), diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs new file mode 100644 index 0000000..36591f3 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 期刊二维码管理 +/// +[ApiController] +[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] +[Route("api/userJournalQrCode")] +public class UserJournalQrCodeController(IUserJournalService userJournalService) : BaseController +{ + /// + /// 生成期刊二维码 + /// + /// 生成输入 + /// 二维码记录 + [HttpPost("add")] + public async Task> AddAsync([FromBody] CreateUserJournalQrCodeInput input) + { + var result = await userJournalService.CreateQrCodeAsync(input, GetCurrentUserId() ?? 0); + return BaseResponse.Success(result); + } + + /// + /// 分页查询期刊二维码 + /// + /// 查询条件 + /// 分页结果 + [HttpPost("pagelist")] + public async Task>> GetPageListAsync([FromBody] UserJournalQrCodeQueryInput input) + { + var result = await userJournalService.GetQrCodePageListAsync(input); + return BaseResponse>.Success(result); + } + + /// + /// 获取期刊二维码详情 + /// + /// 二维码记录Id + /// 二维码记录 + [HttpGet("detail/{id:long}")] + public async Task> GetDetailAsync(long id) + { + var result = await userJournalService.GetQrCodeDetailAsync(id); + return BaseResponse.Success(result); + } + + /// + /// 删除未绑定的期刊二维码 + /// + /// 删除输入 + /// 是否成功 + [HttpPost("delete")] + public async Task> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input) + { + var result = await userJournalService.DeleteQrCodeAsync(input, GetCurrentUserId() ?? 0); + return BaseResponse.Success(result); + } +}