mi-assessment/server/MiAssessment/src/MiAssessment.Core/Services/InviteService.cs
zpc 21e8ff5372 refactor: 清理遗留实体和无效代码
- 删除无数据库表的实体: UserDetail, UserAddress, PaymentOrder, Admin, AdminLoginLog, AdminOperationLog, Picture, Delivery
- 删除关联服务: AddressService, PaymentService, PaymentOrderService, PaymentRewardDispatcher, DefaultPaymentRewardHandler
- 删除关联接口: IAddressService, IPaymentService, IPaymentOrderService, IPaymentRewardHandler, IPaymentRewardDispatcher
- 删除关联控制器: AddressController
- 删除关联DTO: AddressModels, CreatePaymentOrderRequest, PaymentOrderDto, PaymentOrderQueryRequest
- 删除关联测试: PaymentOrderServicePropertyTests, PaymentRewardDispatcherPropertyTests
- 修复实体字段映射: User, UserLoginLog, UserRefreshToken, Config, OrderNotify
- 更新 NotifyController 移除 IPaymentOrderService 依赖
- 更新 ServiceModule 移除已删除服务的DI注册
- 更新 MiAssessmentDbContext 移除已删除实体的DbSet和OnModelCreating配置
2026-02-20 20:29:34 +08:00

477 lines
16 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 MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Models.Common;
using MiAssessment.Model.Models.Invite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 分销服务实现
/// </summary>
/// <remarks>
/// 提供分销模块的核心业务功能,包括:
/// - 邀请信息查询
/// - 小程序码生成
/// - 邀请记录查询
/// - 佣金信息查询
/// - 提现记录查询
/// - 提现申请处理
/// </remarks>
public class InviteService : IInviteService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<InviteService> _logger;
private readonly IWechatService _wechatService;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="logger">日志记录器</param>
/// <param name="wechatService">微信服务</param>
public InviteService(
MiAssessmentDbContext dbContext,
ILogger<InviteService> logger,
IWechatService wechatService)
{
_dbContext = dbContext;
_logger = logger;
_wechatService = wechatService;
}
/// <inheritdoc />
/// <summary>
/// 获取邀请信息
/// </summary>
/// <remarks>
/// 返回用户的邀请码、余额、邀请人数等信息。
/// Requirements: 11.1
/// </remarks>
public async Task<InviteInfoDto> GetInfoAsync(long userId)
{
_logger.LogDebug("获取邀请信息userId: {UserId}", userId);
// 查询用户信息
var user = await _dbContext.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new
{
u.InviteCode,
u.Balance,
u.WithdrawnAmount,
u.Uid
})
.FirstOrDefaultAsync();
if (user == null)
{
_logger.LogWarning("用户不存在userId: {UserId}", userId);
throw new InvalidOperationException("用户不存在");
}
// 统计邀请人数(直属下级)
var inviteCount = await _dbContext.Users
.AsNoTracking()
.CountAsync(u => u.ParentUserId == userId);
// 计算待提现金额(可提现余额)
var pendingAmount = user.Balance;
_logger.LogDebug("获取邀请信息成功userId: {UserId}, inviteCode: {InviteCode}, inviteCount: {InviteCount}",
userId, user.InviteCode, inviteCount);
return new InviteInfoDto
{
InviteUrl = $"pages/index/index?inviter={user.Uid}",
InviteCode = user.InviteCode ?? string.Empty,
WithdrawnAmount = user.WithdrawnAmount,
PendingAmount = pendingAmount,
InviteCount = inviteCount
};
}
/// <inheritdoc />
/// <summary>
/// 生成邀请二维码
/// </summary>
/// <remarks>
/// 调用微信小程序码接口生成带参数的二维码。
/// 二维码包含用户的邀请码参数,扫码后可识别邀请关系。
/// Requirements: 11.2
/// </remarks>
public async Task<QrcodeDto> GetQrcodeAsync(long userId)
{
_logger.LogDebug("生成邀请二维码userId: {UserId}", userId);
// 查询用户信息获取邀请码
var user = await _dbContext.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new { u.Uid, u.InviteCode })
.FirstOrDefaultAsync();
if (user == null)
{
_logger.LogWarning("用户不存在userId: {UserId}", userId);
throw new InvalidOperationException("用户不存在");
}
// 获取微信access_token
var accessToken = await _wechatService.GetAccessTokenAsync();
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogWarning("获取微信access_token失败userId: {UserId}", userId);
throw new InvalidOperationException("获取微信access_token失败");
}
// 生成小程序码
// 使用用户UID作为邀请参数
var scene = $"inviter={user.Uid}";
var page = "pages/index/index";
// 调用微信小程序码接口
// 这里返回一个占位URL实际实现需要调用微信API生成二维码图片
// 并上传到OSS或返回base64
var qrcodeUrl = $"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={accessToken}&scene={scene}&page={page}";
_logger.LogDebug("生成邀请二维码成功userId: {UserId}, scene: {Scene}", userId, scene);
return new QrcodeDto
{
QrcodeUrl = qrcodeUrl
};
}
/// <inheritdoc />
/// <summary>
/// 获取邀请记录列表
/// </summary>
/// <remarks>
/// 分页查询当前用户的直属下级用户列表及其贡献的佣金。
/// Requirements: 12.1
/// </remarks>
public async Task<PagedResult<InviteRecordDto>> GetRecordListAsync(long userId, int page, int pageSize)
{
_logger.LogDebug("获取邀请记录列表userId: {UserId}, page: {Page}, pageSize: {PageSize}",
userId, page, pageSize);
// 确保分页参数有效
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 20;
if (pageSize > 100) pageSize = 100;
// 查询直属下级用户Pid = userId
var query = _dbContext.Users
.AsNoTracking()
.Where(u => u.ParentUserId == userId);
// 获取总数
var total = await query.CountAsync();
// 分页查询下级用户
var invitedUsers = await query
.OrderByDescending(u => u.CreateTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(u => new
{
u.Id,
u.Nickname,
u.Avatar,
u.Uid,
u.CreateTime
})
.ToListAsync();
// 获取每个下级用户贡献的佣金总额
var userIds = invitedUsers.Select(u => u.Id).ToList();
var commissions = await _dbContext.Commissions
.AsNoTracking()
.Where(c => c.UserId == userId && userIds.Contains((int)c.FromUserId) && !c.IsDeleted)
.GroupBy(c => c.FromUserId)
.Select(g => new
{
FromUserId = g.Key,
TotalCommission = g.Sum(c => c.CommissionAmount)
})
.ToDictionaryAsync(x => x.FromUserId, x => x.TotalCommission);
// 组装结果
var records = invitedUsers.Select(u => new InviteRecordDto
{
UserId = u.Id,
Nickname = u.Nickname ?? "未设置昵称",
Avatar = u.Avatar ?? string.Empty,
Uid = u.Uid,
RegisterDate = u.CreateTime.ToString("yyyy-MM-dd"),
Commission = commissions.TryGetValue(u.Id, out var commission) ? commission : 0
}).ToList();
_logger.LogDebug("获取到 {Count} 条邀请记录,总数: {Total}", records.Count, total);
return PagedResult<InviteRecordDto>.Create(records, total, page, pageSize);
}
/// <inheritdoc />
/// <summary>
/// 获取佣金信息
/// </summary>
/// <remarks>
/// 返回用户的累计收益、已提现金额、待提现金额。
/// Requirements: 12.2
/// </remarks>
public async Task<CommissionInfoDto> GetCommissionAsync(long userId)
{
_logger.LogDebug("获取佣金信息userId: {UserId}", userId);
// 查询用户信息
var user = await _dbContext.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new
{
u.TotalIncome,
u.WithdrawnAmount,
u.Balance
})
.FirstOrDefaultAsync();
if (user == null)
{
_logger.LogWarning("用户不存在userId: {UserId}", userId);
throw new InvalidOperationException("用户不存在");
}
_logger.LogDebug("获取佣金信息成功userId: {UserId}, totalIncome: {TotalIncome}, withdrawnAmount: {WithdrawnAmount}, pendingAmount: {PendingAmount}",
userId, user.TotalIncome, user.WithdrawnAmount, user.Balance);
return new CommissionInfoDto
{
TotalIncome = user.TotalIncome,
WithdrawnAmount = user.WithdrawnAmount,
PendingAmount = user.Balance
};
}
/// <inheritdoc />
/// <summary>
/// 获取提现记录列表
/// </summary>
/// <remarks>
/// 分页查询当前用户的提现记录。
/// Requirements: 13.5
/// </remarks>
public async Task<PagedResult<WithdrawRecordDto>> GetWithdrawListAsync(long userId, int page, int pageSize)
{
_logger.LogDebug("获取提现记录列表userId: {UserId}, page: {Page}, pageSize: {PageSize}",
userId, page, pageSize);
// 确保分页参数有效
if (page < 1) page = 1;
if (pageSize < 1) pageSize = 20;
if (pageSize > 100) pageSize = 100;
// 查询用户的提现记录
var query = _dbContext.Withdrawals
.AsNoTracking()
.Where(w => w.UserId == userId && !w.IsDeleted);
// 获取总数
var total = await query.CountAsync();
// 分页查询
var withdrawals = await query
.OrderByDescending(w => w.CreateTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(w => new WithdrawRecordDto
{
Id = w.Id,
WithdrawalNo = w.WithdrawalNo,
Amount = w.Amount,
Status = w.Status,
StatusText = GetWithdrawStatusText(w.Status),
CreateTime = w.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"),
PayTime = w.PayTime.HasValue ? w.PayTime.Value.ToString("yyyy-MM-dd HH:mm:ss") : null
})
.ToListAsync();
_logger.LogDebug("获取到 {Count} 条提现记录,总数: {Total}", withdrawals.Count, total);
return PagedResult<WithdrawRecordDto>.Create(withdrawals, total, page, pageSize);
}
/// <inheritdoc />
/// <summary>
/// 申请提现
/// </summary>
/// <remarks>
/// 验证用户余额充足后创建提现记录并扣减用户余额。
/// 提现金额必须满足以下条件:
/// - 金额大于等于最低限额1元
/// - 金额必须为整数
/// - 金额不超过可提现余额
/// Requirements: 13.1, 13.2, 13.3, 13.4
/// </remarks>
public async Task<ApplyWithdrawResponse> ApplyWithdrawAsync(long userId, decimal amount)
{
_logger.LogDebug("申请提现userId: {UserId}, amount: {Amount}", userId, amount);
// 验证提现金额≥1元 (Requirement 13.2)
if (amount < 1)
{
_logger.LogWarning("提现金额小于最低限额userId: {UserId}, amount: {Amount}", userId, amount);
return new ApplyWithdrawResponse
{
Success = false,
ErrorMessage = "提现金额不能小于1元"
};
}
// 验证提现金额为整数 (Requirement 13.3)
if (amount != Math.Floor(amount))
{
_logger.LogWarning("提现金额不是整数userId: {UserId}, amount: {Amount}", userId, amount);
return new ApplyWithdrawResponse
{
Success = false,
ErrorMessage = "提现金额必须为整数"
};
}
// 使用事务确保数据一致性
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 查询用户信息(需要锁定记录以防止并发问题)
var user = await _dbContext.Users
.Where(u => u.Id == userId)
.FirstOrDefaultAsync();
if (user == null)
{
_logger.LogWarning("用户不存在userId: {UserId}", userId);
return new ApplyWithdrawResponse
{
Success = false,
ErrorMessage = "用户不存在"
};
}
// 验证余额充足 (Requirement 13.4)
if (amount > user.Balance)
{
_logger.LogWarning("提现金额超过可提现余额userId: {UserId}, amount: {Amount}, balance: {Balance}",
userId, amount, user.Balance);
return new ApplyWithdrawResponse
{
Success = false,
ErrorMessage = "超出待提现金额"
};
}
// 生成提现单号 (格式: W + yyyyMMdd + 6位序列号)
var withdrawalNo = await GenerateWithdrawalNoAsync();
// 记录提现前余额
var beforeBalance = user.Balance;
var afterBalance = beforeBalance - amount;
// 创建提现记录 (Requirement 13.1)
var withdrawal = new MiAssessment.Model.Entities.Withdrawal
{
WithdrawalNo = withdrawalNo,
UserId = userId,
Amount = amount,
BeforeBalance = beforeBalance,
AfterBalance = afterBalance,
Status = 1, // 申请中
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsDeleted = false
};
_dbContext.Withdrawals.Add(withdrawal);
// 扣减用户余额 (Requirement 13.1)
user.Balance = afterBalance;
user.UpdateTime = DateTime.Now;
// 保存更改
await _dbContext.SaveChangesAsync();
// 提交事务
await transaction.CommitAsync();
_logger.LogInformation("提现申请成功userId: {UserId}, withdrawalNo: {WithdrawalNo}, amount: {Amount}, beforeBalance: {BeforeBalance}, afterBalance: {AfterBalance}",
userId, withdrawalNo, amount, beforeBalance, afterBalance);
return new ApplyWithdrawResponse
{
Success = true,
WithdrawalNo = withdrawalNo
};
}
catch (Exception ex)
{
// 回滚事务
await transaction.RollbackAsync();
_logger.LogError(ex, "提现申请失败userId: {UserId}, amount: {Amount}", userId, amount);
throw;
}
}
/// <summary>
/// 生成提现单号
/// </summary>
/// <remarks>
/// 格式: W + yyyyMMdd + 6位序列号
/// 例如: W20240115000001
/// </remarks>
/// <returns>提现单号</returns>
private async Task<string> GenerateWithdrawalNoAsync()
{
var datePrefix = $"W{DateTime.Now:yyyyMMdd}";
// 查询当天最大的提现单号
var maxNo = await _dbContext.Withdrawals
.AsNoTracking()
.Where(w => w.WithdrawalNo.StartsWith(datePrefix))
.OrderByDescending(w => w.WithdrawalNo)
.Select(w => w.WithdrawalNo)
.FirstOrDefaultAsync();
int sequence = 1;
if (!string.IsNullOrEmpty(maxNo) && maxNo.Length == 15)
{
// 提取序列号部分并加1
if (int.TryParse(maxNo.Substring(9), out var lastSequence))
{
sequence = lastSequence + 1;
}
}
return $"{datePrefix}{sequence:D6}";
}
/// <summary>
/// 获取提现状态文本
/// </summary>
/// <param name="status">状态值</param>
/// <returns>状态文本</returns>
private static string GetWithdrawStatusText(int status)
{
return status switch
{
1 => "申请中",
2 => "提现中",
3 => "已提现",
4 => "已取消",
_ => "未知"
};
}
}