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配置
This commit is contained in:
zpc 2026-02-20 20:29:34 +08:00
parent a595eee90d
commit 21e8ff5372
41 changed files with 317 additions and 4188 deletions

View File

@ -36,7 +36,7 @@ public class DashboardService : IDashboardService
// 今日注册用户数
var todayRegistrations = await _dbContext.Users
.CountAsync(u => u.CreatedAt >= today && u.CreatedAt < tomorrow);
.CountAsync(u => u.CreateTime >= today && u.CreateTime < tomorrow);
// 总用户数
var totalUsers = await _dbContext.Users.CountAsync();

View File

@ -1,350 +0,0 @@
using System.Security.Claims;
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Base;
using MiAssessment.Model.Models.Address;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MiAssessment.Api.Controllers;
/// <summary>
/// 地址控制器 - 处理用户收货地址相关功能
/// </summary>
/// <remarks>
/// 提供地址的增删改查、设置默认地址等功能
/// Requirements: 1.1-1.7
/// </remarks>
[ApiController]
[Route("api")]
public class AddressController : ControllerBase
{
private readonly IAddressService _addressService;
private readonly ILogger<AddressController> _logger;
public AddressController(IAddressService addressService, ILogger<AddressController> logger)
{
_addressService = addressService;
_logger = logger;
}
/// <summary>
/// 添加收货地址
/// </summary>
/// <remarks>
/// POST /api/addAddress
///
/// 每位用户最多只能添加10条收货地址
/// Requirements: 1.1
/// </remarks>
[HttpPost("addAddress")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse<AddressDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<AddressDto>> AddAddress([FromBody] AddAddressRequest request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<AddressDto>.Unauthorized();
}
// 参数验证
if (string.IsNullOrWhiteSpace(request.ReceiverName))
{
return ApiResponse<AddressDto>.Fail("请输入收货人姓名");
}
if (string.IsNullOrWhiteSpace(request.ReceiverPhone))
{
return ApiResponse<AddressDto>.Fail("请输入收货人电话");
}
if (!IsValidMobile(request.ReceiverPhone))
{
return ApiResponse<AddressDto>.Fail("请输入正确的手机号");
}
if (string.IsNullOrWhiteSpace(request.DetailedAddress))
{
return ApiResponse<AddressDto>.Fail("请输入详细地址");
}
try
{
var address = await _addressService.AddAddressAsync(userId.Value, request);
return ApiResponse<AddressDto>.Success(address, "添加成功");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Add address failed: UserId={UserId}, Error={Error}", userId, ex.Message);
return ApiResponse<AddressDto>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to add address: UserId={UserId}", userId);
return ApiResponse<AddressDto>.Fail("添加失败");
}
}
/// <summary>
/// 更新收货地址
/// </summary>
/// <remarks>
/// POST /api/updateAddress
/// Requirements: 1.2
/// </remarks>
[HttpPost("updateAddress")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse<AddressDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<AddressDto>> UpdateAddress([FromBody] UpdateAddressRequest request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<AddressDto>.Unauthorized();
}
// 参数验证
if (request.Id <= 0)
{
return ApiResponse<AddressDto>.Fail("请选择要修改的地址");
}
if (string.IsNullOrWhiteSpace(request.ReceiverName))
{
return ApiResponse<AddressDto>.Fail("请输入收货人姓名");
}
if (string.IsNullOrWhiteSpace(request.ReceiverPhone))
{
return ApiResponse<AddressDto>.Fail("请输入收货人电话");
}
if (!IsValidMobile(request.ReceiverPhone))
{
return ApiResponse<AddressDto>.Fail("请输入正确的手机号");
}
if (string.IsNullOrWhiteSpace(request.DetailedAddress))
{
return ApiResponse<AddressDto>.Fail("请输入详细地址");
}
try
{
var address = await _addressService.UpdateAddressAsync(userId.Value, request);
return ApiResponse<AddressDto>.Success(address, "修改成功");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Update address failed: UserId={UserId}, Error={Error}", userId, ex.Message);
return ApiResponse<AddressDto>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update address: UserId={UserId}", userId);
return ApiResponse<AddressDto>.Fail("修改失败");
}
}
/// <summary>
/// 获取默认收货地址
/// </summary>
/// <remarks>
/// GET /api/getDefaultAddress
/// Requirements: 1.3
/// </remarks>
[HttpGet("getDefaultAddress")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse<AddressDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<AddressDto?>> GetDefaultAddress()
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<AddressDto?>.Unauthorized();
}
try
{
var address = await _addressService.GetDefaultAddressAsync(userId.Value);
return ApiResponse<AddressDto?>.Success(address, "获取成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get default address: UserId={UserId}", userId);
return ApiResponse<AddressDto?>.Fail("获取失败");
}
}
/// <summary>
/// 获取收货地址列表
/// </summary>
/// <remarks>
/// GET /api/getAddressList
/// Requirements: 1.4
/// </remarks>
[HttpGet("getAddressList")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse<List<AddressDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<List<AddressDto>>> GetAddressList()
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<List<AddressDto>>.Unauthorized();
}
try
{
var addresses = await _addressService.GetAddressListAsync(userId.Value);
return ApiResponse<List<AddressDto>>.Success(addresses, "获取成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get address list: UserId={UserId}", userId);
return ApiResponse<List<AddressDto>>.Fail("获取失败");
}
}
/// <summary>
/// 删除收货地址
/// </summary>
/// <remarks>
/// POST /api/deleteAddress
/// Requirements: 1.5
/// </remarks>
[HttpPost("deleteAddress")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task<ApiResponse> DeleteAddress([FromBody] AddressIdRequest request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse.Unauthorized();
}
if (request.Id <= 0)
{
return ApiResponse.Fail("请选择要删除的地址");
}
try
{
await _addressService.DeleteAddressAsync(userId.Value, request.Id);
return ApiResponse.Success("删除成功");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Delete address failed: UserId={UserId}, Error={Error}", userId, ex.Message);
return ApiResponse.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete address: UserId={UserId}", userId);
return ApiResponse.Fail("删除失败");
}
}
/// <summary>
/// 设置默认收货地址
/// </summary>
/// <remarks>
/// POST /api/setDefaultAddress
/// Requirements: 1.6
/// </remarks>
[HttpPost("setDefaultAddress")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
public async Task<ApiResponse> SetDefaultAddress([FromBody] AddressIdRequest request)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse.Unauthorized();
}
if (request.Id <= 0)
{
return ApiResponse.Fail("请选择要设为默认的地址");
}
try
{
await _addressService.SetDefaultAddressAsync(userId.Value, request.Id);
return ApiResponse.Success("设置成功");
}
catch (InvalidOperationException ex)
{
_logger.LogWarning("Set default address failed: UserId={UserId}, Error={Error}", userId, ex.Message);
return ApiResponse.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to set default address: UserId={UserId}", userId);
return ApiResponse.Fail("设置失败");
}
}
/// <summary>
/// 获取地址详情
/// </summary>
/// <remarks>
/// GET /api/getAddressDetail
/// Requirements: 1.7
/// </remarks>
[HttpGet("getAddressDetail")]
[Authorize]
[ProducesResponseType(typeof(ApiResponse<AddressDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<AddressDto?>> GetAddressDetail([FromQuery] int id)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return ApiResponse<AddressDto?>.Unauthorized();
}
if (id <= 0)
{
return ApiResponse<AddressDto?>.Fail("请选择要查看的地址");
}
try
{
var address = await _addressService.GetAddressDetailAsync(userId.Value, id);
if (address == null)
{
return ApiResponse<AddressDto?>.Fail("地址不存在");
}
return ApiResponse<AddressDto?>.Success(address, "获取成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get address detail: UserId={UserId}, AddressId={AddressId}", userId, id);
return ApiResponse<AddressDto?>.Fail("获取失败");
}
}
#region Private Helper Methods
/// <summary>
/// 获取当前登录用户ID
/// </summary>
private int? GetCurrentUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId))
{
return null;
}
return userId;
}
/// <summary>
/// 验证手机号格式
/// </summary>
private static bool IsValidMobile(string mobile)
{
if (string.IsNullOrWhiteSpace(mobile))
return false;
// 中国大陆手机号11位数字以1开头
return mobile.Length == 11 && mobile.StartsWith("1") && mobile.All(char.IsDigit);
}
#endregion
}

View File

@ -13,16 +13,13 @@ namespace MiAssessment.Api.Controllers;
public class NotifyController : ControllerBase
{
private readonly IPaymentNotifyService _paymentNotifyService;
private readonly IPaymentOrderService _paymentOrderService;
private readonly ILogger<NotifyController> _logger;
public NotifyController(
IPaymentNotifyService paymentNotifyService,
IPaymentOrderService paymentOrderService,
ILogger<NotifyController> logger)
{
_paymentNotifyService = paymentNotifyService;
_paymentOrderService = paymentOrderService;
_logger = logger;
}
@ -67,48 +64,6 @@ public class NotifyController : ControllerBase
_logger.LogInformation("微信支付回调处理完成: Success={Success}, Message={Message}",
result.Success, result.Message);
// 如果回调处理成功且有订单号和支付数据,调用 PaymentOrderService 处理支付成功
if (result.Success && !string.IsNullOrEmpty(result.OrderNo) && result.NotifyData != null)
{
try
{
// 从回调数据中获取交易号和支付金额(分转元)
var transactionId = result.NotifyData.TransactionId;
var payAmount = result.NotifyData.TotalFee / 100m;
_logger.LogInformation("开始处理支付订单: OrderNo={OrderNo}, TransactionId={TransactionId}, PayAmount={PayAmount}",
result.OrderNo, transactionId, payAmount);
// 调用 PaymentOrderService 处理支付成功(更新 PaymentOrder 状态并触发奖励发放)
var paymentResult = await _paymentOrderService.HandlePaymentSuccessAsync(
result.OrderNo,
transactionId,
payAmount);
if (paymentResult)
{
_logger.LogInformation("支付订单处理成功: OrderNo={OrderNo}", result.OrderNo);
// 更新 OrderNotify 状态为处理成功
await _paymentNotifyService.UpdateNotifyStatusAsync(result.OrderNo, 1, "处理成功");
}
else
{
_logger.LogWarning("支付订单处理失败: OrderNo={OrderNo}", result.OrderNo);
// 更新 OrderNotify 状态为处理失败
await _paymentNotifyService.UpdateNotifyStatusAsync(result.OrderNo, 2, "支付订单处理失败");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理支付订单异常: OrderNo={OrderNo}", result.OrderNo);
// 更新 OrderNotify 状态为处理失败
await _paymentNotifyService.UpdateNotifyStatusAsync(result.OrderNo, 2, $"处理异常: {ex.Message}");
}
}
// 根据回调版本返回对应格式的响应
if (!string.IsNullOrEmpty(result.JsonResponse))
{

View File

@ -1,63 +0,0 @@
using MiAssessment.Model.Models.Address;
namespace MiAssessment.Core.Interfaces;
/// <summary>
/// 地址服务接口
/// </summary>
public interface IAddressService
{
/// <summary>
/// 添加收货地址
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="request">添加地址请求</param>
/// <returns>新创建的地址</returns>
Task<AddressDto> AddAddressAsync(int userId, AddAddressRequest request);
/// <summary>
/// 更新收货地址
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="request">更新地址请求</param>
/// <returns>更新后的地址</returns>
Task<AddressDto> UpdateAddressAsync(int userId, UpdateAddressRequest request);
/// <summary>
/// 获取默认收货地址
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>默认地址,如果没有则返回最新的一条</returns>
Task<AddressDto?> GetDefaultAddressAsync(int userId);
/// <summary>
/// 获取收货地址列表
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>地址列表</returns>
Task<List<AddressDto>> GetAddressListAsync(int userId);
/// <summary>
/// 删除收货地址
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="addressId">地址ID</param>
/// <returns>是否删除成功</returns>
Task<bool> DeleteAddressAsync(int userId, int addressId);
/// <summary>
/// 设置默认收货地址
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="addressId">地址ID</param>
/// <returns>是否设置成功</returns>
Task<bool> SetDefaultAddressAsync(int userId, int addressId);
/// <summary>
/// 获取地址详情
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="addressId">地址ID</param>
/// <returns>地址详情</returns>
Task<AddressDto?> GetAddressDetailAsync(int userId, int addressId);
}

View File

@ -1,72 +0,0 @@
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models;
using MiAssessment.Model.Models.Payment;
namespace MiAssessment.Core.Interfaces;
/// <summary>
/// 通用支付订单服务接口
/// </summary>
public interface IPaymentOrderService
{
/// <summary>
/// 创建支付订单
/// </summary>
/// <param name="request">创建订单请求</param>
/// <returns>创建的支付订单</returns>
Task<PaymentOrder> CreateOrderAsync(CreatePaymentOrderRequest request);
/// <summary>
/// 根据订单号获取订单详情
/// </summary>
/// <param name="orderNo">订单号</param>
/// <returns>支付订单如果不存在则返回null</returns>
Task<PaymentOrder?> GetOrderByNoAsync(string orderNo);
/// <summary>
/// 根据订单ID获取订单详情
/// </summary>
/// <param name="orderId">订单ID</param>
/// <returns>支付订单如果不存在则返回null</returns>
Task<PaymentOrder?> GetOrderByIdAsync(int orderId);
/// <summary>
/// 处理支付成功
/// </summary>
/// <param name="orderNo">订单号</param>
/// <param name="transactionId">第三方交易号</param>
/// <param name="payAmount">实付金额</param>
/// <returns>是否处理成功</returns>
Task<bool> HandlePaymentSuccessAsync(string orderNo, string transactionId, decimal payAmount);
/// <summary>
/// 处理奖励发放
/// </summary>
/// <param name="orderNo">订单号</param>
/// <returns>是否处理成功</returns>
Task<bool> ProcessRewardAsync(string orderNo);
/// <summary>
/// 获取用户订单列表
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="request">查询请求</param>
/// <returns>分页订单列表</returns>
Task<PageResponse<PaymentOrderDto>> GetUserOrdersAsync(int userId, PaymentOrderQueryRequest request);
/// <summary>
/// 取消订单
/// </summary>
/// <param name="orderNo">订单号</param>
/// <param name="userId">用户ID用于验证权限</param>
/// <returns>是否取消成功</returns>
Task<bool> CancelOrderAsync(string orderNo, int userId);
/// <summary>
/// 更新订单状态
/// </summary>
/// <param name="orderNo">订单号</param>
/// <param name="status">新状态</param>
/// <returns>是否更新成功</returns>
Task<bool> UpdateOrderStatusAsync(string orderNo, byte status);
}

View File

@ -1,38 +0,0 @@
using MiAssessment.Model.Entities;
namespace MiAssessment.Core.Interfaces;
/// <summary>
/// 支付奖励分发器接口
/// 负责根据订单类型查找并调用对应的奖励处理器
/// </summary>
public interface IPaymentRewardDispatcher
{
/// <summary>
/// 根据订单类型获取对应的奖励处理器
/// </summary>
/// <param name="orderType">订单类型</param>
/// <returns>奖励处理器,如果未找到则返回 null</returns>
IPaymentRewardHandler? GetHandler(string orderType);
/// <summary>
/// 检查是否存在指定订单类型的处理器
/// </summary>
/// <param name="orderType">订单类型</param>
/// <returns>是否存在处理器</returns>
bool HasHandler(string orderType);
/// <summary>
/// 获取所有已注册的订单类型
/// </summary>
/// <returns>已注册的订单类型列表</returns>
IReadOnlyCollection<string> GetRegisteredOrderTypes();
/// <summary>
/// 处理奖励发放
/// 根据订单类型查找对应的处理器并执行奖励发放
/// </summary>
/// <param name="order">支付订单</param>
/// <returns>奖励处理结果</returns>
Task<RewardResult> ProcessRewardAsync(PaymentOrder order);
}

View File

@ -1,71 +0,0 @@
using MiAssessment.Model.Entities;
namespace MiAssessment.Core.Interfaces;
/// <summary>
/// 支付奖励处理器接口
/// 用于处理支付成功后的奖励发放逻辑
/// </summary>
public interface IPaymentRewardHandler
{
/// <summary>
/// 处理的订单类型
/// </summary>
string OrderType { get; }
/// <summary>
/// 处理奖励发放
/// </summary>
/// <param name="order">支付订单</param>
/// <returns>奖励处理结果</returns>
Task<RewardResult> ProcessRewardAsync(PaymentOrder order);
}
/// <summary>
/// 奖励处理结果
/// </summary>
public class RewardResult
{
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 消息(成功时为空,失败时为错误原因)
/// </summary>
public string? Message { get; set; }
/// <summary>
/// 奖励数据JSON格式
/// </summary>
public string? RewardData { get; set; }
/// <summary>
/// 创建成功结果
/// </summary>
/// <param name="rewardData">奖励数据</param>
/// <returns>成功结果</returns>
public static RewardResult Ok(string? rewardData = null)
{
return new RewardResult
{
Success = true,
RewardData = rewardData
};
}
/// <summary>
/// 创建失败结果
/// </summary>
/// <param name="message">错误消息</param>
/// <returns>失败结果</returns>
public static RewardResult Fail(string message)
{
return new RewardResult
{
Success = false,
Message = message
};
}
}

View File

@ -1,25 +0,0 @@
using MiAssessment.Model.Models.Payment;
namespace MiAssessment.Core.Interfaces;
/// <summary>
/// 支付服务接口
/// 提供基础的支付相关功能,具体业务逻辑由业务模块扩展
/// </summary>
public interface IPaymentService
{
/// <summary>
/// 验证用户余额是否充足
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="amount">需要的金额</param>
/// <returns>是否充足</returns>
Task<bool> ValidateBalanceAsync(int userId, decimal amount);
/// <summary>
/// 获取用户余额
/// </summary>
/// <param name="userId">用户ID</param>
/// <returns>用户余额</returns>
Task<decimal> GetUserBalanceAsync(int userId);
}

View File

@ -1,221 +0,0 @@
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models.Address;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 地址服务实现
/// </summary>
public class AddressService : IAddressService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<AddressService> _logger;
private const int MaxAddressCount = 10;
public AddressService(MiAssessmentDbContext dbContext, ILogger<AddressService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <inheritdoc />
public async Task<AddressDto> AddAddressAsync(int userId, AddAddressRequest request)
{
// 检查地址数量限制
var addressCount = await _dbContext.UserAddresses
.CountAsync(a => a.UserId == userId && a.IsDeleted == 0);
if (addressCount >= MaxAddressCount)
{
throw new InvalidOperationException("最多只能添加10个收货地址");
}
var now = DateTime.Now;
var address = new UserAddress
{
UserId = userId,
ReceiverName = request.ReceiverName,
ReceiverPhone = request.ReceiverPhone,
DetailedAddress = request.DetailedAddress,
IsDefault = (byte)(request.IsDefault == 1 ? 1 : 0),
IsDeleted = 0,
CreatedAt = now,
UpdatedAt = now
};
// 如果设置为默认地址,将其他地址设为非默认
if (address.IsDefault == 1)
{
await SetOtherAddressNotDefaultAsync(userId);
}
// 如果是第一个地址,自动设为默认
if (addressCount == 0)
{
address.IsDefault = 1;
}
_dbContext.UserAddresses.Add(address);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Address added: UserId={UserId}, AddressId={AddressId}", userId, address.Id);
return MapToDto(address);
}
/// <inheritdoc />
public async Task<AddressDto> UpdateAddressAsync(int userId, UpdateAddressRequest request)
{
var address = await _dbContext.UserAddresses
.FirstOrDefaultAsync(a => a.Id == request.Id && a.UserId == userId && a.IsDeleted == 0);
if (address == null)
{
throw new InvalidOperationException("地址不存在");
}
address.ReceiverName = request.ReceiverName;
address.ReceiverPhone = request.ReceiverPhone;
address.DetailedAddress = request.DetailedAddress;
address.UpdatedAt = DateTime.Now;
// 处理默认地址设置
if (request.IsDefault.HasValue && request.IsDefault.Value == 1 && address.IsDefault != 1)
{
await SetOtherAddressNotDefaultAsync(userId, address.Id);
address.IsDefault = 1;
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Address updated: UserId={UserId}, AddressId={AddressId}", userId, address.Id);
return MapToDto(address);
}
/// <inheritdoc />
public async Task<AddressDto?> GetDefaultAddressAsync(int userId)
{
// 先查找默认地址
var address = await _dbContext.UserAddresses
.FirstOrDefaultAsync(a => a.UserId == userId && a.IsDefault == 1 && a.IsDeleted == 0);
// 如果没有默认地址,返回最新添加的一条
if (address == null)
{
address = await _dbContext.UserAddresses
.Where(a => a.UserId == userId && a.IsDeleted == 0)
.OrderByDescending(a => a.Id)
.FirstOrDefaultAsync();
}
return address != null ? MapToDto(address) : null;
}
/// <inheritdoc />
public async Task<List<AddressDto>> GetAddressListAsync(int userId)
{
var addresses = await _dbContext.UserAddresses
.Where(a => a.UserId == userId && a.IsDeleted == 0)
.OrderByDescending(a => a.IsDefault)
.ThenByDescending(a => a.Id)
.ToListAsync();
return addresses.Select(MapToDto).ToList();
}
/// <inheritdoc />
public async Task<bool> DeleteAddressAsync(int userId, int addressId)
{
var address = await _dbContext.UserAddresses
.FirstOrDefaultAsync(a => a.Id == addressId && a.UserId == userId && a.IsDeleted == 0);
if (address == null)
{
throw new InvalidOperationException("地址不存在");
}
// 软删除
address.IsDeleted = 1;
address.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Address deleted: UserId={UserId}, AddressId={AddressId}", userId, addressId);
return true;
}
/// <inheritdoc />
public async Task<bool> SetDefaultAddressAsync(int userId, int addressId)
{
var address = await _dbContext.UserAddresses
.FirstOrDefaultAsync(a => a.Id == addressId && a.UserId == userId && a.IsDeleted == 0);
if (address == null)
{
throw new InvalidOperationException("地址不存在");
}
// 已经是默认地址
if (address.IsDefault == 1)
{
return true;
}
// 将其他地址设为非默认
await SetOtherAddressNotDefaultAsync(userId);
// 设置当前地址为默认
address.IsDefault = 1;
address.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Default address set: UserId={UserId}, AddressId={AddressId}", userId, addressId);
return true;
}
/// <inheritdoc />
public async Task<AddressDto?> GetAddressDetailAsync(int userId, int addressId)
{
var address = await _dbContext.UserAddresses
.FirstOrDefaultAsync(a => a.Id == addressId && a.UserId == userId && a.IsDeleted == 0);
return address != null ? MapToDto(address) : null;
}
/// <summary>
/// 将其他地址设为非默认
/// </summary>
private async Task SetOtherAddressNotDefaultAsync(int userId, int? exceptId = null)
{
var query = _dbContext.UserAddresses
.Where(a => a.UserId == userId && a.IsDefault == 1 && a.IsDeleted == 0);
if (exceptId.HasValue)
{
query = query.Where(a => a.Id != exceptId.Value);
}
await query.ExecuteUpdateAsync(s => s.SetProperty(a => a.IsDefault, (byte)0));
}
/// <summary>
/// 将实体映射为DTO
/// </summary>
private static AddressDto MapToDto(UserAddress address)
{
return new AddressDto
{
Id = address.Id,
UserId = address.UserId,
ReceiverName = address.ReceiverName,
ReceiverPhone = address.ReceiverPhone,
DetailedAddress = address.DetailedAddress,
IsDefault = address.IsDefault ?? 0,
CreateTime = address.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss"),
UpdateTime = address.UpdatedAt.ToString("yyyy-MM-dd HH:mm:ss")
};
}
}

View File

@ -392,37 +392,25 @@ public class AuthService : IAuthService
// 获取客户端IP这里使用空字符串作为占位符实际IP应从Controller传入
var clientIp = deviceInfo ?? string.Empty;
// 6.2 解析IP地址获取地理位置
IpLocationResult? locationResult = null;
if (!string.IsNullOrWhiteSpace(clientIp))
{
locationResult = await _ipLocationService.GetLocationAsync(clientIp);
}
var now = DateTime.UtcNow;
var today = DateOnly.FromDateTime(now);
var now = DateTime.Now;
// 6.1 记录登录日志
var loginLog = new UserLoginLog
{
UserId = userId,
LoginDate = today,
LoginTime = now,
LastLoginTime = now,
Device = device,
Ip = clientIp,
Location = locationResult?.Success == true
? $"{locationResult.Province}{locationResult.City}"
: null,
Year = now.Year,
Month = now.Month,
Week = GetWeekOfYear(now)
LoginType = "wechat",
LoginIp = clientIp,
UserAgent = device,
Platform = "miniprogram",
Status = 1,
CreateTime = now
};
await _dbContext.UserLoginLogs.AddAsync(loginLog);
// 更新用户最后登录时间
user.LastLoginTime = now;
user.LastLoginIp = clientIp;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
@ -434,7 +422,7 @@ public class AuthService : IAuthService
{
Uid = user.Uid,
Nickname = user.Nickname,
Headimg = user.HeadImg
Headimg = user.Avatar
};
}
catch (Exception ex)
@ -802,7 +790,7 @@ public class AuthService : IAuthService
{
mobileUser.UnionId = currentUser.UnionId;
}
mobileUser.UpdatedAt = DateTime.UtcNow;
mobileUser.UpdateTime = DateTime.Now;
_dbContext.Users.Update(mobileUser);
// 删除当前用户

View File

@ -1,85 +0,0 @@
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Entities;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 默认支付奖励处理器示例
/// 用于演示如何实现自定义奖励处理器
///
/// 使用方法:
/// 1. 创建一个新类实现 IPaymentRewardHandler 接口
/// 2. 设置 OrderType 属性为要处理的订单类型
/// 3. 在 ProcessRewardAsync 方法中实现奖励发放逻辑
/// 4. 在 ServiceModule.cs 中注册处理器
///
/// 注册示例:
/// <code>
/// builder.RegisterType&lt;MyRewardHandler&gt;()
/// .As&lt;IPaymentRewardHandler&gt;()
/// .InstancePerLifetimeScope();
/// </code>
/// </summary>
/// <example>
/// 实现钻石充值奖励处理器示例:
/// <code>
/// public class DiamondRechargeRewardHandler : IPaymentRewardHandler
/// {
/// public string OrderType => "diamond_recharge";
///
/// public async Task&lt;RewardResult&gt; ProcessRewardAsync(PaymentOrder order)
/// {
/// // 1. 解析业务数据
/// var bizData = JsonSerializer.Deserialize&lt;DiamondRechargeData&gt;(order.BizData);
///
/// // 2. 发放钻石
/// await _userService.AddDiamondsAsync(order.UserId, bizData.DiamondAmount);
///
/// // 3. 返回结果
/// return RewardResult.Ok(JsonSerializer.Serialize(new { diamonds = bizData.DiamondAmount }));
/// }
/// }
/// </code>
/// </example>
public class DefaultPaymentRewardHandler : IPaymentRewardHandler
{
private readonly ILogger<DefaultPaymentRewardHandler> _logger;
/// <summary>
/// 处理的订单类型
/// 默认处理器使用 "default" 类型,实际项目中应替换为具体的业务类型
/// </summary>
public string OrderType => "default";
public DefaultPaymentRewardHandler(ILogger<DefaultPaymentRewardHandler> logger)
{
_logger = logger;
}
/// <summary>
/// 处理奖励发放
/// 默认实现仅记录日志,实际项目中应实现具体的奖励逻辑
/// </summary>
/// <param name="order">支付订单</param>
/// <returns>奖励处理结果</returns>
public Task<RewardResult> ProcessRewardAsync(PaymentOrder order)
{
_logger.LogInformation(
"默认奖励处理器被调用: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}, BizData={BizData}",
order.OrderNo,
order.UserId,
order.Amount,
order.BizData);
// 默认处理器直接返回成功
// 实际项目中应根据 order.BizData 中的业务数据执行具体的奖励逻辑
var rewardData = System.Text.Json.JsonSerializer.Serialize(new
{
message = "默认奖励处理完成",
processedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
});
return Task.FromResult(RewardResult.Ok(rewardData));
}
}

View File

@ -75,7 +75,7 @@ public class InviteService : IInviteService
// 统计邀请人数(直属下级)
var inviteCount = await _dbContext.Users
.AsNoTracking()
.CountAsync(u => u.Pid == userId);
.CountAsync(u => u.ParentUserId == userId);
// 计算待提现金额(可提现余额)
var pendingAmount = user.Balance;
@ -166,23 +166,23 @@ public class InviteService : IInviteService
// 查询直属下级用户Pid = userId
var query = _dbContext.Users
.AsNoTracking()
.Where(u => u.Pid == userId);
.Where(u => u.ParentUserId == userId);
// 获取总数
var total = await query.CountAsync();
// 分页查询下级用户
var invitedUsers = await query
.OrderByDescending(u => u.CreatedAt)
.OrderByDescending(u => u.CreateTime)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(u => new
{
u.Id,
u.Nickname,
u.HeadImg,
u.Avatar,
u.Uid,
u.CreatedAt
u.CreateTime
})
.ToListAsync();
@ -204,9 +204,9 @@ public class InviteService : IInviteService
{
UserId = u.Id,
Nickname = u.Nickname ?? "未设置昵称",
Avatar = u.HeadImg ?? string.Empty,
Avatar = u.Avatar ?? string.Empty,
Uid = u.Uid,
RegisterDate = u.CreatedAt.ToString("yyyy-MM-dd"),
RegisterDate = u.CreateTime.ToString("yyyy-MM-dd"),
Commission = commissions.TryGetValue(u.Id, out var commission) ? commission : 0
}).ToList();
@ -398,7 +398,7 @@ public class InviteService : IInviteService
// 扣减用户余额 (Requirement 13.1)
user.Balance = afterBalance;
user.UpdatedAt = DateTime.Now;
user.UpdateTime = DateTime.Now;
// 保存更改
await _dbContext.SaveChangesAsync();

View File

@ -367,7 +367,7 @@ public class PaymentNotifyService : IPaymentNotifyService
existingNotify.PayTime = DateTime.Now;
existingNotify.PayAmount = notifyData.TotalFee / 100m;
existingNotify.RawData = null; // 可选存储原始XML
existingNotify.UpdatedAt = DateTime.Now;
existingNotify.UpdateTime = DateTime.Now;
}
else
{
@ -383,8 +383,8 @@ public class PaymentNotifyService : IPaymentNotifyService
Attach = notifyData.Attach,
OpenId = notifyData.OpenId,
RawData = null,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.OrderNotifies.Add(notify);
@ -412,7 +412,7 @@ public class PaymentNotifyService : IPaymentNotifyService
{
notify.Status = status;
notify.ErrorMessage = message;
notify.UpdatedAt = DateTime.Now;
notify.UpdateTime = DateTime.Now;
await _dbContext.SaveChangesAsync();
return true;
}

View File

@ -1,389 +0,0 @@
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models;
using MiAssessment.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 通用支付订单服务实现
/// </summary>
public class PaymentOrderService : IPaymentOrderService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly IEnumerable<IPaymentRewardHandler> _rewardHandlers;
private readonly ILogger<PaymentOrderService> _logger;
public PaymentOrderService(
MiAssessmentDbContext dbContext,
IEnumerable<IPaymentRewardHandler> rewardHandlers,
ILogger<PaymentOrderService> logger)
{
_dbContext = dbContext;
_rewardHandlers = rewardHandlers;
_logger = logger;
}
/// <inheritdoc />
public async Task<PaymentOrder> CreateOrderAsync(CreatePaymentOrderRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.UserId <= 0)
throw new ArgumentException("用户ID无效", nameof(request));
if (string.IsNullOrWhiteSpace(request.OrderType))
throw new ArgumentException("订单类型不能为空", nameof(request));
if (request.Amount <= 0)
throw new ArgumentException("订单金额必须大于0", nameof(request));
// 生成唯一订单号
var orderNo = GenerateOrderNo();
var order = new PaymentOrder
{
OrderNo = orderNo,
UserId = request.UserId,
OrderType = request.OrderType,
Title = request.Title ?? string.Empty,
Amount = request.Amount,
PayAmount = request.PayAmount ?? request.Amount,
PayMethod = request.PayMethod,
Status = 0, // 待支付
BizId = request.BizId,
BizData = request.BizData,
RewardStatus = 0, // 未发放
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
_dbContext.PaymentOrders.Add(order);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("创建支付订单成功: OrderNo={OrderNo}, UserId={UserId}, OrderType={OrderType}, Amount={Amount}",
orderNo, request.UserId, request.OrderType, request.Amount);
return order;
}
/// <inheritdoc />
public async Task<PaymentOrder?> GetOrderByNoAsync(string orderNo)
{
if (string.IsNullOrWhiteSpace(orderNo))
return null;
return await _dbContext.PaymentOrders
.FirstOrDefaultAsync(o => o.OrderNo == orderNo);
}
/// <inheritdoc />
public async Task<PaymentOrder?> GetOrderByIdAsync(int orderId)
{
if (orderId <= 0)
return null;
return await _dbContext.PaymentOrders
.FirstOrDefaultAsync(o => o.Id == orderId);
}
/// <inheritdoc />
public async Task<bool> HandlePaymentSuccessAsync(string orderNo, string transactionId, decimal payAmount)
{
if (string.IsNullOrWhiteSpace(orderNo))
{
_logger.LogWarning("处理支付成功失败: 订单号为空");
return false;
}
var order = await GetOrderByNoAsync(orderNo);
if (order == null)
{
_logger.LogWarning("处理支付成功失败: 订单不存在, OrderNo={OrderNo}", orderNo);
return false;
}
// 幂等性检查:如果订单已支付,直接返回成功
if (order.Status == 1)
{
_logger.LogInformation("订单已支付,跳过重复处理: OrderNo={OrderNo}", orderNo);
return true;
}
// 只有待支付状态的订单才能处理
if (order.Status != 0)
{
_logger.LogWarning("订单状态不正确,无法处理支付成功: OrderNo={OrderNo}, Status={Status}", orderNo, order.Status);
return false;
}
// 更新订单状态
order.Status = 1; // 已支付
order.PaidAt = DateTime.Now;
order.TransactionId = transactionId;
order.PayAmount = payAmount;
order.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("订单支付成功: OrderNo={OrderNo}, TransactionId={TransactionId}, PayAmount={PayAmount}",
orderNo, transactionId, payAmount);
// 触发奖励发放
await ProcessRewardAsync(orderNo);
return true;
}
/// <inheritdoc />
public async Task<bool> ProcessRewardAsync(string orderNo)
{
if (string.IsNullOrWhiteSpace(orderNo))
{
_logger.LogWarning("处理奖励失败: 订单号为空");
return false;
}
var order = await GetOrderByNoAsync(orderNo);
if (order == null)
{
_logger.LogWarning("处理奖励失败: 订单不存在, OrderNo={OrderNo}", orderNo);
return false;
}
// 只有已支付且未发放奖励的订单才能处理
if (order.Status != 1)
{
_logger.LogWarning("订单未支付,无法发放奖励: OrderNo={OrderNo}, Status={Status}", orderNo, order.Status);
return false;
}
if (order.RewardStatus == 1)
{
_logger.LogInformation("奖励已发放,跳过重复处理: OrderNo={OrderNo}", orderNo);
return true;
}
// 查找对应的奖励处理器
var handler = _rewardHandlers.FirstOrDefault(h => h.OrderType == order.OrderType);
if (handler == null)
{
_logger.LogInformation("未找到订单类型对应的奖励处理器: OrderNo={OrderNo}, OrderType={OrderType}",
orderNo, order.OrderType);
// 没有处理器不算失败,可能该订单类型不需要奖励
return true;
}
try
{
_logger.LogInformation("开始处理奖励: OrderNo={OrderNo}, OrderType={OrderType}, Handler={Handler}",
orderNo, order.OrderType, handler.GetType().Name);
var result = await handler.ProcessRewardAsync(order);
if (result.Success)
{
order.RewardStatus = 1; // 已发放
order.RewardData = result.RewardData;
order.RewardAt = DateTime.Now;
order.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("奖励发放成功: OrderNo={OrderNo}, RewardData={RewardData}",
orderNo, result.RewardData);
return true;
}
else
{
order.RewardStatus = 2; // 发放失败
order.RewardData = result.Message;
order.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogWarning("奖励发放失败: OrderNo={OrderNo}, Message={Message}",
orderNo, result.Message);
return false;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "奖励发放异常: OrderNo={OrderNo}", orderNo);
order.RewardStatus = 2; // 发放失败
order.RewardData = ex.Message;
order.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
return false;
}
}
/// <inheritdoc />
public async Task<PageResponse<PaymentOrderDto>> GetUserOrdersAsync(int userId, PaymentOrderQueryRequest request)
{
if (userId <= 0)
throw new ArgumentException("用户ID无效", nameof(userId));
request ??= new PaymentOrderQueryRequest();
// 确保分页参数有效
if (request.Page < 1) request.Page = 1;
if (request.PageSize < 1) request.PageSize = 10;
if (request.PageSize > 100) request.PageSize = 100;
var query = _dbContext.PaymentOrders
.Where(o => o.UserId == userId);
// 按订单类型筛选
if (!string.IsNullOrWhiteSpace(request.OrderType))
{
query = query.Where(o => o.OrderType == request.OrderType);
}
// 按状态筛选
if (request.Status.HasValue)
{
query = query.Where(o => o.Status == request.Status.Value);
}
// 按时间范围筛选
if (request.StartTime.HasValue)
{
query = query.Where(o => o.CreatedAt >= request.StartTime.Value);
}
if (request.EndTime.HasValue)
{
query = query.Where(o => o.CreatedAt <= request.EndTime.Value);
}
// 获取总数
var total = await query.CountAsync();
// 分页查询
var orders = await query
.OrderByDescending(o => o.CreatedAt)
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(o => new PaymentOrderDto
{
Id = o.Id,
OrderNo = o.OrderNo,
UserId = o.UserId,
OrderType = o.OrderType,
Title = o.Title,
Amount = o.Amount,
PayAmount = o.PayAmount,
PayMethod = o.PayMethod,
Status = o.Status,
PaidAt = o.PaidAt,
TransactionId = o.TransactionId,
BizId = o.BizId,
BizData = o.BizData,
RewardStatus = o.RewardStatus,
RewardData = o.RewardData,
RewardAt = o.RewardAt,
CreatedAt = o.CreatedAt,
UpdatedAt = o.UpdatedAt
})
.ToListAsync();
var lastPage = (int)Math.Ceiling((double)total / request.PageSize);
return new PageResponse<PaymentOrderDto>
{
Data = orders,
Total = total,
Page = request.Page,
PageSize = request.PageSize,
LastPage = lastPage
};
}
/// <inheritdoc />
public async Task<bool> CancelOrderAsync(string orderNo, int userId)
{
if (string.IsNullOrWhiteSpace(orderNo))
{
_logger.LogWarning("取消订单失败: 订单号为空");
return false;
}
var order = await GetOrderByNoAsync(orderNo);
if (order == null)
{
_logger.LogWarning("取消订单失败: 订单不存在, OrderNo={OrderNo}", orderNo);
return false;
}
// 验证用户权限
if (order.UserId != userId)
{
_logger.LogWarning("取消订单失败: 用户无权限, OrderNo={OrderNo}, UserId={UserId}, OrderUserId={OrderUserId}",
orderNo, userId, order.UserId);
return false;
}
// 只有待支付状态的订单才能取消
if (order.Status != 0)
{
_logger.LogWarning("取消订单失败: 订单状态不正确, OrderNo={OrderNo}, Status={Status}", orderNo, order.Status);
return false;
}
order.Status = 2; // 已取消
order.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("订单取消成功: OrderNo={OrderNo}, UserId={UserId}", orderNo, userId);
return true;
}
/// <inheritdoc />
public async Task<bool> UpdateOrderStatusAsync(string orderNo, byte status)
{
if (string.IsNullOrWhiteSpace(orderNo))
{
_logger.LogWarning("更新订单状态失败: 订单号为空");
return false;
}
var order = await GetOrderByNoAsync(orderNo);
if (order == null)
{
_logger.LogWarning("更新订单状态失败: 订单不存在, OrderNo={OrderNo}", orderNo);
return false;
}
order.Status = status;
order.UpdatedAt = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("订单状态更新成功: OrderNo={OrderNo}, Status={Status}", orderNo, status);
return true;
}
/// <summary>
/// 生成唯一订单号
/// 格式: yyyyMMddHHmmss + 6位随机数
/// </summary>
private static string GenerateOrderNo()
{
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
var random = Random.Shared.Next(100000, 999999);
return $"{timestamp}{random}";
}
}

View File

@ -1,136 +0,0 @@
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Entities;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 支付奖励分发器
/// 负责根据订单类型查找并调用对应的奖励处理器
/// </summary>
public class PaymentRewardDispatcher : IPaymentRewardDispatcher
{
private readonly IEnumerable<IPaymentRewardHandler> _handlers;
private readonly ILogger<PaymentRewardDispatcher> _logger;
private readonly Dictionary<string, IPaymentRewardHandler> _handlerMap;
public PaymentRewardDispatcher(
IEnumerable<IPaymentRewardHandler> handlers,
ILogger<PaymentRewardDispatcher> logger)
{
_handlers = handlers ?? throw new ArgumentNullException(nameof(handlers));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
// 构建处理器映射表,提高查找效率
_handlerMap = new Dictionary<string, IPaymentRewardHandler>(StringComparer.OrdinalIgnoreCase);
foreach (var handler in _handlers)
{
if (!string.IsNullOrWhiteSpace(handler.OrderType))
{
if (_handlerMap.ContainsKey(handler.OrderType))
{
_logger.LogWarning("发现重复的奖励处理器: OrderType={OrderType}, 已存在={ExistingHandler}, 新处理器={NewHandler}",
handler.OrderType,
_handlerMap[handler.OrderType].GetType().Name,
handler.GetType().Name);
}
else
{
_handlerMap[handler.OrderType] = handler;
_logger.LogDebug("注册奖励处理器: OrderType={OrderType}, Handler={Handler}",
handler.OrderType, handler.GetType().Name);
}
}
}
_logger.LogInformation("奖励分发器初始化完成,已注册 {Count} 个处理器", _handlerMap.Count);
}
/// <inheritdoc />
public IPaymentRewardHandler? GetHandler(string orderType)
{
if (string.IsNullOrWhiteSpace(orderType))
{
_logger.LogWarning("获取处理器失败: 订单类型为空");
return null;
}
if (_handlerMap.TryGetValue(orderType, out var handler))
{
_logger.LogDebug("找到奖励处理器: OrderType={OrderType}, Handler={Handler}",
orderType, handler.GetType().Name);
return handler;
}
_logger.LogInformation("未找到订单类型对应的奖励处理器: OrderType={OrderType}", orderType);
return null;
}
/// <inheritdoc />
public bool HasHandler(string orderType)
{
if (string.IsNullOrWhiteSpace(orderType))
return false;
return _handlerMap.ContainsKey(orderType);
}
/// <inheritdoc />
public IReadOnlyCollection<string> GetRegisteredOrderTypes()
{
return _handlerMap.Keys.ToList().AsReadOnly();
}
/// <inheritdoc />
public async Task<RewardResult> ProcessRewardAsync(PaymentOrder order)
{
if (order == null)
{
_logger.LogWarning("处理奖励失败: 订单为空");
return RewardResult.Fail("订单不能为空");
}
if (string.IsNullOrWhiteSpace(order.OrderType))
{
_logger.LogWarning("处理奖励失败: 订单类型为空, OrderNo={OrderNo}", order.OrderNo);
return RewardResult.Fail("订单类型不能为空");
}
var handler = GetHandler(order.OrderType);
if (handler == null)
{
// 没有处理器不算失败,可能该订单类型不需要奖励
_logger.LogInformation("订单类型无需奖励处理: OrderNo={OrderNo}, OrderType={OrderType}",
order.OrderNo, order.OrderType);
return RewardResult.Ok();
}
try
{
_logger.LogInformation("开始处理奖励: OrderNo={OrderNo}, OrderType={OrderType}, Handler={Handler}",
order.OrderNo, order.OrderType, handler.GetType().Name);
var result = await handler.ProcessRewardAsync(order);
if (result.Success)
{
_logger.LogInformation("奖励处理成功: OrderNo={OrderNo}, OrderType={OrderType}, RewardData={RewardData}",
order.OrderNo, order.OrderType, result.RewardData);
}
else
{
_logger.LogWarning("奖励处理失败: OrderNo={OrderNo}, OrderType={OrderType}, Message={Message}",
order.OrderNo, order.OrderType, result.Message);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "奖励处理异常: OrderNo={OrderNo}, OrderType={OrderType}, Handler={Handler}",
order.OrderNo, order.OrderType, handler.GetType().Name);
return RewardResult.Fail($"奖励处理异常: {ex.Message}");
}
}
}

View File

@ -1,59 +0,0 @@
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// 支付服务实现
/// 提供基础的支付相关功能,具体业务逻辑由业务模块扩展
/// 注意:余额相关字段已移至 UserDetail 扩展表,此处返回默认值
/// </summary>
public class PaymentService : IPaymentService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<PaymentService> _logger;
public PaymentService(
MiAssessmentDbContext dbContext,
ILogger<PaymentService> logger)
{
_dbContext = dbContext;
_logger = logger;
}
/// <inheritdoc />
/// <remarks>
/// 余额字段已从 User 实体移除,此方法需要在业务层重新实现
/// 当前返回 false表示不支持余额支付
/// </remarks>
public async Task<bool> ValidateBalanceAsync(int userId, decimal amount)
{
var user = await _dbContext.Users.FindAsync(userId);
if (user == null)
return false;
// 余额字段已移至 UserDetail 扩展表
// 业务层需要实现具体的余额验证逻辑
_logger.LogWarning("ValidateBalanceAsync: 余额字段已移至 UserDetail 扩展表,请在业务层实现具体逻辑");
return false;
}
/// <inheritdoc />
/// <remarks>
/// 余额字段已从 User 实体移除,此方法需要在业务层重新实现
/// 当前返回 0
/// </remarks>
public async Task<decimal> GetUserBalanceAsync(int userId)
{
var user = await _dbContext.Users.FindAsync(userId);
if (user == null)
return 0;
// 余额字段已移至 UserDetail 扩展表
// 业务层需要实现具体的余额查询逻辑
_logger.LogWarning("GetUserBalanceAsync: 余额字段已移至 UserDetail 扩展表,请在业务层实现具体逻辑");
return 0;
}
}

View File

@ -50,7 +50,7 @@ public class UserService : BaseService<User, int>, IUserService
if (string.IsNullOrWhiteSpace(mobile))
return null;
return await _dbSet.FirstOrDefaultAsync(u => u.Mobile == mobile);
return await _dbSet.FirstOrDefaultAsync(u => u.Phone == mobile);
}
/// <inheritdoc/>
@ -66,15 +66,15 @@ public class UserService : BaseService<User, int>, IUserService
{
OpenId = dto.OpenId ?? string.Empty,
UnionId = dto.UnionId,
Mobile = dto.Mobile,
Phone = dto.Mobile,
Uid = uid,
Nickname = dto.Nickname ?? $"User{Random.Shared.Next(1000, 9999)}",
HeadImg = dto.Headimg ?? string.Empty,
Pid = dto.Pid,
Avatar = dto.Headimg ?? string.Empty,
ParentUserId = dto.Pid,
Status = 1,
IsTest = 0,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
await _dbSet.AddAsync(user);
@ -99,15 +99,15 @@ public class UserService : BaseService<User, int>, IUserService
user.Nickname = dto.Nickname;
if (!string.IsNullOrWhiteSpace(dto.Headimg))
user.HeadImg = dto.Headimg;
user.Avatar = dto.Headimg;
if (!string.IsNullOrWhiteSpace(dto.Mobile))
user.Mobile = dto.Mobile;
user.Phone = dto.Mobile;
if (!string.IsNullOrWhiteSpace(dto.UnionId))
user.UnionId = dto.UnionId;
user.UpdatedAt = DateTime.UtcNow;
user.UpdateTime = DateTime.Now;
_dbSet.Update(user);
await _dbContext.SaveChangesAsync();
@ -122,17 +122,17 @@ public class UserService : BaseService<User, int>, IUserService
if (user == null)
return null;
var registrationDays = (int)(DateTime.UtcNow - user.CreatedAt).TotalDays;
var registrationDays = (int)(DateTime.Now - user.CreateTime).TotalDays;
var maskedMobile = MaskMobileNumber(user.Mobile);
var mobileIs = string.IsNullOrWhiteSpace(user.Mobile) ? 0 : 1;
var maskedMobile = MaskMobileNumber(user.Phone);
var mobileIs = string.IsNullOrWhiteSpace(user.Phone) ? 0 : 1;
return new UserInfoDto
{
Id = user.Id,
Uid = user.Uid,
Nickname = user.Nickname,
Headimg = user.HeadImg,
Headimg = user.Avatar,
Mobile = maskedMobile,
MobileIs = mobileIs,
Money = 0, // 业务字段已移除,返回默认值

View File

@ -299,8 +299,8 @@ public class WechatPayService : IWechatPayService
RetryCount = 0,
Attach = attach,
OpenId = openId,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.OrderNotifies.Add(orderNotify);

View File

@ -882,8 +882,8 @@ public class WechatPayV3Service : IWechatPayV3Service
RetryCount = 0,
Attach = attach,
OpenId = openId,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.OrderNotifies.Add(orderNotify);

View File

@ -540,9 +540,8 @@ public class WechatService : IWechatService
RetryCount = 0,
Attach = request.Attach,
OpenId = request.OpenId,
Extend = JsonSerializer.Serialize(new { orderType = request.Attach, title = title }),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
_dbContext.OrderNotifies.Add(orderNotify);

View File

@ -58,16 +58,6 @@ public class ServiceModule : Module
return new AuthService(dbContext, userService, jwtService, wechatService, ipLocationService, redisService, jwtSettings, logger);
}).As<IAuthService>().InstancePerLifetimeScope();
// ========== 用户管理系统服务注册 ==========
// 注册地址服务
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var logger = c.Resolve<ILogger<AddressService>>();
return new AddressService(dbContext, logger);
}).As<IAddressService>().InstancePerLifetimeScope();
// ========== 支付系统服务注册 ==========
// 注册微信支付配置服务从Admin库读取配置
@ -105,15 +95,7 @@ public class ServiceModule : Module
return new WechatPayService(dbContext, httpClientFactory.CreateClient(), logger, configService, wechatService, redisService, settings, appSettings, v3ServiceLazy);
}).As<IWechatPayService>().InstancePerLifetimeScope();
// 注册支付服务
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var logger = c.Resolve<ILogger<PaymentService>>();
return new PaymentService(dbContext, logger);
}).As<IPaymentService>().InstancePerLifetimeScope();
// 注册支付回调服务
// ========== 配置系统服务注册 ==========
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
@ -124,33 +106,6 @@ public class ServiceModule : Module
return new PaymentNotifyService(dbContext, wechatPayService, wechatPayV3Service, wechatPayConfigService, logger);
}).As<IPaymentNotifyService>().InstancePerLifetimeScope();
// 注册支付订单服务
builder.Register(c =>
{
var dbContext = c.Resolve<MiAssessmentDbContext>();
var rewardHandlers = c.Resolve<IEnumerable<IPaymentRewardHandler>>();
var logger = c.Resolve<ILogger<PaymentOrderService>>();
return new PaymentOrderService(dbContext, rewardHandlers, logger);
}).As<IPaymentOrderService>().InstancePerLifetimeScope();
// 注册奖励分发器
builder.Register(c =>
{
var rewardHandlers = c.Resolve<IEnumerable<IPaymentRewardHandler>>();
var logger = c.Resolve<ILogger<PaymentRewardDispatcher>>();
return new PaymentRewardDispatcher(rewardHandlers, logger);
}).As<IPaymentRewardDispatcher>().SingleInstance();
// ========== 奖励处理器注册 ==========
// 注册默认奖励处理器(示例)
// 实际项目中可以注册多个处理器,每个处理器处理不同的订单类型
// 例如DiamondRechargeRewardHandler, VipPurchaseRewardHandler 等
builder.Register(c =>
{
var logger = c.Resolve<ILogger<DefaultPaymentRewardHandler>>();
return new DefaultPaymentRewardHandler(logger);
}).As<IPaymentRewardHandler>().InstancePerLifetimeScope();
// ========== 配置系统服务注册 ==========
// 注册配置服务

View File

@ -16,20 +16,9 @@ public partial class MiAssessmentDbContext : DbContext
{
}
// ==================== Admin 基础表 ====================
public virtual DbSet<Admin> Admins { get; set; }
public virtual DbSet<AdminLoginLog> AdminLoginLogs { get; set; }
public virtual DbSet<AdminOperationLog> AdminOperationLogs { get; set; }
// ==================== 用户基础表 ====================
public virtual DbSet<User> Users { get; set; }
public virtual DbSet<UserDetail> UserDetails { get; set; }
public virtual DbSet<UserAddress> UserAddresses { get; set; }
public virtual DbSet<UserRefreshToken> UserRefreshTokens { get; set; }
public virtual DbSet<UserLoginLog> UserLoginLogs { get; set; }
@ -39,12 +28,6 @@ public partial class MiAssessmentDbContext : DbContext
public virtual DbSet<OrderNotify> OrderNotifies { get; set; }
public virtual DbSet<PaymentOrder> PaymentOrders { get; set; }
public virtual DbSet<Picture> Pictures { get; set; }
public virtual DbSet<Delivery> Deliveries { get; set; }
// ==================== 小程序业务表 ====================
public virtual DbSet<Banner> Banners { get; set; }
@ -86,131 +69,6 @@ public partial class MiAssessmentDbContext : DbContext
{
modelBuilder.UseCollation("Chinese_PRC_CI_AS");
// ==================== Admin 基础表配置 ====================
modelBuilder.Entity<Admin>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_admins");
entity.ToTable("admins", tb => tb.HasComment("管理员表,存储后台管理员信息"));
entity.HasIndex(e => e.Status, "ix_admins_status");
entity.HasIndex(e => e.Token, "ix_admins_token");
entity.HasIndex(e => e.Username, "ix_admins_username");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.AdminId)
.HasComment("上级管理员ID")
.HasColumnName("admin_id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.GetTime)
.HasDefaultValueSql("(getdate())")
.HasComment("获取时间")
.HasColumnName("get_time");
entity.Property(e => e.Nickname)
.HasMaxLength(20)
.HasComment("昵称")
.HasColumnName("nickname");
entity.Property(e => e.Password)
.HasMaxLength(40)
.HasComment("密码(加密)")
.HasColumnName("password");
entity.Property(e => e.Qid)
.HasComment("权限组ID")
.HasColumnName("qid");
entity.Property(e => e.Random)
.HasMaxLength(20)
.HasComment("随机字符串")
.HasColumnName("random");
entity.Property(e => e.Status)
.HasDefaultValue(0)
.HasComment("状态0-正常")
.HasColumnName("status");
entity.Property(e => e.Token)
.HasMaxLength(100)
.HasComment("登录令牌")
.HasColumnName("token");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间")
.HasColumnName("updated_at");
entity.Property(e => e.Username)
.HasMaxLength(20)
.HasComment("用户名")
.HasColumnName("username");
});
modelBuilder.Entity<AdminLoginLog>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_admin_login_logs");
entity.ToTable("admin_login_logs", tb => tb.HasComment("管理员登录日志表,记录管理员登录信息(仅结构,不迁移历史数据)"));
entity.HasIndex(e => e.AdminId, "ix_admin_login_logs_admin_id");
entity.HasIndex(e => e.CreatedAt, "ix_admin_login_logs_created_at");
entity.HasIndex(e => e.Ip, "ix_admin_login_logs_ip");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.AdminId)
.HasComment("管理员ID")
.HasColumnName("admin_id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("登录时间")
.HasColumnName("created_at");
entity.Property(e => e.Ip)
.HasMaxLength(50)
.HasComment("登录IP地址")
.HasColumnName("ip");
});
modelBuilder.Entity<AdminOperationLog>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_admin_operation_logs");
entity.ToTable("admin_operation_logs", tb => tb.HasComment("管理员操作日志表,记录管理员操作信息(仅结构,不迁移历史数据)"));
entity.HasIndex(e => e.AdminId, "ix_admin_operation_logs_admin_id");
entity.HasIndex(e => e.CreatedAt, "ix_admin_operation_logs_created_at");
entity.HasIndex(e => e.Ip, "ix_admin_operation_logs_ip");
entity.HasIndex(e => e.Operation, "ix_admin_operation_logs_operation");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.AdminId)
.HasComment("管理员ID")
.HasColumnName("admin_id");
entity.Property(e => e.Content)
.HasComment("操作内容详情")
.HasColumnName("content");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("操作时间")
.HasColumnName("created_at");
entity.Property(e => e.Ip)
.HasMaxLength(50)
.HasComment("操作IP地址")
.HasColumnName("ip");
entity.Property(e => e.Operation)
.HasMaxLength(255)
.HasComment("操作名称")
.HasColumnName("operation");
});
// ==================== 用户基础表配置 ====================
modelBuilder.Entity<User>(entity =>
{
@ -218,178 +76,75 @@ public partial class MiAssessmentDbContext : DbContext
entity.ToTable("users", tb => tb.HasComment("用户主表,存储用户基本信息"));
entity.HasIndex(e => e.CreatedAt, "ix_users_created_at");
entity.HasIndex(e => e.Mobile, "ix_users_mobile").HasFilter("([mobile] IS NOT NULL)");
entity.HasIndex(e => e.CreateTime, "ix_users_created_at");
entity.HasIndex(e => e.Phone, "ix_users_mobile").HasFilter("([Phone] IS NOT NULL)");
entity.HasIndex(e => e.OpenId, "ix_users_open_id");
entity.HasIndex(e => e.Pid, "ix_users_pid");
entity.HasIndex(e => e.ParentUserId, "ix_users_pid");
entity.HasIndex(e => e.Status, "ix_users_status");
entity.HasIndex(e => e.Uid, "uk_users_uid").IsUnique();
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.GzhOpenId)
.HasMaxLength(255)
.HasComment("公众号openid")
.HasColumnName("gzh_open_id");
entity.Property(e => e.HeadImg)
.HasMaxLength(255)
.HasComment("头像URL")
.HasColumnName("head_img");
entity.Property(e => e.IsTest)
.HasComment("是否测试账号: 0否 1是")
.HasColumnName("is_test");
entity.Property(e => e.LastLoginTime)
.HasComment("最后登录时间")
.HasColumnName("last_login_time");
entity.Property(e => e.LastLoginIp)
.HasMaxLength(50)
.HasComment("最后登录IP")
.HasColumnName("last_login_ip");
entity.Property(e => e.Mobile)
.HasMaxLength(15)
.HasComment("手机号")
.HasColumnName("mobile");
entity.Property(e => e.Nickname)
.HasMaxLength(255)
.HasComment("昵称")
.HasColumnName("nickname");
.HasComment("主键ID");
entity.Property(e => e.Uid)
.HasMaxLength(6)
.HasComment("用户唯一标识");
entity.Property(e => e.OpenId)
.HasMaxLength(64)
.HasComment("微信openid");
entity.Property(e => e.UnionId)
.HasMaxLength(64)
.HasComment("微信unionid");
entity.Property(e => e.GzhOpenId)
.HasMaxLength(64)
.HasComment("公众号openid");
entity.Property(e => e.Phone)
.HasMaxLength(20)
.HasComment("手机号");
entity.Property(e => e.Nickname)
.HasMaxLength(50)
.HasComment("微信openid")
.HasColumnName("open_id");
entity.Property(e => e.Password)
.HasMaxLength(40)
.HasComment("密码")
.HasColumnName("password");
entity.Property(e => e.Pid)
.HasComment("推荐人ID")
.HasColumnName("pid");
.HasComment("昵称");
entity.Property(e => e.Avatar)
.HasMaxLength(500)
.HasComment("头像URL");
entity.Property(e => e.UserLevel)
.HasDefaultValue(1)
.HasComment("用户等级1普通用户 2合伙人 3渠道合伙人")
.HasColumnName("user_level");
.HasComment("用户等级1普通用户 2合伙人 3渠道合伙人");
entity.Property(e => e.ParentUserId)
.HasComment("推荐人用户ID");
entity.Property(e => e.InviteCode)
.HasMaxLength(10)
.HasComment("用户专属邀请码")
.HasColumnName("invite_code");
.HasComment("用户专属邀请码");
entity.Property(e => e.Balance)
.HasDefaultValue(0m)
.HasComment("可提现余额")
.HasColumnType("decimal(10, 2)")
.HasColumnName("balance");
.HasColumnType("decimal(10, 2)");
entity.Property(e => e.TotalIncome)
.HasDefaultValue(0m)
.HasComment("累计收益")
.HasColumnType("decimal(10, 2)")
.HasColumnName("total_income");
.HasColumnType("decimal(10, 2)");
entity.Property(e => e.WithdrawnAmount)
.HasDefaultValue(0m)
.HasComment("已提现金额")
.HasColumnType("decimal(10, 2)")
.HasColumnName("withdrawn_amount");
.HasColumnType("decimal(10, 2)");
entity.Property(e => e.Status)
.HasDefaultValue((byte)1)
.HasComment("状态: 1正常 0禁用")
.HasColumnName("status");
entity.Property(e => e.Uid)
.HasMaxLength(16)
.HasComment("用户唯一标识")
.HasColumnName("uid");
entity.Property(e => e.UnionId)
.HasMaxLength(255)
.HasComment("微信unionid")
.HasColumnName("union_id");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间")
.HasColumnName("updated_at");
});
modelBuilder.Entity<UserDetail>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_user_details");
entity.ToTable("user_details", tb => tb.HasComment("用户详情扩展表,用于存储业务扩展字段(余额、积分、等级等)"));
entity.HasIndex(e => e.UserId, "uk_user_details_user_id").IsUnique();
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.UserId)
.HasComment("用户ID唯一")
.HasColumnName("user_id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间")
.HasColumnName("updated_at");
// 配置与 User 的一对一关系
entity.HasOne(e => e.User)
.WithOne(u => u.UserDetail)
.HasForeignKey<UserDetail>(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_user_details_users");
});
modelBuilder.Entity<UserAddress>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_user_addresses");
entity.ToTable("user_addresses", tb => tb.HasComment("用户收货地址表,存储用户的收货地址信息"));
entity.HasIndex(e => new { e.UserId, e.IsDefault }, "ix_user_addresses_is_default").HasFilter("([is_deleted]=(0))");
entity.HasIndex(e => e.UserId, "ix_user_addresses_user_id");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.DetailedAddress)
.HasMaxLength(255)
.HasComment("详细地址")
.HasColumnName("detailed_address");
entity.Property(e => e.IsDefault)
.HasDefaultValue((byte)0)
.HasComment("是否默认地址: 0否 1是")
.HasColumnName("is_default");
entity.Property(e => e.IsDeleted)
.HasDefaultValue((byte)0)
.HasComment("是否删除: 0否 1是")
.HasColumnName("is_deleted");
entity.Property(e => e.ReceiverName)
.HasDefaultValue(1)
.HasComment("状态: 1正常 0禁用");
entity.Property(e => e.IsTest)
.HasComment("是否测试账号: 0否 1是");
entity.Property(e => e.LastLoginTime)
.HasComment("最后登录时间");
entity.Property(e => e.LastLoginIp)
.HasMaxLength(50)
.HasComment("收货人姓名")
.HasColumnName("receiver_name");
entity.Property(e => e.ReceiverPhone)
.HasMaxLength(20)
.HasComment("收货人电话")
.HasColumnName("receiver_phone");
entity.Property(e => e.UpdatedAt)
.HasComment("最后登录IP");
entity.Property(e => e.CreateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间")
.HasColumnName("updated_at");
entity.Property(e => e.UserId)
.HasComment("用户ID")
.HasColumnName("user_id");
.HasComment("创建时间");
entity.Property(e => e.UpdateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间");
entity.Property(e => e.IsDeleted)
.HasDefaultValue(false)
.HasComment("软删除标记");
});
modelBuilder.Entity<UserRefreshToken>(entity =>
@ -403,37 +158,28 @@ public partial class MiAssessmentDbContext : DbContext
entity.HasIndex(e => e.ExpiresAt, "ix_user_refresh_tokens_expires_at");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
.HasComment("主键ID");
entity.Property(e => e.UserId)
.HasComment("用户ID")
.HasColumnName("user_id");
.HasComment("用户ID");
entity.Property(e => e.TokenHash)
.HasMaxLength(256)
.HasComment("Token 哈希值SHA256")
.HasColumnName("token_hash");
.HasComment("Token 哈希值SHA256");
entity.Property(e => e.ExpiresAt)
.HasComment("过期时间")
.HasColumnName("expires_at");
.HasComment("过期时间");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
.HasComment("创建时间");
entity.Property(e => e.CreatedByIp)
.HasMaxLength(50)
.HasComment("创建时的 IP 地址")
.HasColumnName("created_by_ip");
.HasComment("创建时的 IP 地址");
entity.Property(e => e.RevokedAt)
.HasComment("撤销时间")
.HasColumnName("revoked_at");
.HasComment("撤销时间");
entity.Property(e => e.RevokedByIp)
.HasMaxLength(50)
.HasComment("撤销时的 IP 地址")
.HasColumnName("revoked_by_ip");
.HasComment("撤销时的 IP 地址");
entity.Property(e => e.ReplacedByToken)
.HasMaxLength(256)
.HasComment("被替换的新 Token 哈希值")
.HasColumnName("replaced_by_token");
.HasComment("被替换的新 Token 哈希值");
entity.HasOne(e => e.User)
.WithMany()
@ -446,54 +192,35 @@ public partial class MiAssessmentDbContext : DbContext
{
entity.HasKey(e => e.Id).HasName("pk_user_login_logs");
entity.ToTable("user_login_logs", tb => tb.HasComment("用户登录日志表,记录用户每次登录的时间、设备和位置信息"));
entity.HasIndex(e => e.LoginDate, "ix_user_login_logs_login_date");
entity.HasIndex(e => e.LoginTime, "ix_user_login_logs_login_time");
entity.ToTable("user_login_logs", tb => tb.HasComment("用户登录日志表,记录用户每次登录信息"));
entity.HasIndex(e => e.UserId, "ix_user_login_logs_user_id");
entity.HasIndex(e => e.Year, "ix_user_login_logs_year");
entity.HasIndex(e => new { e.Year, e.Month }, "ix_user_login_logs_year_month");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.Device)
.HasMaxLength(50)
.HasComment("设备类型")
.HasColumnName("device");
entity.Property(e => e.Ip)
.HasMaxLength(50)
.HasComment("登录IP")
.HasColumnName("ip");
entity.Property(e => e.LastLoginTime)
.HasComment("最后登录时间")
.HasColumnName("last_login_time");
entity.Property(e => e.Location)
.HasMaxLength(100)
.HasComment("登录位置")
.HasColumnName("location");
entity.Property(e => e.LoginDate)
.HasComment("登录日期")
.HasColumnName("login_date");
entity.Property(e => e.LoginTime)
.HasComment("登录时间")
.HasColumnName("login_time");
entity.Property(e => e.Month)
.HasComment("月份")
.HasColumnName("month");
.HasComment("主键ID");
entity.Property(e => e.UserId)
.HasComment("用户ID")
.HasColumnName("user_id");
entity.Property(e => e.Week)
.HasComment("周数")
.HasColumnName("week");
entity.Property(e => e.Year)
.HasComment("年份")
.HasColumnName("year");
.HasComment("用户ID");
entity.Property(e => e.LoginType)
.HasMaxLength(20)
.HasComment("登录类型");
entity.Property(e => e.LoginIp)
.HasMaxLength(50)
.HasComment("登录IP");
entity.Property(e => e.UserAgent)
.HasMaxLength(500)
.HasComment("用户代理");
entity.Property(e => e.Platform)
.HasMaxLength(20)
.HasComment("平台");
entity.Property(e => e.Status)
.HasDefaultValue(1)
.HasComment("状态1成功 0失败");
entity.Property(e => e.FailReason)
.HasMaxLength(200)
.HasComment("失败原因");
entity.Property(e => e.CreateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间");
});
// ==================== 系统基础表配置 ====================
@ -501,22 +228,36 @@ public partial class MiAssessmentDbContext : DbContext
{
entity.HasKey(e => e.Id).HasName("pk_configs");
entity.ToTable("configs", tb => tb.HasComment("系统配置表,存储系统各项配置信息"));
entity.ToTable("configs", tb => tb.HasComment("业务配置表,存储业务相关配置信息"));
entity.HasIndex(e => e.ConfigKey, "ix_configs_key");
entity.HasIndex(e => e.ConfigKey, "uk_configs_key").IsUnique();
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
.HasComment("主键ID");
entity.Property(e => e.ConfigKey)
.HasMaxLength(255)
.HasComment("配置键名")
.HasColumnName("config_key");
.HasMaxLength(100)
.HasComment("配置键名");
entity.Property(e => e.ConfigValue)
.HasComment("配置值JSON格式")
.HasColumnName("config_value");
.HasComment("配置值JSON格式");
entity.Property(e => e.ConfigType)
.HasMaxLength(50)
.HasComment("配置类型");
entity.Property(e => e.Description)
.HasMaxLength(500)
.HasComment("描述");
entity.Property(e => e.Sort)
.HasComment("排序");
entity.Property(e => e.CreateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间");
entity.Property(e => e.UpdateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间");
entity.Property(e => e.IsDeleted)
.HasDefaultValue(false)
.HasComment("软删除标记");
});
modelBuilder.Entity<OrderNotify>(entity =>
@ -531,212 +272,50 @@ public partial class MiAssessmentDbContext : DbContext
entity.HasIndex(e => e.Status, "ix_order_notifies_status");
entity.HasIndex(e => e.CreatedAt, "ix_order_notifies_created_at");
entity.HasIndex(e => e.CreateTime, "ix_order_notifies_created_at");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
.HasComment("主键ID");
entity.Property(e => e.OrderNo)
.HasMaxLength(64)
.HasComment("商户订单号")
.HasColumnName("order_no");
.HasComment("商户订单号");
entity.Property(e => e.TransactionId)
.HasMaxLength(64)
.HasComment("微信支付订单号")
.HasColumnName("transaction_id");
.HasComment("微信支付订单号");
entity.Property(e => e.NotifyUrl)
.HasMaxLength(255)
.HasComment("回调通知URL")
.HasColumnName("notify_url");
.HasMaxLength(500)
.HasComment("回调通知URL");
entity.Property(e => e.NonceStr)
.HasMaxLength(64)
.HasComment("随机字符串")
.HasColumnName("nonce_str");
.HasComment("随机字符串");
entity.Property(e => e.PayTime)
.HasComment("支付时间")
.HasColumnName("pay_time");
.HasComment("支付时间");
entity.Property(e => e.PayAmount)
.HasComment("支付金额")
.HasColumnType("decimal(10, 2)")
.HasColumnName("pay_amount");
.HasColumnType("decimal(10, 2)");
entity.Property(e => e.Status)
.HasDefaultValue((byte)0)
.HasComment("处理状态0=待处理1=处理成功2=处理失败")
.HasColumnName("status");
.HasDefaultValue(0)
.HasComment("处理状态0=待处理1=处理成功2=处理失败");
entity.Property(e => e.RetryCount)
.HasDefaultValue(0)
.HasComment("重试次数")
.HasColumnName("retry_count");
.HasComment("重试次数");
entity.Property(e => e.Attach)
.HasMaxLength(128)
.HasComment("附加数据(订单类型)")
.HasColumnName("attach");
.HasMaxLength(100)
.HasComment("附加数据(订单类型)");
entity.Property(e => e.OpenId)
.HasMaxLength(64)
.HasComment("用户OpenId")
.HasColumnName("open_id");
.HasMaxLength(100)
.HasComment("用户OpenId");
entity.Property(e => e.RawData)
.HasComment("原始回调数据")
.HasColumnName("raw_data");
.HasComment("原始回调数据");
entity.Property(e => e.ErrorMessage)
.HasMaxLength(500)
.HasComment("错误信息")
.HasColumnName("error_message");
entity.Property(e => e.CreatedAt)
.HasComment("错误信息");
entity.Property(e => e.CreateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasComment("创建时间");
entity.Property(e => e.UpdateTime)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间")
.HasColumnName("updated_at");
});
modelBuilder.Entity<PaymentOrder>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_payment_orders");
entity.ToTable("payment_orders", tb => tb.HasComment("通用支付订单表,支持多种订单类型和奖励发放机制"));
entity.HasIndex(e => e.OrderNo, "uk_payment_orders_order_no").IsUnique();
entity.HasIndex(e => e.UserId, "ix_payment_orders_user_id");
entity.HasIndex(e => e.OrderType, "ix_payment_orders_order_type");
entity.HasIndex(e => e.Status, "ix_payment_orders_status");
entity.HasIndex(e => e.CreatedAt, "ix_payment_orders_created_at");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.OrderNo)
.HasMaxLength(64)
.HasComment("订单号(唯一)")
.HasColumnName("order_no");
entity.Property(e => e.UserId)
.HasComment("用户ID")
.HasColumnName("user_id");
entity.Property(e => e.OrderType)
.HasMaxLength(50)
.HasComment("订单类型diamond_recharge, vip_purchase 等)")
.HasColumnName("order_type");
entity.Property(e => e.Title)
.HasMaxLength(100)
.HasComment("订单标题")
.HasColumnName("title");
entity.Property(e => e.Amount)
.HasComment("订单金额(单位:元)")
.HasColumnType("decimal(10, 2)")
.HasColumnName("amount");
entity.Property(e => e.PayAmount)
.HasComment("实付金额(单位:元)")
.HasColumnType("decimal(10, 2)")
.HasColumnName("pay_amount");
entity.Property(e => e.PayMethod)
.HasMaxLength(20)
.HasComment("支付方式wechat, alipay 等)")
.HasColumnName("pay_method");
entity.Property(e => e.Status)
.HasDefaultValue((byte)0)
.HasComment("状态0-待支付 1-已支付 2-已取消 3-已退款")
.HasColumnName("status");
entity.Property(e => e.PaidAt)
.HasComment("支付时间")
.HasColumnName("paid_at");
entity.Property(e => e.TransactionId)
.HasMaxLength(64)
.HasComment("第三方交易号")
.HasColumnName("transaction_id");
entity.Property(e => e.BizId)
.HasComment("业务关联ID")
.HasColumnName("biz_id");
entity.Property(e => e.BizData)
.HasComment("业务扩展数据JSON格式")
.HasColumnName("biz_data");
entity.Property(e => e.RewardStatus)
.HasDefaultValue((byte)0)
.HasComment("奖励状态0-未发放 1-已发放 2-发放失败")
.HasColumnName("reward_status");
entity.Property(e => e.RewardData)
.HasComment("奖励数据JSON格式")
.HasColumnName("reward_data");
entity.Property(e => e.RewardAt)
.HasComment("奖励发放时间")
.HasColumnName("reward_at");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("更新时间")
.HasColumnName("updated_at");
// 配置与 User 的关系
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("fk_payment_orders_users");
});
modelBuilder.Entity<Picture>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_pictures");
entity.ToTable("pictures", tb => tb.HasComment("图片管理表,存储上传的图片信息"));
entity.HasIndex(e => e.Status, "ix_pictures_status");
entity.HasIndex(e => e.Token, "ix_pictures_token");
entity.HasIndex(e => e.Type, "ix_pictures_type");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("(getdate())")
.HasComment("创建时间")
.HasColumnName("created_at");
entity.Property(e => e.ImgUrl)
.HasMaxLength(255)
.HasComment("图片URL地址")
.HasColumnName("img_url");
entity.Property(e => e.Status)
.HasDefaultValue((byte)1)
.HasComment("状态1-正常")
.HasColumnName("status");
entity.Property(e => e.Token)
.HasMaxLength(255)
.HasComment("图片令牌/标识")
.HasColumnName("token");
entity.Property(e => e.Type)
.HasComment("图片类型")
.HasColumnName("type");
});
modelBuilder.Entity<Delivery>(entity =>
{
entity.HasKey(e => e.Id).HasName("pk_deliveries");
entity.ToTable("deliveries", tb => tb.HasComment("快递公司配置表,存储快递公司信息"));
entity.HasIndex(e => e.Code, "ix_deliveries_code");
entity.Property(e => e.Id)
.HasComment("主键ID")
.HasColumnName("id");
entity.Property(e => e.Code)
.HasMaxLength(30)
.HasComment("快递公司编码")
.HasColumnName("code");
entity.Property(e => e.Name)
.HasMaxLength(50)
.HasComment("快递公司名称")
.HasColumnName("name");
.HasComment("更新时间");
});
// ==================== 小程序业务表配置 ====================

View File

@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 管理员表,存储后台管理员信息
/// </summary>
public partial class Admin
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string? Username { get; set; }
/// <summary>
/// 昵称
/// </summary>
public string Nickname { get; set; } = null!;
/// <summary>
/// 密码(加密)
/// </summary>
public string? Password { get; set; }
/// <summary>
/// 权限组ID
/// </summary>
public int? Qid { get; set; }
/// <summary>
/// 状态0-正常
/// </summary>
public int? Status { get; set; }
/// <summary>
/// 获取时间
/// </summary>
public DateTime GetTime { get; set; }
/// <summary>
/// 随机字符串
/// </summary>
public string Random { get; set; } = null!;
/// <summary>
/// 登录令牌
/// </summary>
public string? Token { get; set; }
/// <summary>
/// 上级管理员ID
/// </summary>
public int AdminId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 管理员登录日志表,记录管理员登录信息(仅结构,不迁移历史数据)
/// </summary>
public partial class AdminLoginLog
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 管理员ID
/// </summary>
public int AdminId { get; set; }
/// <summary>
/// 登录IP地址
/// </summary>
public string Ip { get; set; } = null!;
/// <summary>
/// 登录时间
/// </summary>
public DateTime CreatedAt { get; set; }
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 管理员操作日志表,记录管理员操作信息(仅结构,不迁移历史数据)
/// </summary>
public partial class AdminOperationLog
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 管理员ID
/// </summary>
public int AdminId { get; set; }
/// <summary>
/// 操作IP地址
/// </summary>
public string Ip { get; set; } = null!;
/// <summary>
/// 操作名称
/// </summary>
public string? Operation { get; set; }
/// <summary>
/// 操作内容详情
/// </summary>
public string? Content { get; set; }
/// <summary>
/// 操作时间
/// </summary>
public DateTime CreatedAt { get; set; }
}

View File

@ -1,25 +1,64 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 系统配置表,存储系统各项配置信息
/// 业务配置表,存储业务相关配置信息
/// </summary>
public partial class Config
[Table("configs")]
public class Config
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
[Key]
public long Id { get; set; }
/// <summary>
/// 配置键名
/// </summary>
[Required]
[MaxLength(100)]
public string ConfigKey { get; set; } = null!;
/// <summary>
/// 配置值JSON格式
/// </summary>
public string? ConfigValue { get; set; }
[Required]
public string ConfigValue { get; set; } = null!;
/// <summary>
/// 配置类型
/// </summary>
[Required]
[MaxLength(50)]
public string ConfigType { get; set; } = null!;
/// <summary>
/// 描述
/// </summary>
[MaxLength(500)]
public string? Description { get; set; }
/// <summary>
/// 排序
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdateTime { get; set; }
/// <summary>
/// 软删除标记
/// </summary>
public bool IsDeleted { get; set; }
}

View File

@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 快递公司配置表,存储快递公司信息
/// </summary>
public partial class Delivery
{
/// <summary>
/// 主键ID
/// </summary>
public short Id { get; set; }
/// <summary>
/// 快递公司名称
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// 快递公司编码
/// </summary>
public string Code { get; set; } = null!;
}

View File

@ -1,35 +1,44 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 支付通知记录表,记录微信支付回调通知
/// </summary>
public partial class OrderNotify
[Table("order_notifies")]
public class OrderNotify
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
[Key]
public long Id { get; set; }
/// <summary>
/// 商户订单号
/// </summary>
[Required]
[MaxLength(64)]
public string OrderNo { get; set; } = null!;
/// <summary>
/// 微信支付订单号
/// </summary>
[MaxLength(64)]
public string? TransactionId { get; set; }
/// <summary>
/// 回调通知URL
/// </summary>
[MaxLength(500)]
public string? NotifyUrl { get; set; }
/// <summary>
/// 随机字符串
/// </summary>
[MaxLength(64)]
public string? NonceStr { get; set; }
/// <summary>
@ -45,7 +54,7 @@ public partial class OrderNotify
/// <summary>
/// 处理状态0=待处理1=处理成功2=处理失败
/// </summary>
public byte Status { get; set; }
public int Status { get; set; }
/// <summary>
/// 重试次数
@ -55,11 +64,13 @@ public partial class OrderNotify
/// <summary>
/// 附加数据(订单类型)
/// </summary>
[MaxLength(100)]
public string? Attach { get; set; }
/// <summary>
/// 用户OpenId
/// </summary>
[MaxLength(100)]
public string? OpenId { get; set; }
/// <summary>
@ -70,20 +81,16 @@ public partial class OrderNotify
/// <summary>
/// 错误信息
/// </summary>
[MaxLength(500)]
public string? ErrorMessage { get; set; }
/// <summary>
/// 扩展数据JSON格式
/// </summary>
public string? Extend { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
public DateTime UpdateTime { get; set; }
}

View File

@ -1,106 +0,0 @@
using System;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 通用支付订单表,支持多种订单类型和奖励发放机制
/// </summary>
public partial class PaymentOrder
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 订单号(唯一)
/// </summary>
public string OrderNo { get; set; } = null!;
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 订单类型diamond_recharge, vip_purchase 等)
/// </summary>
public string OrderType { get; set; } = null!;
/// <summary>
/// 订单标题
/// </summary>
public string Title { get; set; } = null!;
/// <summary>
/// 订单金额(单位:元)
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 实付金额(单位:元)
/// </summary>
public decimal PayAmount { get; set; }
/// <summary>
/// 支付方式wechat, alipay 等)
/// </summary>
public string? PayMethod { get; set; }
/// <summary>
/// 状态0-待支付 1-已支付 2-已取消 3-已退款
/// </summary>
public byte Status { get; set; }
/// <summary>
/// 支付时间
/// </summary>
public DateTime? PaidAt { get; set; }
/// <summary>
/// 第三方交易号
/// </summary>
public string? TransactionId { get; set; }
/// <summary>
/// 业务关联ID
/// </summary>
public int? BizId { get; set; }
/// <summary>
/// 业务扩展数据JSON格式
/// </summary>
public string? BizData { get; set; }
/// <summary>
/// 奖励状态0-未发放 1-已发放 2-发放失败
/// </summary>
public byte RewardStatus { get; set; }
/// <summary>
/// 奖励数据JSON格式
/// </summary>
public string? RewardData { get; set; }
/// <summary>
/// 奖励发放时间
/// </summary>
public DateTime? RewardAt { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
// ==================== 导航属性 ====================
/// <summary>
/// 关联的用户
/// </summary>
public virtual User? User { get; set; }
}

View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 图片管理表,存储上传的图片信息
/// </summary>
public partial class Picture
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 图片URL地址
/// </summary>
public string ImgUrl { get; set; } = null!;
/// <summary>
/// 图片令牌/标识
/// </summary>
public string Token { get; set; } = null!;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 状态1-正常
/// </summary>
public byte Status { get; set; }
/// <summary>
/// 图片类型
/// </summary>
public byte? Type { get; set; }
}

View File

@ -1,73 +1,79 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 用户主表,存储用户基本信息
/// 精简版:只保留核心字段,业务字段移至 UserDetail 扩展表
/// </summary>
[Table("users")]
public partial class User
{
/// <summary>
/// 主键ID
/// </summary>
[Key]
public int Id { get; set; }
/// <summary>
/// 用户唯一标识
/// </summary>
[Required]
[MaxLength(6)]
public string Uid { get; set; } = null!;
/// <summary>
/// 微信openid
/// </summary>
[Required]
[MaxLength(64)]
public string OpenId { get; set; } = null!;
/// <summary>
/// 微信unionid
/// </summary>
[MaxLength(64)]
public string? UnionId { get; set; }
/// <summary>
/// 公众号openid
/// </summary>
[MaxLength(64)]
public string? GzhOpenId { get; set; }
/// <summary>
/// 用户唯一标识
/// </summary>
public string Uid { get; set; } = null!;
/// <summary>
/// 手机号
/// </summary>
public string? Mobile { get; set; }
[MaxLength(20)]
public string? Phone { get; set; }
/// <summary>
/// 昵称
/// </summary>
public string Nickname { get; set; } = null!;
[MaxLength(50)]
public string? Nickname { get; set; }
/// <summary>
/// 头像URL
/// </summary>
public string HeadImg { get; set; } = null!;
/// <summary>
/// 密码
/// </summary>
public string? Password { get; set; }
/// <summary>
/// 推荐人ID
/// </summary>
public int Pid { get; set; }
[MaxLength(500)]
public string? Avatar { get; set; }
/// <summary>
/// 用户等级1普通用户 2合伙人 3渠道合伙人
/// </summary>
public int UserLevel { get; set; } = 1;
/// <summary>
/// 推荐人用户ID
/// </summary>
public int? ParentUserId { get; set; }
/// <summary>
/// 用户专属邀请码
/// </summary>
[MaxLength(10)]
public string? InviteCode { get; set; }
/// <summary>
@ -91,23 +97,13 @@ public partial class User
/// <summary>
/// 状态: 1正常 0禁用
/// </summary>
public byte Status { get; set; }
public int Status { get; set; }
/// <summary>
/// 是否测试账号: 0否 1是
/// </summary>
public int IsTest { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
/// <summary>
/// 最后登录时间
/// </summary>
@ -116,12 +112,22 @@ public partial class User
/// <summary>
/// 最后登录IP
/// </summary>
[MaxLength(50)]
public string? LastLoginIp { get; set; }
// ==================== 导航属性 ====================
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 用户详情(一对一关联)
/// 更新时间
/// </summary>
public virtual UserDetail? UserDetail { get; set; }
public DateTime UpdateTime { get; set; }
/// <summary>
/// 软删除标记
/// </summary>
public bool IsDeleted { get; set; }
}

View File

@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 用户收货地址表,存储用户的收货地址信息
/// </summary>
public partial class UserAddress
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 收货人姓名
/// </summary>
public string ReceiverName { get; set; } = null!;
/// <summary>
/// 收货人电话
/// </summary>
public string ReceiverPhone { get; set; } = null!;
/// <summary>
/// 详细地址
/// </summary>
public string DetailedAddress { get; set; } = null!;
/// <summary>
/// 是否默认地址: 0否 1是
/// </summary>
public byte? IsDefault { get; set; }
/// <summary>
/// 是否删除: 0否 1是
/// </summary>
public byte? IsDeleted { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
}

View File

@ -1,37 +0,0 @@
using System;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 用户详情扩展表,用于存储业务扩展字段
/// 与 User 表一对一关联,按需添加余额、积分、等级等业务字段
/// </summary>
public partial class UserDetail
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户ID唯一与 User 表一对一关联)
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
// ==================== 导航属性 ====================
/// <summary>
/// 关联的用户
/// </summary>
public virtual User User { get; set; } = null!;
}

View File

@ -1,65 +1,64 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MiAssessment.Model.Entities;
/// <summary>
/// 用户登录日志表,记录用户每次登录的时间、设备和位置信息
/// 用户登录日志表,记录用户每次登录信息
/// </summary>
public partial class UserLoginLog
[Table("user_login_logs")]
public class UserLoginLog
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
[Key]
public long Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 登录日期
/// 登录类型wechat/mobile/sms等
/// </summary>
public DateOnly LoginDate { get; set; }
/// <summary>
/// 登录时间
/// </summary>
public DateTime? LoginTime { get; set; }
/// <summary>
/// 最后登录时间
/// </summary>
public DateTime? LastLoginTime { get; set; }
/// <summary>
/// 设备类型
/// </summary>
public string? Device { get; set; }
[Required]
[MaxLength(20)]
public string LoginType { get; set; } = null!;
/// <summary>
/// 登录IP
/// </summary>
public string? Ip { get; set; }
[MaxLength(50)]
public string? LoginIp { get; set; }
/// <summary>
/// 登录位置
/// 用户代理
/// </summary>
public string? Location { get; set; }
[MaxLength(500)]
public string? UserAgent { get; set; }
/// <summary>
/// 年份
/// 平台miniprogram/h5/app等
/// </summary>
public int Year { get; set; }
[MaxLength(20)]
public string? Platform { get; set; }
/// <summary>
/// 月份
/// 状态1成功 0失败
/// </summary>
public int Month { get; set; }
public int Status { get; set; }
/// <summary>
/// 周数
/// 失败原因
/// </summary>
public int Week { get; set; }
[MaxLength(200)]
public string? FailReason { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreateTime { get; set; }
}

View File

@ -1,115 +0,0 @@
namespace MiAssessment.Model.Models.Address;
/// <summary>
/// 地址响应DTO
/// </summary>
public class AddressDto
{
/// <summary>
/// 地址ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 收货人姓名
/// </summary>
public string ReceiverName { get; set; } = string.Empty;
/// <summary>
/// 收货人电话
/// </summary>
public string ReceiverPhone { get; set; } = string.Empty;
/// <summary>
/// 详细地址
/// </summary>
public string DetailedAddress { get; set; } = string.Empty;
/// <summary>
/// 是否默认地址: 0否 1是
/// </summary>
public int IsDefault { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public string? CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public string? UpdateTime { get; set; }
}
/// <summary>
/// 添加地址请求
/// </summary>
public class AddAddressRequest
{
/// <summary>
/// 收货人姓名
/// </summary>
public string ReceiverName { get; set; } = string.Empty;
/// <summary>
/// 收货人电话
/// </summary>
public string ReceiverPhone { get; set; } = string.Empty;
/// <summary>
/// 详细地址
/// </summary>
public string DetailedAddress { get; set; } = string.Empty;
/// <summary>
/// 是否设为默认地址: 0否 1是
/// </summary>
public int IsDefault { get; set; }
}
/// <summary>
/// 更新地址请求
/// </summary>
public class UpdateAddressRequest
{
/// <summary>
/// 地址ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 收货人姓名
/// </summary>
public string ReceiverName { get; set; } = string.Empty;
/// <summary>
/// 收货人电话
/// </summary>
public string ReceiverPhone { get; set; } = string.Empty;
/// <summary>
/// 详细地址
/// </summary>
public string DetailedAddress { get; set; } = string.Empty;
/// <summary>
/// 是否设为默认地址: 0否 1是
/// </summary>
public int? IsDefault { get; set; }
}
/// <summary>
/// 地址ID请求
/// </summary>
public class AddressIdRequest
{
/// <summary>
/// 地址ID
/// </summary>
public int Id { get; set; }
}

View File

@ -1,47 +0,0 @@
namespace MiAssessment.Model.Models.Payment;
/// <summary>
/// 创建支付订单请求
/// </summary>
public class CreatePaymentOrderRequest
{
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 订单类型diamond_recharge, vip_purchase 等)
/// </summary>
public string OrderType { get; set; } = string.Empty;
/// <summary>
/// 订单标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 订单金额(单位:元)
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 实付金额(单位:元),默认等于订单金额
/// </summary>
public decimal? PayAmount { get; set; }
/// <summary>
/// 支付方式wechat, alipay 等)
/// </summary>
public string? PayMethod { get; set; }
/// <summary>
/// 业务关联ID
/// </summary>
public int? BizId { get; set; }
/// <summary>
/// 业务扩展数据JSON格式
/// </summary>
public string? BizData { get; set; }
}

View File

@ -1,120 +0,0 @@
namespace MiAssessment.Model.Models.Payment;
/// <summary>
/// 支付订单DTO
/// </summary>
public class PaymentOrderDto
{
/// <summary>
/// 主键ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// 订单号
/// </summary>
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 订单类型
/// </summary>
public string OrderType { get; set; } = string.Empty;
/// <summary>
/// 订单标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 订单金额(单位:元)
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 实付金额(单位:元)
/// </summary>
public decimal PayAmount { get; set; }
/// <summary>
/// 支付方式
/// </summary>
public string? PayMethod { get; set; }
/// <summary>
/// 状态0-待支付 1-已支付 2-已取消 3-已退款
/// </summary>
public byte Status { get; set; }
/// <summary>
/// 状态文本
/// </summary>
public string StatusText => Status switch
{
0 => "待支付",
1 => "已支付",
2 => "已取消",
3 => "已退款",
_ => "未知"
};
/// <summary>
/// 支付时间
/// </summary>
public DateTime? PaidAt { get; set; }
/// <summary>
/// 第三方交易号
/// </summary>
public string? TransactionId { get; set; }
/// <summary>
/// 业务关联ID
/// </summary>
public int? BizId { get; set; }
/// <summary>
/// 业务扩展数据JSON格式
/// </summary>
public string? BizData { get; set; }
/// <summary>
/// 奖励状态0-未发放 1-已发放 2-发放失败
/// </summary>
public byte RewardStatus { get; set; }
/// <summary>
/// 奖励状态文本
/// </summary>
public string RewardStatusText => RewardStatus switch
{
0 => "未发放",
1 => "已发放",
2 => "发放失败",
_ => "未知"
};
/// <summary>
/// 奖励数据JSON格式
/// </summary>
public string? RewardData { get; set; }
/// <summary>
/// 奖励发放时间
/// </summary>
public DateTime? RewardAt { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedAt { get; set; }
}

View File

@ -1,45 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace MiAssessment.Model.Models.Payment;
/// <summary>
/// 支付订单查询请求
/// </summary>
public class PaymentOrderQueryRequest
{
/// <summary>
/// 页码从1开始
/// </summary>
[FromQuery(Name = "page")]
public int Page { get; set; } = 1;
/// <summary>
/// 每页数量
/// </summary>
[FromQuery(Name = "pageSize")]
public int PageSize { get; set; } = 10;
/// <summary>
/// 订单类型(可选)
/// </summary>
[FromQuery(Name = "orderType")]
public string? OrderType { get; set; }
/// <summary>
/// 订单状态可选0-待支付 1-已支付 2-已取消 3-已退款
/// </summary>
[FromQuery(Name = "status")]
public byte? Status { get; set; }
/// <summary>
/// 开始时间(可选)
/// </summary>
[FromQuery(Name = "startTime")]
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间(可选)
/// </summary>
[FromQuery(Name = "endTime")]
public DateTime? EndTime { get; set; }
}

View File

@ -1,607 +0,0 @@
using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Core.Interfaces;
using MiAssessment.Core.Services;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models.Payment;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Core;
/// <summary>
/// PaymentOrderService 属性测试
/// Feature: framework-template
///
/// Property 1: Payment Order Creation Integrity
/// *For any* payment order creation request with valid user ID, order type, and amount, the created order SHALL have:
/// - A unique order number that does not exist in the database
/// - The order_type field correctly set to the requested type
/// - The amount field correctly set to the requested amount
/// - The biz_data field correctly storing the provided JSON data
/// **Validates: Requirements 5.1, 5.2, 5.3**
///
/// Property 2: Payment Order State Transition
/// *For any* payment order that receives a successful payment callback:
/// - The status SHALL be updated from 0 (pending) to 1 (paid)
/// - The paid_at timestamp SHALL be set
/// - The transaction_id SHALL be recorded
/// - The reward processing SHALL be triggered
/// - If reward succeeds, reward_status SHALL be 1 and reward_at SHALL be set
/// - If reward fails, reward_status SHALL be 2
/// **Validates: Requirements 5.4, 5.5, 5.6, 5.7**
/// </summary>
public class PaymentOrderServicePropertyTests
{
private readonly Mock<ILogger<PaymentOrderService>> _mockLogger = new();
/// <summary>
/// 订单状态:待支付
/// </summary>
private const byte StatusPending = 0;
/// <summary>
/// 订单状态:已支付
/// </summary>
private const byte StatusPaid = 1;
/// <summary>
/// 奖励状态:未发放
/// </summary>
private const byte RewardStatusPending = 0;
/// <summary>
/// 奖励状态:已发放
/// </summary>
private const byte RewardStatusSuccess = 1;
/// <summary>
/// 奖励状态:发放失败
/// </summary>
private const byte RewardStatusFailed = 2;
#region Property 1: Payment Order Creation Integrity
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.1: Created order has unique order number
/// A unique order number that does not exist in the database
///
/// **Validates: Requirements 5.1**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_ShouldGenerateUniqueOrderNo(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request1 = CreateValidRequest(userId.Get, seed.Get);
var request2 = CreateValidRequest(userId.Get, seed.Get + 1);
// Act: Create two orders
var order1 = service.CreateOrderAsync(request1).GetAwaiter().GetResult();
var order2 = service.CreateOrderAsync(request2).GetAwaiter().GetResult();
// Assert: Order numbers should be unique
return order1.OrderNo != order2.OrderNo;
}
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.2: Created order has correct order_type
/// The order_type field correctly set to the requested type
///
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_ShouldSetCorrectOrderType(PositiveInt userId, NonEmptyString orderType, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
// Generate a valid order type (alphanumeric with underscores)
var validOrderType = GenerateValidOrderType(orderType.Get, seed.Get);
var request = CreateValidRequest(userId.Get, seed.Get);
request.OrderType = validOrderType;
// Act
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
// Assert: Order type should match the request
return order.OrderType == validOrderType;
}
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.3: Created order has correct amount
/// The amount field correctly set to the requested amount
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_ShouldSetCorrectAmount(PositiveInt userId, PositiveInt amountCents, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
// Generate a valid amount (convert cents to decimal to avoid floating point issues)
var amount = Math.Round((decimal)(amountCents.Get % 100000 + 1) / 100, 2);
var request = CreateValidRequest(userId.Get, seed.Get);
request.Amount = amount;
// Act
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
// Assert: Amount should match the request
return order.Amount == amount;
}
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.4: Created order has correct biz_data
/// The biz_data field correctly storing the provided JSON data
///
/// **Validates: Requirements 5.3**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_ShouldSetCorrectBizData(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
// Generate valid JSON biz_data
var bizData = $"{{\"product_id\":{seed.Get},\"quantity\":{(seed.Get % 10) + 1}}}";
var request = CreateValidRequest(userId.Get, seed.Get);
request.BizData = bizData;
// Act
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
// Assert: BizData should match the request
return order.BizData == bizData;
}
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.5: Created order has initial status of pending (0)
/// The created order should have status = 0 (pending)
///
/// **Validates: Requirements 5.1**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_ShouldHavePendingStatus(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
// Act
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
// Assert: Status should be pending (0)
return order.Status == StatusPending;
}
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.6: Created order has initial reward_status of pending (0)
/// The created order should have reward_status = 0 (not issued)
///
/// **Validates: Requirements 5.1**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_ShouldHavePendingRewardStatus(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
// Act
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
// Assert: RewardStatus should be pending (0)
return order.RewardStatus == RewardStatusPending;
}
/// <summary>
/// Feature: framework-template, Property 1: Payment Order Creation Integrity
///
/// Property 1.7: Order number does not exist in database before creation
/// A unique order number that does not exist in the database
///
/// **Validates: Requirements 5.1**
/// </summary>
[Property(MaxTest = 100)]
public bool CreateOrder_OrderNoShouldNotExistBeforeCreation(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
// Act
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
// Verify: The order number should exist exactly once in the database
var count = dbContext.PaymentOrders.Count(o => o.OrderNo == order.OrderNo);
// Assert: Should exist exactly once
return count == 1;
}
#endregion
#region Property 2: Payment Order State Transition
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.1: Payment success updates status from 0 to 1
/// The status SHALL be updated from 0 (pending) to 1 (paid)
///
/// **Validates: Requirements 5.4**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_ShouldUpdateStatusToPaid(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
var result = service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
// Assert: Status should be updated to paid (1)
return result && updatedOrder != null && updatedOrder.Status == StatusPaid;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.2: Payment success sets paid_at timestamp
/// The paid_at timestamp SHALL be set
///
/// **Validates: Requirements 5.4**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_ShouldSetPaidAtTimestamp(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var beforePayment = DateTime.Now.AddSeconds(-1);
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
var afterPayment = DateTime.Now.AddSeconds(1);
// Assert: PaidAt should be set and within reasonable time range
return updatedOrder != null &&
updatedOrder.PaidAt.HasValue &&
updatedOrder.PaidAt.Value >= beforePayment &&
updatedOrder.PaidAt.Value <= afterPayment;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.3: Payment success records transaction_id
/// The transaction_id SHALL be recorded
///
/// **Validates: Requirements 5.4**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_ShouldRecordTransactionId(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
// Assert: TransactionId should be recorded
return updatedOrder != null && updatedOrder.TransactionId == transactionId;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.4: Payment success with successful reward handler sets reward_status to 1
/// If reward succeeds, reward_status SHALL be 1 and reward_at SHALL be set
///
/// **Validates: Requirements 5.5, 5.6**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_WithSuccessfulReward_ShouldSetRewardStatusToSuccess(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var orderType = $"test_reward_success_{seed.Get % 100}";
var rewardData = $"{{\"reward_id\":{seed.Get}}}";
// Create a mock reward handler that succeeds
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok(rewardData));
var service = CreateService(dbContext, new[] { mockHandler.Object });
var request = CreateValidRequest(userId.Get, seed.Get);
request.OrderType = orderType;
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
// Assert: RewardStatus should be success (1) and RewardAt should be set
return updatedOrder != null &&
updatedOrder.RewardStatus == RewardStatusSuccess &&
updatedOrder.RewardAt.HasValue;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.5: Payment success with failed reward handler sets reward_status to 2
/// If reward fails, reward_status SHALL be 2
///
/// **Validates: Requirements 5.7**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_WithFailedReward_ShouldSetRewardStatusToFailed(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var orderType = $"test_reward_fail_{seed.Get % 100}";
var errorMessage = $"Reward processing failed for seed {seed.Get}";
// Create a mock reward handler that fails
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Fail(errorMessage));
var service = CreateService(dbContext, new[] { mockHandler.Object });
var request = CreateValidRequest(userId.Get, seed.Get);
request.OrderType = orderType;
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
// Assert: RewardStatus should be failed (2)
return updatedOrder != null && updatedOrder.RewardStatus == RewardStatusFailed;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.6: Payment success triggers reward processing
/// The reward processing SHALL be triggered
///
/// **Validates: Requirements 5.5**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_ShouldTriggerRewardProcessing(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var orderType = $"test_trigger_{seed.Get % 100}";
var rewardProcessed = false;
// Create a mock reward handler that tracks if it was called
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => rewardProcessed = true)
.ReturnsAsync(RewardResult.Ok());
var service = CreateService(dbContext, new[] { mockHandler.Object });
var request = CreateValidRequest(userId.Get, seed.Get);
request.OrderType = orderType;
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Assert: Reward handler should have been called
return rewardProcessed;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.7: Idempotent payment success handling
/// Processing the same payment twice should not change the order state
///
/// **Validates: Requirements 5.4**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_ShouldBeIdempotent(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var service = CreateService(dbContext);
var request = CreateValidRequest(userId.Get, seed.Get);
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act: Process payment twice
var result1 = service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
var result2 = service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
// Assert: Both calls should succeed and order should be in paid state
return result1 && result2 && updatedOrder != null && updatedOrder.Status == StatusPaid;
}
/// <summary>
/// Feature: framework-template, Property 2: Payment Order State Transition
///
/// Property 2.8: Successful reward stores reward_data
/// If reward succeeds, reward_data SHALL contain the reward information
///
/// **Validates: Requirements 5.6**
/// </summary>
[Property(MaxTest = 100)]
public bool HandlePaymentSuccess_WithSuccessfulReward_ShouldStoreRewardData(PositiveInt userId, PositiveInt seed)
{
// Arrange
using var dbContext = CreateDbContext();
var orderType = $"test_reward_data_{seed.Get % 100}";
var rewardData = $"{{\"diamonds\":{seed.Get % 1000 + 100},\"bonus\":{seed.Get % 50}}}";
// Create a mock reward handler that returns reward data
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok(rewardData));
var service = CreateService(dbContext, new[] { mockHandler.Object });
var request = CreateValidRequest(userId.Get, seed.Get);
request.OrderType = orderType;
var order = service.CreateOrderAsync(request).GetAwaiter().GetResult();
var transactionId = $"TX_{seed.Get}_{DateTime.Now.Ticks}";
// Act
service.HandlePaymentSuccessAsync(order.OrderNo, transactionId, order.Amount).GetAwaiter().GetResult();
// Refresh the order from database
var updatedOrder = service.GetOrderByNoAsync(order.OrderNo).GetAwaiter().GetResult();
// Assert: RewardData should be stored
return updatedOrder != null && updatedOrder.RewardData == rewardData;
}
#endregion
#region Helper Methods
/// <summary>
/// 创建内存数据库上下文
/// </summary>
private MiAssessmentDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new MiAssessmentDbContext(options);
}
/// <summary>
/// 创建 PaymentOrderService 实例
/// </summary>
private PaymentOrderService CreateService(MiAssessmentDbContext dbContext, IEnumerable<IPaymentRewardHandler>? handlers = null)
{
return new PaymentOrderService(
dbContext,
handlers ?? Array.Empty<IPaymentRewardHandler>(),
_mockLogger.Object);
}
/// <summary>
/// 创建有效的支付订单请求
/// </summary>
private CreatePaymentOrderRequest CreateValidRequest(int userId, int seed)
{
return new CreatePaymentOrderRequest
{
UserId = Math.Max(1, userId), // Ensure positive user ID
OrderType = $"test_order_{seed % 100}",
Title = $"测试订单 {seed}",
Amount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2), // 1.00 to 101.00
PayMethod = "wechat",
BizData = $"{{\"seed\":{seed}}}"
};
}
/// <summary>
/// 生成有效的订单类型(只包含字母、数字和下划线)
/// </summary>
private string GenerateValidOrderType(string input, int seed)
{
// Filter to only alphanumeric and underscore characters
var filtered = new string(input.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
// Ensure it's not empty and has reasonable length
if (string.IsNullOrEmpty(filtered))
{
filtered = "order";
}
// Limit length and add seed for uniqueness
var maxLength = Math.Min(filtered.Length, 20);
return $"{filtered.Substring(0, maxLength)}_{seed % 1000}";
}
#endregion
}

View File

@ -1,540 +0,0 @@
using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Core.Interfaces;
using MiAssessment.Core.Services;
using MiAssessment.Model.Entities;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Core;
/// <summary>
/// PaymentRewardDispatcher 属性测试
/// Feature: framework-template
///
/// Property 5: Reward Handler Dispatch
/// *For any* payment order with a specific order_type:
/// - The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
/// - If a handler is found, its ProcessRewardAsync method SHALL be called with the order
/// - The handler's RewardResult SHALL determine the order's reward_status and reward_data
///
/// **Validates: Requirements 6.2, 6.3**
/// </summary>
public class PaymentRewardDispatcherPropertyTests
{
private readonly Mock<ILogger<PaymentRewardDispatcher>> _mockLogger = new();
#region Property 5: Reward Handler Dispatch
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.1: Handler found for order type → handler's ProcessRewardAsync is called
/// If a handler is found, its ProcessRewardAsync method SHALL be called with the order
///
/// **Validates: Requirements 6.2, 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithMatchingHandler_ShouldCallProcessRewardAsync(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var handlerCalled = false;
PaymentOrder? receivedOrder = null;
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback<PaymentOrder>(order =>
{
handlerCalled = true;
receivedOrder = order;
})
.ReturnsAsync(RewardResult.Ok());
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Handler should be called with the correct order
return handlerCalled && receivedOrder != null && receivedOrder.OrderNo == paymentOrder.OrderNo;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.2: Handler not found → returns success with no reward data
/// If no handler is found for the order type, the system should return success (not failure)
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithNoMatchingHandler_ShouldReturnSuccessWithNoRewardData(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var differentOrderType = $"different_{orderType}";
// Create a handler for a different order type
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(differentOrderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok("should_not_be_called"));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should return success with no reward data
return result.Success && result.RewardData == null;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.3: Handler returns success → result contains reward data
/// The handler's RewardResult SHALL determine the order's reward_data
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithSuccessfulHandler_ShouldReturnRewardData(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var expectedRewardData = $"{{\"diamonds\":{seed.Get % 1000 + 100},\"bonus\":{seed.Get % 50}}}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok(expectedRewardData));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Result should contain the reward data from handler
return result.Success && result.RewardData == expectedRewardData;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.4: Handler returns failure → result contains error message
/// The handler's RewardResult SHALL determine the order's reward_status
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithFailedHandler_ShouldReturnErrorMessage(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var expectedErrorMessage = $"Reward processing failed for seed {seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Fail(expectedErrorMessage));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Result should contain the error message from handler
return !result.Success && result.Message == expectedErrorMessage;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.5: Handler throws exception → result contains error message
/// If the handler throws an exception, the dispatcher should catch it and return failure
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithExceptionThrowingHandler_ShouldReturnErrorMessage(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var exceptionMessage = $"Unexpected error for seed {seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ThrowsAsync(new InvalidOperationException(exceptionMessage));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, orderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Result should be failure and contain error message
return !result.Success && result.Message != null && result.Message.Contains(exceptionMessage);
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.6: Multiple handlers registered → correct handler is selected
/// The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithMultipleHandlers_ShouldSelectCorrectHandler(PositiveInt seed)
{
// Arrange
var targetOrderType = GenerateValidOrderType(seed.Get);
var otherOrderType1 = $"other1_{seed.Get}";
var otherOrderType2 = $"other2_{seed.Get}";
var targetRewardData = $"{{\"target_reward\":{seed.Get}}}";
var targetHandlerCalled = false;
var otherHandler1Called = false;
var otherHandler2Called = false;
// Create target handler
var targetHandler = new Mock<IPaymentRewardHandler>();
targetHandler.Setup(h => h.OrderType).Returns(targetOrderType);
targetHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => targetHandlerCalled = true)
.ReturnsAsync(RewardResult.Ok(targetRewardData));
// Create other handlers
var otherHandler1 = new Mock<IPaymentRewardHandler>();
otherHandler1.Setup(h => h.OrderType).Returns(otherOrderType1);
otherHandler1.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => otherHandler1Called = true)
.ReturnsAsync(RewardResult.Ok("other1_data"));
var otherHandler2 = new Mock<IPaymentRewardHandler>();
otherHandler2.Setup(h => h.OrderType).Returns(otherOrderType2);
otherHandler2.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.Callback(() => otherHandler2Called = true)
.ReturnsAsync(RewardResult.Ok("other2_data"));
var dispatcher = CreateDispatcher(new[]
{
otherHandler1.Object,
targetHandler.Object,
otherHandler2.Object
});
var paymentOrder = CreatePaymentOrder(seed.Get, targetOrderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Only target handler should be called, result should match target handler's output
return targetHandlerCalled &&
!otherHandler1Called &&
!otherHandler2Called &&
result.Success &&
result.RewardData == targetRewardData;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.7: GetHandler returns correct handler for registered order type
/// The system SHALL search for a registered IPaymentRewardHandler with matching OrderType
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool GetHandler_WithRegisteredOrderType_ShouldReturnCorrectHandler(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var handler = dispatcher.GetHandler(orderType);
// Assert: Should return the registered handler
return handler != null && handler.OrderType == orderType;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.8: GetHandler returns null for unregistered order type
/// If no handler is found, GetHandler should return null
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool GetHandler_WithUnregisteredOrderType_ShouldReturnNull(PositiveInt seed)
{
// Arrange
var registeredOrderType = GenerateValidOrderType(seed.Get);
var unregisteredOrderType = $"unregistered_{seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(registeredOrderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var handler = dispatcher.GetHandler(unregisteredOrderType);
// Assert: Should return null for unregistered order type
return handler == null;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.9: HasHandler returns true for registered order type
/// The system should correctly identify if a handler exists for an order type
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool HasHandler_WithRegisteredOrderType_ShouldReturnTrue(PositiveInt seed)
{
// Arrange
var orderType = GenerateValidOrderType(seed.Get);
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(orderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var hasHandler = dispatcher.HasHandler(orderType);
// Assert: Should return true for registered order type
return hasHandler;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.10: HasHandler returns false for unregistered order type
/// The system should correctly identify if no handler exists for an order type
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool HasHandler_WithUnregisteredOrderType_ShouldReturnFalse(PositiveInt seed)
{
// Arrange
var registeredOrderType = GenerateValidOrderType(seed.Get);
var unregisteredOrderType = $"unregistered_{seed.Get}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(registeredOrderType);
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
// Act
var hasHandler = dispatcher.HasHandler(unregisteredOrderType);
// Assert: Should return false for unregistered order type
return !hasHandler;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.11: GetRegisteredOrderTypes returns all registered order types
/// The system should track all registered handlers
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool GetRegisteredOrderTypes_ShouldReturnAllRegisteredTypes(PositiveInt seed)
{
// Arrange
var orderType1 = $"type1_{seed.Get}";
var orderType2 = $"type2_{seed.Get}";
var orderType3 = $"type3_{seed.Get}";
var handler1 = new Mock<IPaymentRewardHandler>();
handler1.Setup(h => h.OrderType).Returns(orderType1);
var handler2 = new Mock<IPaymentRewardHandler>();
handler2.Setup(h => h.OrderType).Returns(orderType2);
var handler3 = new Mock<IPaymentRewardHandler>();
handler3.Setup(h => h.OrderType).Returns(orderType3);
var dispatcher = CreateDispatcher(new[]
{
handler1.Object,
handler2.Object,
handler3.Object
});
// Act
var registeredTypes = dispatcher.GetRegisteredOrderTypes();
// Assert: Should contain all registered order types
return registeredTypes.Count == 3 &&
registeredTypes.Contains(orderType1) &&
registeredTypes.Contains(orderType2) &&
registeredTypes.Contains(orderType3);
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.12: ProcessReward with null order returns failure
/// The dispatcher should handle null orders gracefully
///
/// **Validates: Requirements 6.3**
/// </summary>
[Fact]
public void ProcessReward_WithNullOrder_ShouldReturnFailure()
{
// Arrange
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
// Act
var result = dispatcher.ProcessRewardAsync(null!).GetAwaiter().GetResult();
// Assert: Should return failure
Assert.False(result.Success);
Assert.NotNull(result.Message);
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.13: ProcessReward with empty order type returns failure
/// The dispatcher should handle orders with empty order type gracefully
///
/// **Validates: Requirements 6.3**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithEmptyOrderType_ShouldReturnFailure(PositiveInt seed)
{
// Arrange
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
var paymentOrder = CreatePaymentOrder(seed.Get, "");
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should return failure
return !result.Success && result.Message != null;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.14: Order type matching is case-insensitive
/// The system should match order types regardless of case
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_OrderTypeMatching_ShouldBeCaseInsensitive(PositiveInt seed)
{
// Arrange
var lowerCaseOrderType = $"order_type_{seed.Get}".ToLowerInvariant();
var upperCaseOrderType = lowerCaseOrderType.ToUpperInvariant();
var expectedRewardData = $"{{\"reward\":{seed.Get}}}";
var mockHandler = new Mock<IPaymentRewardHandler>();
mockHandler.Setup(h => h.OrderType).Returns(lowerCaseOrderType);
mockHandler.Setup(h => h.ProcessRewardAsync(It.IsAny<PaymentOrder>()))
.ReturnsAsync(RewardResult.Ok(expectedRewardData));
var dispatcher = CreateDispatcher(new[] { mockHandler.Object });
var paymentOrder = CreatePaymentOrder(seed.Get, upperCaseOrderType);
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should find handler despite case difference
return result.Success && result.RewardData == expectedRewardData;
}
/// <summary>
/// Feature: framework-template, Property 5: Reward Handler Dispatch
///
/// Property 5.15: Empty handlers collection returns success for any order
/// When no handlers are registered, processing should succeed with no reward
///
/// **Validates: Requirements 6.2**
/// </summary>
[Property(MaxTest = 100)]
public bool ProcessReward_WithNoHandlers_ShouldReturnSuccess(PositiveInt seed)
{
// Arrange
var dispatcher = CreateDispatcher(Array.Empty<IPaymentRewardHandler>());
var paymentOrder = CreatePaymentOrder(seed.Get, GenerateValidOrderType(seed.Get));
// Act
var result = dispatcher.ProcessRewardAsync(paymentOrder).GetAwaiter().GetResult();
// Assert: Should return success with no reward data
return result.Success && result.RewardData == null;
}
#endregion
#region Helper Methods
/// <summary>
/// 创建 PaymentRewardDispatcher 实例
/// </summary>
private PaymentRewardDispatcher CreateDispatcher(IEnumerable<IPaymentRewardHandler> handlers)
{
return new PaymentRewardDispatcher(handlers, _mockLogger.Object);
}
/// <summary>
/// 创建测试用的 PaymentOrder
/// </summary>
private PaymentOrder CreatePaymentOrder(int seed, string orderType)
{
return new PaymentOrder
{
Id = seed,
OrderNo = $"PO{DateTime.Now:yyyyMMddHHmmss}{seed:D6}",
UserId = Math.Max(1, seed % 10000),
OrderType = orderType,
Title = $"测试订单 {seed}",
Amount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2),
PayAmount = Math.Round((decimal)((seed % 10000) + 100) / 100, 2),
PayMethod = "wechat",
Status = 1, // 已支付
PaidAt = DateTime.Now,
TransactionId = $"TX_{seed}",
BizData = $"{{\"seed\":{seed}}}",
RewardStatus = 0,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
}
/// <summary>
/// 生成有效的订单类型
/// </summary>
private string GenerateValidOrderType(int seed)
{
var types = new[] { "diamond_recharge", "vip_purchase", "gift_buy", "subscription", "premium" };
return $"{types[seed % types.Length]}_{seed % 1000}";
}
#endregion
}

View File

@ -87,28 +87,18 @@ public class AuthServiceLoginRecordPropertyTests
Adcode = "110000"
});
// Create a user
// 创建用户
var user = new User
{
OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
Uid = "uid123",
Nickname = "TestUser",
HeadImg = "https://example.com/avatar.jpg",
Avatar = "https://example.com/avatar.jpg",
Status = 1,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
await dbContext.Users.AddAsync(user);
// Create UserAccount for the user
var userAccount = new UserAccount
{
UserId = user.Id,
AccountToken = "token123",
TokenNum = "num123",
LastLoginIp = string.Empty
};
await dbContext.UserAccounts.AddAsync(userAccount);
await dbContext.SaveChangesAsync();
var device = "iOS";
@ -118,17 +108,15 @@ public class AuthServiceLoginRecordPropertyTests
await authService.RecordLoginAsync(user.Id, device, clientIp);
// Assert
// 1. UserLoginLog should be created
// 1. UserLoginLog 应该被创建
var loginLog = await dbContext.UserLoginLogs.FirstOrDefaultAsync(l => l.UserId == user.Id);
var logCreated = loginLog != null && loginLog.Device == device;
var logCreated = loginLog != null && loginLog.UserAgent == device;
// 2. UserAccount should be updated
var updatedAccount = await dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == user.Id);
var accountUpdated = updatedAccount != null &&
updatedAccount.LastLoginTime != null &&
updatedAccount.IpProvince == "北京";
// 2. 用户最后登录时间应该被更新
var updatedUser = await dbContext.Users.FirstOrDefaultAsync(u => u.Id == user.Id);
var userUpdated = updatedUser != null && updatedUser.LastLoginTime != null;
return logCreated && accountUpdated;
return logCreated && userUpdated;
}
/// <summary>
@ -153,16 +141,16 @@ public class AuthServiceLoginRecordPropertyTests
var expectedNickname = "TestUser_" + Random.Shared.Next(1000, 9999);
var expectedHeadimg = "https://example.com/avatar_" + Random.Shared.Next(1000, 9999) + ".jpg";
// Create a user
// 创建用户
var user = new User
{
OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
Uid = expectedUid,
Nickname = expectedNickname,
HeadImg = expectedHeadimg,
Avatar = expectedHeadimg,
Status = 1,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
@ -197,10 +185,10 @@ public class AuthServiceLoginRecordPropertyTests
OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
Uid = "uid123",
Nickname = "TestUser",
HeadImg = "https://example.com/avatar.jpg",
Avatar = "https://example.com/avatar.jpg",
Status = 1, // Active
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
await dbContext1.Users.AddAsync(user1);
await dbContext1.SaveChangesAsync();
@ -221,10 +209,10 @@ public class AuthServiceLoginRecordPropertyTests
OpenId = "openid_" + Guid.NewGuid().ToString().Substring(0, 8),
Uid = "uid456",
Nickname = "TestUser2",
HeadImg = "https://example.com/avatar.jpg",
Avatar = "https://example.com/avatar.jpg",
Status = 0, // Inactive
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now
};
await dbContext2.Users.AddAsync(user2);
await dbContext2.SaveChangesAsync();