Compare commits

...

2 Commits

Author SHA1 Message Date
glz
7c53adc804 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-01 17:05:41 +08:00
glz
b95c5029f1 feat: 添加期刊二维码管理功能,调整UserJournal的UserId为可空类型
1.  新增数据库脚本修改User表UserId字段为可空
2.  新增期刊二维码CRUD接口与控制器
3.  重构绑定期刊逻辑,优化重复绑定校验
4.  删除冗余的JwtHelper重载方法
2026-07-01 17:05:38 +08:00
6 changed files with 438 additions and 29 deletions

View File

@ -30,4 +30,34 @@ public interface IUserJournalService : IBaseService<UserJournal>
/// <param name="userId">用户Id</param> /// <param name="userId">用户Id</param>
/// <param name="id">绑定记录Id</param> /// <param name="id">绑定记录Id</param>
Task UnbindJournalAsync(long userId, long id); Task UnbindJournalAsync(long userId, long id);
/// <summary>
/// 生成期刊二维码记录
/// </summary>
/// <param name="input">生成输入</param>
/// <param name="operatorId">操作人Id</param>
/// <returns>二维码记录</returns>
Task<UserJournalQrCodeOutput> CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId);
/// <summary>
/// 分页查询期刊二维码记录
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<UserJournalQrCodeOutput>> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input);
/// <summary>
/// 获取期刊二维码详情
/// </summary>
/// <param name="id">二维码记录Id</param>
/// <returns>二维码记录</returns>
Task<UserJournalQrCodeOutput> GetQrCodeDetailAsync(long id);
/// <summary>
/// 删除未绑定的期刊二维码记录
/// </summary>
/// <param name="input">删除输入</param>
/// <param name="operatorId">操作人Id</param>
/// <returns>是否成功</returns>
Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, long operatorId);
} }

View File

@ -68,11 +68,6 @@ public static class JwtHelper
return $"{AdminTokenKeyPrefix}:{userId}"; return $"{AdminTokenKeyPrefix}:{userId}";
} }
public static string BuildAdminTokenKey(long userId)
{
return BuildAdminTokenKey(userId.ToString());
}
public static string BuildWeChatTokenKey(string wxUserId, string userId) public static string BuildWeChatTokenKey(string wxUserId, string userId)
{ {
return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}"; return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}";

View File

@ -110,3 +110,117 @@ public class UserJournalQueryInput : PageQueryModel
/// </summary> /// </summary>
public string? Type { get; set; } public string? Type { get; set; }
} }
/// <summary>
/// 生成期刊二维码输入DTO
/// </summary>
public class CreateUserJournalQrCodeInput
{
/// <summary>
/// 期刊Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 关联类型: Read, Favorite, Subscribe默认 Subscribe
/// </summary>
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString();
}
/// <summary>
/// 期刊二维码查询输入DTO
/// </summary>
public class UserJournalQrCodeQueryInput : PageQueryModel
{
/// <summary>
/// 期刊Id
/// </summary>
public long? JournalId { get; set; }
/// <summary>
/// 用户Id
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 是否已绑定用户
/// </summary>
public bool? IsBound { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}
/// <summary>
/// 期刊二维码输出DTO
/// </summary>
public class UserJournalQrCodeOutput
{
/// <summary>
/// 二维码记录Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 期刊Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 期刊名称
/// </summary>
public string JournalName { get; set; } = string.Empty;
/// <summary>
/// 用户Id
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 用户昵称
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// 关联类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 是否已绑定用户
/// </summary>
public bool IsBound { get; set; }
/// <summary>
/// 二维码内容
/// </summary>
public string QrCodeContent { get; set; } = string.Empty;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 绑定时间
/// </summary>
public DateTime? BoundAt { get; set; }
}
/// <summary>
/// 删除期刊二维码输入DTO
/// </summary>
public class DeleteUserJournalQrCodeInput
{
/// <summary>
/// 二维码记录Id列表
/// </summary>
public List<long> Ids { get; set; } = [];
}

View File

@ -20,7 +20,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Nullable:False /// Nullable:False
/// </summary> /// </summary>
[SugarColumn(ColumnName = "UserId")] [SugarColumn(ColumnName = "UserId")]
public long UserId { get; set; } public long? UserId { get; set; }
/// <summary> /// <summary>
/// Desc:期刊模板Id对应Journal表的期刊定义 /// Desc:期刊模板Id对应Journal表的期刊定义

View File

@ -7,6 +7,7 @@ using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository; using QYZH.InteractiveMagazine.Repository;
using SqlSugar; using SqlSugar;
using System.Text.Json;
namespace QYZH.InteractiveMagazine.Service; namespace QYZH.InteractiveMagazine.Service;
@ -63,42 +64,57 @@ public class UserJournalService(
} }
// 防重复绑定:同一用户 + 期刊 + 实例 + 类型 // 防重复绑定:同一用户 + 期刊 + 实例 + 类型
var isExist = userJournalRepository.Any(uj => if (!Enum.TryParse<UserJournalTypeEnum>(input.Type, true, out var bindType))
uj.Id == input.Id);
if (isExist)
{ {
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); throw new BusinessException("该期刊已被绑定", ResultCode.BAD_REQUEST);
} }
// 检查是否为首次绑定期刊(用于激活宠物)
var isFirstBind = !userJournalRepository.Context.Queryable<UserJournal>() var isFirstBind = !userJournalRepository.Context.Queryable<UserJournal>()
.Any(uj => uj.UserId == userId); .Any(uj => uj.UserId == userId);
// 创建绑定记录 // 创建绑定记录
var userJournal = new UserJournal var updateCount = await userJournalRepository.Updateable()
{ .SetColumns(uj => uj.UserId == userId)
UserId = userId, .SetColumns(uj => uj.Type == bindType)
JournalId = input.JournalId, .SetColumns(uj => uj.UpdatedBy == userId.ToString())
Id = input.Id, .SetColumns(uj => uj.UpdatedAt == DateTime.Now)
Type = Enum.Parse<UserJournalTypeEnum>(input.Type, true), .Where(uj => uj.Id == input.Id && !uj.IsDeleted && (uj.UserId == null || uj.UserId == 0))
Status = (int)UserJournalStatusEnum.Active, .ExecuteCommandAsync();
IsDeleted = false,
CreatedBy = userId.ToString(),
CreatedAt = DateTime.Now,
UpdatedBy = userId.ToString(),
UpdatedAt = DateTime.Now
};
var result = await userJournalRepository.InsertAsync(userJournal); if (updateCount <= 0)
if (!result)
{ {
logger.LogError("绑定期刊失败写入数据库失败UserId: {UserId}, JournalId: {JournalId}", userId, input.JournalId); logger.LogError("绑定期刊失败写入数据库失败UserId: {UserId}, JournalId: {JournalId}", userId, input.JournalId);
throw new BusinessException("绑定期刊失败,请稍后重试", ResultCode.GLOBAL_ERROR); throw new BusinessException("绑定期刊失败,请稍后重试", ResultCode.GLOBAL_ERROR);
} }
logger.LogInformation("用户绑定期刊成功UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, userJournal.Id); 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); await SendBindJournalMessageAsync(user, journal);
// 首次绑定期刊时激活宠物 // 首次绑定期刊时激活宠物
@ -118,7 +134,7 @@ public class UserJournalService(
return new BindJournalOutput return new BindJournalOutput
{ {
Id = userJournal.Id, Id = userJournal.Id,
UserId = userJournal.UserId, UserId = userId,
JournalId = userJournal.JournalId, JournalId = userJournal.JournalId,
Type = userJournal.Type.ToString(), Type = userJournal.Type.ToString(),
Status = userJournal.Status.ToString(), Status = userJournal.Status.ToString(),
@ -126,6 +142,197 @@ public class UserJournalService(
}; };
} }
/// <summary>
/// 生成期刊二维码记录
/// </summary>
public async Task<UserJournalQrCodeOutput> CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId)
{
if (input.JournalId <= 0)
{
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
}
if (!Enum.TryParse<UserJournalTypeEnum>(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);
}
/// <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,
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) private async Task SendBindJournalMessageAsync(Users user, Journal journal)
{ {
try try
@ -182,7 +389,7 @@ public class UserJournalService(
.Select(uj => new BindJournalOutput .Select(uj => new BindJournalOutput
{ {
Id = uj.Id, Id = uj.Id,
UserId = uj.UserId, UserId = uj.UserId ?? 0,
JournalId = uj.JournalId, JournalId = uj.JournalId,
Type = uj.Type.ToString(), Type = uj.Type.ToString(),
Status = uj.Status.ToString(), Status = uj.Status.ToString(),

View File

@ -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;
/// <summary>
/// 期刊二维码管理
/// </summary>
[ApiController]
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
[Route("api/userJournalQrCode")]
public class UserJournalQrCodeController(IUserJournalService userJournalService) : BaseController
{
/// <summary>
/// 生成期刊二维码
/// </summary>
/// <param name="input">生成输入</param>
/// <returns>二维码记录</returns>
[HttpPost("add")]
public async Task<BaseResponse<UserJournalQrCodeOutput>> AddAsync([FromBody] CreateUserJournalQrCodeInput input)
{
var result = await userJournalService.CreateQrCodeAsync(input, GetCurrentUserId() ?? 0);
return BaseResponse<UserJournalQrCodeOutput>.Success(result);
}
/// <summary>
/// 分页查询期刊二维码
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
[HttpPost("pagelist")]
public async Task<BaseResponse<PageListModel<UserJournalQrCodeOutput>>> GetPageListAsync([FromBody] UserJournalQrCodeQueryInput input)
{
var result = await userJournalService.GetQrCodePageListAsync(input);
return BaseResponse<PageListModel<UserJournalQrCodeOutput>>.Success(result);
}
/// <summary>
/// 获取期刊二维码详情
/// </summary>
/// <param name="id">二维码记录Id</param>
/// <returns>二维码记录</returns>
[HttpGet("detail/{id:long}")]
public async Task<BaseResponse<UserJournalQrCodeOutput>> GetDetailAsync(long id)
{
var result = await userJournalService.GetQrCodeDetailAsync(id);
return BaseResponse<UserJournalQrCodeOutput>.Success(result);
}
/// <summary>
/// 删除未绑定的期刊二维码
/// </summary>
/// <param name="input">删除输入</param>
/// <returns>是否成功</returns>
[HttpPost("delete")]
public async Task<BaseResponse<bool>> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input)
{
var result = await userJournalService.DeleteQrCodeAsync(input, GetCurrentUserId() ?? 0);
return BaseResponse<bool>.Success(result);
}
}