- 删除无数据库表的实体: 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配置
477 lines
16 KiB
C#
477 lines
16 KiB
C#
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 => "已取消",
|
||
_ => "未知"
|
||
};
|
||
}
|
||
}
|