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:
parent
a595eee90d
commit
21e8ff5372
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
// 删除当前用户
|
||||
|
|
|
|||
|
|
@ -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<MyRewardHandler>()
|
||||
/// .As<IPaymentRewardHandler>()
|
||||
/// .InstancePerLifetimeScope();
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// 实现钻石充值奖励处理器示例:
|
||||
/// <code>
|
||||
/// public class DiamondRechargeRewardHandler : IPaymentRewardHandler
|
||||
/// {
|
||||
/// public string OrderType => "diamond_recharge";
|
||||
///
|
||||
/// public async Task<RewardResult> ProcessRewardAsync(PaymentOrder order)
|
||||
/// {
|
||||
/// // 1. 解析业务数据
|
||||
/// var bizData = JsonSerializer.Deserialize<DiamondRechargeData>(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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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, // 业务字段已移除,返回默认值
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
// ========== 配置系统服务注册 ==========
|
||||
|
||||
// 注册配置服务
|
||||
|
|
|
|||
|
|
@ -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("更新时间");
|
||||
});
|
||||
|
||||
// ==================== 小程序业务表配置 ====================
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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!;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user