2026-06-03 16:09:56 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-06-08 16:57:44 +08:00
|
|
|
|
using Newtonsoft.Json;
|
2026-06-03 16:09:56 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.IService;
|
2026-06-05 17:57:24 +08:00
|
|
|
|
|
2026-06-03 16:09:56 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Common;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Models.Dto;
|
2026-06-08 16:57:44 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Models.Dto.Points;
|
2026-06-03 16:09:56 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Entity;
|
2026-06-08 16:57:44 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Enum;
|
2026-06-03 16:09:56 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Repository;
|
|
|
|
|
|
using SqlSugar;
|
|
|
|
|
|
|
|
|
|
|
|
namespace QYZH.InteractiveMagazine.Service;
|
|
|
|
|
|
|
2026-06-08 16:57:44 +08:00
|
|
|
|
public class UsersService(
|
|
|
|
|
|
BaseRepository<Users> usersRepository,
|
|
|
|
|
|
ILogger<UsersService> _logger,
|
|
|
|
|
|
IPointsService pointsService,
|
|
|
|
|
|
ICheckInService checkInService,
|
|
|
|
|
|
ICompensationTaskService compensationTaskService,
|
|
|
|
|
|
IUserJournalService userJournalService,
|
|
|
|
|
|
IOperationLogService operationLogService) : BaseRepository<Users>, IUsersService
|
2026-06-03 16:09:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 分页查询用户列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var page = Queryable()
|
|
|
|
|
|
.WhereIF(!string.IsNullOrEmpty(input.WxUserId), u => u.OpenId == input.WxUserId)
|
|
|
|
|
|
.OrderBy(u => u.Id, OrderByType.Desc)
|
|
|
|
|
|
.ToPage<Users, UsersOutput>(input);
|
|
|
|
|
|
|
|
|
|
|
|
return BaseResponse<PageListModel<UsersOutput>>.Success(page);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-08 16:57:44 +08:00
|
|
|
|
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表)
|
2026-06-03 16:09:56 +08:00
|
|
|
|
/// </summary>
|
2026-06-08 16:57:44 +08:00
|
|
|
|
public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id)
|
2026-06-03 16:09:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
var user = await GetByIdAsync<UsersOutput>(u => u.Id == id);
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
{
|
2026-06-08 16:57:44 +08:00
|
|
|
|
return BaseResponse<UserDetailOutput>.Fail("用户不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 并行查询关联数据
|
|
|
|
|
|
var pointsTask = GetPointsRecordsAsync(id);
|
|
|
|
|
|
var checkInTask = GetCheckInRecordsAsync(id);
|
|
|
|
|
|
var compensationTask = GetFailedCompensationTasksAsync(id);
|
|
|
|
|
|
var journalsTask = GetUserJournalsWithDetailAsync(id);
|
|
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(pointsTask, checkInTask, compensationTask, journalsTask);
|
|
|
|
|
|
|
|
|
|
|
|
return BaseResponse<UserDetailOutput>.Success(new UserDetailOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
BasicInfo = user,
|
|
|
|
|
|
PointsRecords = await pointsTask,
|
|
|
|
|
|
CheckInRecords = await checkInTask,
|
|
|
|
|
|
FailedCompensationTasks = await compensationTask,
|
|
|
|
|
|
Journals = await journalsTask
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取用户积分记录(最近20条)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<List<PointsRecordOutput>> GetPointsRecordsAsync(long userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = await pointsService.GetPointsRecordsAsync(new PointsRecordQueryInput
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = userId,
|
|
|
|
|
|
PageIndex = 1,
|
|
|
|
|
|
PageSize = 20
|
|
|
|
|
|
});
|
|
|
|
|
|
return result.Result ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "获取用户积分记录失败,UserId: {UserId}", userId);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取用户签到记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<List<CheckInRecordOutput>> GetCheckInRecordsAsync(long userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var info = await checkInService.GetCheckInInfoAsync(userId);
|
|
|
|
|
|
return info.RecentRecords ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "获取用户签到记录失败,UserId: {UserId}", userId);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取用户失败的补偿任务(需要手动处理)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<List<CompensationTaskOutput>> GetFailedCompensationTasksAsync(long userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var tasks = await compensationTaskService.GetTasksAsync(new GetCompensationTasksInput
|
|
|
|
|
|
{
|
|
|
|
|
|
Status = CompensationTaskStatusEnum.Failed,
|
|
|
|
|
|
Limit = 50
|
|
|
|
|
|
});
|
|
|
|
|
|
return tasks.Where(t => t.UserId == userId).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "获取用户失败补偿任务失败,UserId: {UserId}", userId);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取用户拥有的期刊列表(含期刊详情)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<List<UserJournalItemOutput>> GetUserJournalsWithDetailAsync(long userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var userJournals = await Context.Queryable<UserJournal>()
|
|
|
|
|
|
.Where(uj => uj.UserId == userId && !uj.IsDeleted)
|
|
|
|
|
|
.OrderByDescending(uj => uj.CreatedAt)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var result = new List<UserJournalItemOutput>();
|
|
|
|
|
|
foreach (var uj in userJournals)
|
|
|
|
|
|
{
|
|
|
|
|
|
var journal = await Context.Queryable<Journal>()
|
|
|
|
|
|
.Where(j => j.Id == uj.JournalId && !j.IsDeleted)
|
|
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (journal != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.Add(new UserJournalItemOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
BindId = uj.Id,
|
|
|
|
|
|
JournalId = uj.JournalId,
|
|
|
|
|
|
JournalTitle = journal.Title,
|
|
|
|
|
|
CoverImageUrl = journal.CoverImageUrl,
|
|
|
|
|
|
Type = uj.Type.ToString(),
|
|
|
|
|
|
Status = uj.Status.ToString(),
|
|
|
|
|
|
CreatedAt = uj.CreatedAt
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(ex, "获取用户期刊列表失败,UserId: {UserId}", userId);
|
|
|
|
|
|
return [];
|
2026-06-03 16:09:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 更新用户状态
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<BaseResponse> UpdateStatusAsync(long id, UpdateUserStatusInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var exists = await Queryable().AnyAsync(u => u.Id == id);
|
|
|
|
|
|
if (!exists)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BaseResponse.Fail("用户不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 16:57:44 +08:00
|
|
|
|
var statusValue = input.Status == 1 ? UserStatusEnum.Active : UserStatusEnum.Disabled;
|
2026-06-03 16:09:56 +08:00
|
|
|
|
var result = await UpdateAsync(
|
2026-06-08 17:54:36 +08:00
|
|
|
|
u => new Users { Status = (int)statusValue },
|
2026-06-03 16:09:56 +08:00
|
|
|
|
u => u.Id == id
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return result ? BaseResponse.Success() : BaseResponse.Fail("更新失败");
|
|
|
|
|
|
}
|
2026-06-08 16:57:44 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 手动增加用户积分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<ManualPointsOutput> ManualAddPointsAsync(long userId, ManualAddPointsInput input, long operatorId, string operatorName, string? ipAddress = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation("管理员手动增加积分,UserId: {UserId}, Amount: {Amount}, Operator: {Operator}",
|
|
|
|
|
|
userId, input.Amount, operatorName);
|
|
|
|
|
|
|
|
|
|
|
|
// 校验用户是否存在
|
|
|
|
|
|
var user = await Queryable().Where(u => u.Id == userId && !u.IsDeleted).FirstAsync();
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
throw new BusinessException("用户不存在", 404);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用积分服务增加积分
|
|
|
|
|
|
var result = await pointsService.AddPointsAsync(new AddPointsInput
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = userId,
|
|
|
|
|
|
Amount = input.Amount,
|
|
|
|
|
|
ChangeType = PointsChangeTypeEnum.ManualAdjust,
|
|
|
|
|
|
Description = $"管理员手动增加: {input.Reason}",
|
|
|
|
|
|
OperatorName = operatorName
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 记录操作日志
|
|
|
|
|
|
var detail = JsonConvert.SerializeObject(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Amount = input.Amount,
|
|
|
|
|
|
Reason = input.Reason,
|
|
|
|
|
|
PreviousBalance = result.PreviousBalance,
|
|
|
|
|
|
NewBalance = result.NewBalance,
|
|
|
|
|
|
RecordId = result.RecordId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await operationLogService.LogAsync(
|
|
|
|
|
|
operatorId, operatorName,
|
|
|
|
|
|
OperationLogActionType.ManualAddPoints,
|
|
|
|
|
|
OperationLogTargetType.User,
|
|
|
|
|
|
userId, user.Name, detail, ipAddress);
|
|
|
|
|
|
|
|
|
|
|
|
return new ManualPointsOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RecordId = result.RecordId,
|
|
|
|
|
|
PreviousBalance = result.PreviousBalance,
|
|
|
|
|
|
NewBalance = result.NewBalance,
|
|
|
|
|
|
ChangeAmount = input.Amount,
|
|
|
|
|
|
OperatorName = operatorName,
|
|
|
|
|
|
OperatedAt = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 手动扣除用户积分
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<ManualPointsOutput> ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogInformation("管理员手动扣除积分,UserId: {UserId}, Amount: {Amount}, Operator: {Operator}",
|
|
|
|
|
|
userId, input.Amount, operatorName);
|
|
|
|
|
|
|
|
|
|
|
|
// 校验用户是否存在
|
|
|
|
|
|
var user = await Queryable().Where(u => u.Id == userId && !u.IsDeleted).FirstAsync();
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
throw new BusinessException("用户不存在", 404);
|
|
|
|
|
|
|
|
|
|
|
|
// 调用积分服务扣除积分
|
|
|
|
|
|
var result = await pointsService.DeductPointsAsync(new DeductPointsInput
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = userId,
|
|
|
|
|
|
Amount = input.Amount,
|
|
|
|
|
|
ChangeType = PointsChangeTypeEnum.ManualAdjust,
|
|
|
|
|
|
Description = $"管理员手动扣除: {input.Reason}",
|
|
|
|
|
|
OperatorName = operatorName
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 记录操作日志
|
|
|
|
|
|
var detail = JsonConvert.SerializeObject(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Amount = input.Amount,
|
|
|
|
|
|
Reason = input.Reason,
|
|
|
|
|
|
PreviousBalance = result.PreviousBalance,
|
|
|
|
|
|
NewBalance = result.NewBalance,
|
|
|
|
|
|
RecordId = result.RecordId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await operationLogService.LogAsync(
|
|
|
|
|
|
operatorId, operatorName,
|
|
|
|
|
|
OperationLogActionType.ManualDeductPoints,
|
|
|
|
|
|
OperationLogTargetType.User,
|
|
|
|
|
|
userId, user.Name, detail, ipAddress);
|
|
|
|
|
|
|
|
|
|
|
|
return new ManualPointsOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
RecordId = result.RecordId,
|
|
|
|
|
|
PreviousBalance = result.PreviousBalance,
|
|
|
|
|
|
NewBalance = result.NewBalance,
|
|
|
|
|
|
ChangeAmount = -input.Amount,
|
|
|
|
|
|
OperatorName = operatorName,
|
|
|
|
|
|
OperatedAt = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-06-03 16:09:56 +08:00
|
|
|
|
}
|