feat(invite): 邀请二维码改为COS存储,优化内存占用

- users表新增InviteQrcodeUrl字段,永久保存二维码URL
- InviteService改为:查用户字段→调微信接口→上传COS→存库
- 去掉Redis缓存base64图片数据的逻辑
- IUploadConfigService新增UploadFileAsync后端直传方法
- 前端进入邀请页自动预加载二维码URL
- 包含之前的小程序名称配置、二维码生成、邀请人绑定等功能
This commit is contained in:
zpc 2026-03-25 01:34:06 +08:00
parent 50a3d7e67d
commit 4bf412af51
17 changed files with 1010 additions and 922 deletions

View File

@ -1,15 +0,0 @@
{
"enabled": true,
"name": "Figma Component Code Connect",
"description": "Check if UI component should be connected to Figma design",
"version": "1",
"when": {
"type": "fileEdited",
"patterns": ["uniapp/**/*.vue", "uniapp/components/**/*"]
},
"then": {
"type": "askAgent",
"prompt": "When a new component file is created or updated, ask the user if they would like to confirm if the code has been correctly attached to the Figma component of the same name. If the user approves: first run the get code connect map tool for the last Figma URL provided by the user. You can prompt them to provide it again if it's unavailable. If the response is empty, run the add code connect map tool, otherwise tell the user they already have code mapped to that component. If the user rejects: Do not run any additional tools."
},
"shortName": "figma-code-connect"
}

View File

@ -137,4 +137,30 @@ public class SystemController : ControllerBase
return ApiResponse<ContactDto>.Fail("获取联系我们信息失败");
}
}
/// <summary>
/// 获取小程序信息
/// </summary>
/// <remarks>
/// GET /api/system/getMiniappInfo
///
/// 从配置表读取小程序名称和简介
/// 不需要用户登录认证
/// </remarks>
/// <returns>小程序名称和简介</returns>
[HttpGet("getMiniappInfo")]
[ProducesResponseType(typeof(ApiResponse<MiniappInfoDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<MiniappInfoDto>> GetMiniappInfo()
{
try
{
var info = await _systemService.GetMiniappInfoAsync();
return ApiResponse<MiniappInfoDto>.Success(info);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get miniapp info");
return ApiResponse<MiniappInfoDto>.Fail("获取小程序信息失败");
}
}
}

View File

@ -42,4 +42,13 @@ public interface ISystemService
/// </remarks>
/// <returns>联系我们数据</returns>
Task<ContactDto> GetContactInfoAsync();
/// <summary>
/// 获取小程序信息
/// </summary>
/// <remarks>
/// 从配置表读取小程序名称config_key: miniapp_name和简介config_key: miniapp_intro
/// </remarks>
/// <returns>小程序信息数据</returns>
Task<MiniappInfoDto> GetMiniappInfoAsync();
}

View File

@ -13,6 +13,15 @@ public interface IUploadConfigService
/// <param name="contentType">文件MIME类型</param>
/// <returns>预签名URL信息null表示不支持COS直传</returns>
Task<PresignedUploadInfo?> GetPresignedUploadUrlAsync(string fileName, string contentType);
/// <summary>
/// 后端直传文件到COS
/// </summary>
/// <param name="fileBytes">文件字节数据</param>
/// <param name="fileName">文件名(含扩展名)</param>
/// <param name="contentType">MIME类型</param>
/// <returns>文件访问URL失败返回null</returns>
Task<string?> UploadFileAsync(byte[] fileBytes, string fileName, string contentType);
}
/// <summary>

View File

@ -1,3 +1,5 @@
using System.Text;
using System.Text.Json;
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Models.Common;
@ -24,6 +26,11 @@ public class InviteService : IInviteService
private readonly MiAssessmentDbContext _dbContext;
private readonly ILogger<InviteService> _logger;
private readonly IWechatService _wechatService;
private readonly HttpClient _httpClient;
private readonly IUploadConfigService _uploadConfigService;
// 微信小程序码API
private const string WxacodeUnlimitUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit";
/// <summary>
/// 构造函数
@ -31,14 +38,20 @@ public class InviteService : IInviteService
/// <param name="dbContext">数据库上下文</param>
/// <param name="logger">日志记录器</param>
/// <param name="wechatService">微信服务</param>
/// <param name="httpClient">HTTP客户端</param>
/// <param name="uploadConfigService">上传配置服务</param>
public InviteService(
MiAssessmentDbContext dbContext,
ILogger<InviteService> logger,
IWechatService wechatService)
IWechatService wechatService,
HttpClient httpClient,
IUploadConfigService uploadConfigService)
{
_dbContext = dbContext;
_logger = logger;
_wechatService = wechatService;
_httpClient = httpClient;
_uploadConfigService = uploadConfigService;
}
/// <inheritdoc />
@ -104,13 +117,11 @@ public class InviteService : IInviteService
/// </remarks>
public async Task<QrcodeDto> GetQrcodeAsync(long userId)
{
_logger.LogDebug("生成邀请二维码userId: {UserId}", userId);
_logger.LogDebug("获取邀请二维码userId: {UserId}", userId);
// 查询用户信息获取邀请码
// 查询用户检查是否已有二维码URL
var user = await _dbContext.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new { u.Uid, u.InviteCode })
.FirstOrDefaultAsync();
if (user == null)
@ -119,6 +130,13 @@ public class InviteService : IInviteService
throw new InvalidOperationException("用户不存在");
}
// 已有二维码URL直接返回
if (!string.IsNullOrEmpty(user.InviteQrcodeUrl))
{
_logger.LogDebug("用户已有邀请二维码userId: {UserId}", userId);
return new QrcodeDto { QrcodeUrl = user.InviteQrcodeUrl };
}
// 获取微信access_token
var accessToken = await _wechatService.GetAccessTokenAsync();
if (string.IsNullOrEmpty(accessToken))
@ -127,22 +145,62 @@ public class InviteService : IInviteService
throw new InvalidOperationException("获取微信access_token失败");
}
// 生成小程序码
// 使用用户UID作为邀请参数
var scene = $"inviter={user.Uid}";
// 调用微信 getwxacodeunlimit 接口
var scene = $"inviter={user.Id}";
var page = "pages/index/index";
// 调用微信小程序码接口
// 这里返回一个占位URL实际实现需要调用微信API生成二维码图片
// 并上传到OSS或返回base64
var qrcodeUrl = $"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={accessToken}&scene={scene}&page={page}";
_logger.LogDebug("生成邀请二维码成功userId: {UserId}, scene: {Scene}", userId, scene);
return new QrcodeDto
var requestBody = new
{
QrcodeUrl = qrcodeUrl
scene,
page,
width = 430,
auto_color = false,
line_color = new { r = 0, g = 0, b = 0 },
is_hyaline = false
};
var jsonContent = new StringContent(
JsonSerializer.Serialize(requestBody),
Encoding.UTF8,
"application/json");
var url = $"{WxacodeUnlimitUrl}?access_token={accessToken}";
var response = await _httpClient.PostAsync(url, jsonContent);
if (!response.IsSuccessStatusCode)
{
_logger.LogError("微信小程序码接口返回错误状态: {StatusCode}", response.StatusCode);
throw new InvalidOperationException("生成二维码失败");
}
var respContentType = response.Content.Headers.ContentType?.MediaType ?? "";
var responseBytes = await response.Content.ReadAsByteArrayAsync();
// 微信错误响应是JSON
if (respContentType.Contains("json") || (responseBytes.Length > 0 && responseBytes[0] == (byte)'{'))
{
var errorJson = Encoding.UTF8.GetString(responseBytes);
_logger.LogError("微信小程序码接口返回错误: {Error}", errorJson);
throw new InvalidOperationException("生成二维码失败,请检查小程序配置");
}
// 上传到COS
var fileName = $"invite_qrcode_{userId}.png";
var cosUrl = await _uploadConfigService.UploadFileAsync(responseBytes, fileName, "image/png");
if (string.IsNullOrEmpty(cosUrl))
{
_logger.LogError("二维码上传COS失败userId: {UserId}", userId);
throw new InvalidOperationException("二维码上传失败");
}
// 保存URL到用户表
user.InviteQrcodeUrl = cosUrl;
user.UpdateTime = DateTime.Now;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("生成邀请二维码成功userId: {UserId}, url: {Url}", userId, cosUrl);
return new QrcodeDto { QrcodeUrl = cosUrl };
}
/// <inheritdoc />

View File

@ -18,6 +18,8 @@ public class SystemService : ISystemService
private const string AboutUsKey = "about_us_content";
private const string AppVersionKey = "app_version";
private const string ServiceQrcodeKey = "service_qrcode";
private const string MiniappNameKey = "miniapp_name";
private const string MiniappIntroKey = "miniapp_intro";
// 默认版本号
private const string DefaultVersion = "1.0.0";
@ -103,4 +105,26 @@ public class SystemService : ISystemService
QrcodeUrl = qrcodeUrl ?? string.Empty
};
}
/// <inheritdoc />
public async Task<MiniappInfoDto> GetMiniappInfoAsync()
{
_logger.LogDebug("获取小程序信息");
var nameTask = _configService.GetConfigValueAsync(MiniappNameKey);
var introTask = _configService.GetConfigValueAsync(MiniappIntroKey);
await Task.WhenAll(nameTask, introTask);
var name = await nameTask;
var intro = await introTask;
_logger.LogDebug("获取小程序信息完成,名称: {Name}, 简介: {Intro}", name, intro);
return new MiniappInfoDto
{
Name = name ?? string.Empty,
Intro = intro ?? string.Empty
};
}
}

View File

@ -18,6 +18,7 @@ public class UploadConfigService : IUploadConfigService
private readonly AdminConfigReadDbContext _adminConfigDbContext;
private readonly IRedisService _redisService;
private readonly ILogger<UploadConfigService> _logger;
private readonly HttpClient _httpClient;
private const string CacheKey = "upload:setting";
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
@ -27,11 +28,13 @@ public class UploadConfigService : IUploadConfigService
public UploadConfigService(
AdminConfigReadDbContext adminConfigDbContext,
IRedisService redisService,
ILogger<UploadConfigService> logger)
ILogger<UploadConfigService> logger,
HttpClient httpClient)
{
_adminConfigDbContext = adminConfigDbContext;
_redisService = redisService;
_logger = logger;
_httpClient = httpClient;
}
/// <inheritdoc />
@ -74,6 +77,64 @@ public class UploadConfigService : IUploadConfigService
};
}
/// <inheritdoc />
public async Task<string?> UploadFileAsync(byte[] fileBytes, string fileName, string contentType)
{
var setting = await GetUploadSettingAsync();
if (setting == null || setting.Type != "3")
{
_logger.LogWarning("上传配置不支持COS当前类型: {Type}", setting?.Type);
return null;
}
var validationError = ValidateConfig(setting);
if (validationError != null)
{
_logger.LogWarning("COS配置验证失败: {Error}", validationError);
return null;
}
// 生成日期目录和唯一文件名
var now = DateTime.Now;
var datePath = $"{now.Year}/{now.Month:D2}/{now.Day:D2}";
var extension = Path.GetExtension(fileName).ToLowerInvariant();
var timestamp = now.ToString("yyyyMMddHHmmssfff");
var guid = Guid.NewGuid().ToString("N")[..8];
var uniqueFileName = $"{timestamp}_{guid}{extension}";
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
// 生成预签名URL用于PUT上传
var presignedUrl = GeneratePresignedUrl(setting, objectKey, "PUT", contentType, DefaultExpiresInSeconds);
try
{
using var content = new ByteArrayContent(fileBytes);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
using var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl);
request.Content = content;
request.Headers.Host = host;
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
_logger.LogError("COS上传失败: {StatusCode}, {Error}", response.StatusCode, errorBody);
return null;
}
var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey);
_logger.LogInformation("COS上传成功: {ObjectKey}", objectKey);
return fileUrl;
}
catch (Exception ex)
{
_logger.LogError(ex, "COS上传异常: {FileName}", fileName);
return null;
}
}
/// <summary>
/// 生成腾讯云COS预签名URLPUT方式
/// </summary>

View File

@ -227,7 +227,9 @@ public class ServiceModule : Module
var dbContext = c.Resolve<MiAssessmentDbContext>();
var logger = c.Resolve<ILogger<InviteService>>();
var wechatService = c.Resolve<IWechatService>();
return new InviteService(dbContext, logger, wechatService);
var httpClientFactory = c.Resolve<System.Net.Http.IHttpClientFactory>();
var uploadConfigService = c.Resolve<IUploadConfigService>();
return new InviteService(dbContext, logger, wechatService, httpClientFactory.CreateClient(), uploadConfigService);
}).As<IInviteService>().InstancePerLifetimeScope();
// ========== 小程序系统模块服务注册 ==========
@ -248,7 +250,8 @@ public class ServiceModule : Module
var adminConfigDbContext = c.Resolve<AdminConfigReadDbContext>();
var redisService = c.Resolve<IRedisService>();
var logger = c.Resolve<ILogger<UploadConfigService>>();
return new UploadConfigService(adminConfigDbContext, redisService, logger);
var httpClientFactory = c.Resolve<System.Net.Http.IHttpClientFactory>();
return new UploadConfigService(adminConfigDbContext, redisService, logger, httpClientFactory.CreateClient());
}).As<IUploadConfigService>().InstancePerLifetimeScope();
// ========== 小程序团队模块服务注册 ==========

View File

@ -125,6 +125,12 @@ public partial class User
/// </summary>
public DateTime UpdateTime { get; set; }
/// <summary>
/// 邀请二维码URLCOS存储地址
/// </summary>
[MaxLength(500)]
public string? InviteQrcodeUrl { get; set; }
/// <summary>
/// 软删除标记
/// </summary>

View File

@ -0,0 +1,17 @@
namespace MiAssessment.Model.Models.System;
/// <summary>
/// 小程序信息数据传输对象
/// </summary>
public class MiniappInfoDto
{
/// <summary>
/// 小程序名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 小程序简介
/// </summary>
public string Intro { get; set; } = string.Empty;
}

View File

@ -1,3 +1,4 @@
using System.Net.Http;
using FsCheck;
using FsCheck.Xunit;
using Microsoft.EntityFrameworkCore;
@ -12,21 +13,23 @@ using Xunit;
namespace MiAssessment.Tests.Services;
/// <summary>
/// InviteService 属性测
/// 验证分销服务的分页查询一致
/// InviteService 属性测<EFBFBD>?
/// 验证分销服务的分页查询一致<EFBFBD>?
/// </summary>
public class InviteServicePropertyTests
{
private readonly Mock<ILogger<InviteService>> _mockLogger = new();
private readonly Mock<IWechatService> _mockWechatService = new();
private readonly HttpClient _httpClient = new();
private readonly Mock<IRedisService> _mockRedisService = new();
#region Property 3: - GetRecordListAsync
#region Property 3: <EFBFBD>?- GetRecordListAsync
/// <summary>
/// Property 3: 邀请记录分页查询返回的记录数不超过pageSize
/// *For any* paginated query, the returned items count SHALL not exceed pageSize.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -41,7 +44,7 @@ public class InviteServicePropertyTests
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建多个下级用户超过pageSize
// 创建多个下级用户超过pageSize<EFBFBD>?
var invitedUserCount = pageSize + 10;
for (int i = 0; i < invitedUserCount; i++)
{
@ -52,7 +55,7 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetRecordListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
@ -65,7 +68,7 @@ public class InviteServicePropertyTests
/// Property 3: 邀请记录分页查询的Total等于满足条件的总记录数
/// *For any* paginated query, Total SHALL equal the count of all matching records.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -80,11 +83,11 @@ public class InviteServicePropertyTests
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建另一个用
// 创建另一个用<EFBFBD>?
var otherUser = CreateUser(otherUserId, seed.Get + 50000);
dbContext.Users.Add(otherUser);
// 为当前用户创建下级用
// 为当前用户创建下级用<EFBFBD>?
var userInvitedCount = Math.Max(1, seed.Get % 15 + 1); // 1-15
for (int i = 0; i < userInvitedCount; i++)
{
@ -93,7 +96,7 @@ public class InviteServicePropertyTests
dbContext.Users.Add(invitedUser);
}
// 为其他用户创建下级用户不应计入Total
// 为其他用户创建下级用户不应计入Total<EFBFBD>?
for (int i = 0; i < 5; i++)
{
var otherInvitedUser = CreateUser(otherUserId + 1000 + i, seed.Get + 60000 + i);
@ -103,7 +106,7 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetRecordListAsync(userId, 1, 20).GetAwaiter().GetResult();
@ -113,10 +116,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 3: 遍历所有页面能获取所有满足条件的邀请记
/// Property 3: 遍历所有页面能获取所有满足条件的邀请记<EFBFBD>?
/// *For any* paginated query, traversing all pages SHALL return all matching records.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 50)]
@ -125,7 +128,7 @@ public class InviteServicePropertyTests
// Arrange
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
var pageSize = Math.Max(1, seed.Get % 5 + 1); // 1-5 (小pageSize以测试多页)
var pageSize = Math.Max(1, seed.Get % 5 + 1); // 1-5 (小pageSize以测试多<EFBFBD>?
// 创建当前用户
var currentUser = CreateUser(userId, seed.Get);
@ -144,9 +147,9 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 遍历所有页
// Act: 遍历所有页<EFBFBD>?
var allRetrievedIds = new HashSet<long>();
var page = 1;
var maxPages = (totalRecords / pageSize) + 2; // 防止无限循环
@ -173,7 +176,7 @@ public class InviteServicePropertyTests
/// Property 3: 邀请记录分页查询的TotalPages计算正确
/// *For any* paginated query, TotalPages SHALL equal ceil(Total / PageSize).
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -199,7 +202,7 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetRecordListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
@ -210,10 +213,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 3: 邀请记录分页查询不同页面返回不重复的记
/// Property 3: 邀请记录分页查询不同页面返回不重复的记<EFBFBD>?
/// *For any* paginated query, different pages SHALL return non-overlapping records.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 50)]
@ -222,13 +225,13 @@ public class InviteServicePropertyTests
// Arrange
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
var pageSize = 3; // 固定pageSize以确保多
var pageSize = 3; // 固定pageSize以确保多<EFBFBD>?
// 创建当前用户
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建足够多的下级用户以产生多
// 创建足够多的下级用户以产生多<EFBFBD>?
var totalRecords = 10;
for (int i = 0; i < totalRecords; i++)
{
@ -239,13 +242,13 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 获取前两
// Act: 获取前两<EFBFBD>?
var page1 = service.GetRecordListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
var page2 = service.GetRecordListAsync(userId, 2, pageSize).GetAwaiter().GetResult();
// Assert: 两页的记录ID不重
// Assert: 两页的记录ID不重<EFBFBD>?
var page1Ids = page1.List.Select(r => r.UserId).ToHashSet();
var page2Ids = page2.List.Select(r => r.UserId).ToHashSet();
@ -254,13 +257,13 @@ public class InviteServicePropertyTests
#endregion
#region Property 3: - GetWithdrawListAsync
#region Property 3: <EFBFBD>?- GetWithdrawListAsync
/// <summary>
/// Property 3: 提现记录分页查询返回的记录数不超过pageSize
/// *For any* paginated query, the returned items count SHALL not exceed pageSize.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -275,7 +278,7 @@ public class InviteServicePropertyTests
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建多条提现记录超过pageSize
// 创建多条提现记录超过pageSize<EFBFBD>?
var withdrawalCount = pageSize + 10;
for (int i = 0; i < withdrawalCount; i++)
{
@ -284,7 +287,7 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetWithdrawListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
@ -297,7 +300,7 @@ public class InviteServicePropertyTests
/// Property 3: 提现记录分页查询的Total等于满足条件的总记录数
/// *For any* paginated query, Total SHALL equal the count of all matching records.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -312,24 +315,24 @@ public class InviteServicePropertyTests
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建另一个用
// 创建另一个用<EFBFBD>?
var otherUser = CreateUser(otherUserId, seed.Get + 50000);
dbContext.Users.Add(otherUser);
// 为当前用户创建提现记
// 为当前用户创建提现记<EFBFBD>?
var userWithdrawalCount = Math.Max(1, seed.Get % 15 + 1); // 1-15
for (int i = 0; i < userWithdrawalCount; i++)
{
dbContext.Withdrawals.Add(CreateWithdrawal(seed.Get + i, userId));
}
// 为其他用户创建提现记录不应计入Total
// 为其他用户创建提现记录不应计入Total<EFBFBD>?
for (int i = 0; i < 5; i++)
{
dbContext.Withdrawals.Add(CreateWithdrawal(seed.Get + 1000 + i, otherUserId));
}
// 创建已删除的提现记录不应计入Total
// 创建已删除的提现记录不应计入Total<EFBFBD>?
for (int i = 0; i < 3; i++)
{
var deletedWithdrawal = CreateWithdrawal(seed.Get + 2000 + i, userId);
@ -339,12 +342,12 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetWithdrawListAsync(userId, 1, 20).GetAwaiter().GetResult();
// Assert: Total等于当前用户未删除的提现记录
// Assert: Total等于当前用户未删除的提现记录<EFBFBD>?
return result.Total == userWithdrawalCount;
}
@ -352,7 +355,7 @@ public class InviteServicePropertyTests
/// Property 3: 遍历所有页面能获取所有满足条件的提现记录
/// *For any* paginated query, traversing all pages SHALL return all matching records.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 50)]
@ -361,7 +364,7 @@ public class InviteServicePropertyTests
// Arrange
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
var pageSize = Math.Max(1, seed.Get % 5 + 1); // 1-5 (小pageSize以测试多页)
var pageSize = Math.Max(1, seed.Get % 5 + 1); // 1-5 (小pageSize以测试多<EFBFBD>?
// 创建当前用户
var currentUser = CreateUser(userId, seed.Get);
@ -379,9 +382,9 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 遍历所有页
// Act: 遍历所有页<EFBFBD>?
var allRetrievedIds = new HashSet<long>();
var page = 1;
var maxPages = (totalRecords / pageSize) + 2; // 防止无限循环
@ -408,7 +411,7 @@ public class InviteServicePropertyTests
/// Property 3: 提现记录分页查询的TotalPages计算正确
/// *For any* paginated query, TotalPages SHALL equal ceil(Total / PageSize).
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -432,7 +435,7 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetWithdrawListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
@ -446,7 +449,7 @@ public class InviteServicePropertyTests
/// Property 3: 提现记录分页查询不同页面返回不重复的记录
/// *For any* paginated query, different pages SHALL return non-overlapping records.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 50)]
@ -455,13 +458,13 @@ public class InviteServicePropertyTests
// Arrange
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
var pageSize = 3; // 固定pageSize以确保多
var pageSize = 3; // 固定pageSize以确保多<EFBFBD>?
// 创建当前用户
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建足够多的提现记录以产生多
// 创建足够多的提现记录以产生多<EFBFBD>?
var totalRecords = 10;
for (int i = 0; i < totalRecords; i++)
{
@ -470,13 +473,13 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 获取前两
// Act: 获取前两<EFBFBD>?
var page1 = service.GetWithdrawListAsync(userId, 1, pageSize).GetAwaiter().GetResult();
var page2 = service.GetWithdrawListAsync(userId, 2, pageSize).GetAwaiter().GetResult();
// Assert: 两页的记录ID不重
// Assert: 两页的记录ID不重<EFBFBD>?
var page1Ids = page1.List.Select(r => r.Id).ToHashSet();
var page2Ids = page2.List.Select(r => r.Id).ToHashSet();
@ -484,10 +487,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 3: 提现记录分页查询不返回已删除的记
/// Property 3: 提现记录分页查询不返回已删除的记<EFBFBD>?
/// *For any* paginated query, deleted records (IsDeleted=true) SHALL not be returned.
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 100)]
@ -501,7 +504,7 @@ public class InviteServicePropertyTests
var currentUser = CreateUser(userId, seed.Get);
dbContext.Users.Add(currentUser);
// 创建正常的提现记
// 创建正常的提现记<EFBFBD>?
var normalWithdrawals = new List<Withdrawal>();
for (int i = 0; i < 3; i++)
{
@ -522,16 +525,16 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetWithdrawListAsync(userId, 1, 20).GetAwaiter().GetResult();
// Assert:
// 1. 返回的记录数等于正常记录
// 1. 返回的记录数等于正常记录<EFBFBD>?
if (result.List.Count != 3) return false;
// 2. 返回的记录中不包含已删除的记
// 2. 返回的记录中不包含已删除的记<EFBFBD>?
var returnedIds = result.List.Select(r => r.Id).ToHashSet();
var deletedIds = deletedWithdrawals.Select(w => w.Id).ToHashSet();
@ -540,12 +543,12 @@ public class InviteServicePropertyTests
#endregion
#region
#region <EFBFBD>?
/// <summary>
/// Property 3: 空数据库返回空列表 - 邀请记录
/// Property 3: 空数据库返回空列<EFBFBD>?- 邀请记<E8AFB7>?
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Fact]
@ -554,12 +557,12 @@ public class InviteServicePropertyTests
// Arrange
using var dbContext = CreateDbContext();
// 创建用户但没有下
// 创建用户但没有下<EFBFBD>?
var user = CreateUser(1, 1);
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetRecordListAsync(1, 1, 20).GetAwaiter().GetResult();
@ -570,9 +573,9 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 3: 空数据库返回空列- 提现记录
/// Property 3: 空数据库返回空列<EFBFBD>?- 提现记录
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Fact]
@ -581,12 +584,12 @@ public class InviteServicePropertyTests
// Arrange
using var dbContext = CreateDbContext();
// 创建用户但没有提现记
// 创建用户但没有提现记<EFBFBD>?
var user = CreateUser(1, 1);
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.GetWithdrawListAsync(1, 1, 20).GetAwaiter().GetResult();
@ -597,9 +600,9 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 3: 分页参数边界值处理 - 邀请记录
/// Property 3: 分页参数边界值处<EFBFBD>?- 邀请记<E8AFB7>?
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 50)]
@ -623,9 +626,9 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 测试边界- page=0 应该被处理为 page=1
// Act: 测试边界<EFBFBD>?- page=0 应该被处理为 page=1
var result = service.GetRecordListAsync(userId, 0, 10).GetAwaiter().GetResult();
// Assert: 应该返回第一页的数据
@ -633,9 +636,9 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 3: 分页参数边界值处- 提现记录
/// Property 3: 分页参数边界值处<EFBFBD>?- 提现记录
///
/// **Feature: miniapp-api, Property 3: 分页查询一致性**
/// **Feature: miniapp-api, Property 3: 分页查询一致<EFBFBD>?*
/// **Validates: Requirements 12.1, 13.5**
/// </summary>
[Property(MaxTest = 50)]
@ -657,9 +660,9 @@ public class InviteServicePropertyTests
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 测试边界- page=0 应该被处理为 page=1
// Act: 测试边界<EFBFBD>?- page=0 应该被处理为 page=1
var result = service.GetWithdrawListAsync(userId, 0, 10).GetAwaiter().GetResult();
// Assert: 应该返回第一页的数据
@ -668,13 +671,13 @@ public class InviteServicePropertyTests
#endregion
#region Property 7:
#region Property 7: <EFBFBD>?
/// <summary>
/// Property 7: 提现成功后用户余额等于原余额减去提现金额
/// *For any* successful withdrawal, user.Balance = originalBalance - withdrawalAmount.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -695,12 +698,12 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
// Assert: 提现成功后,用户余额 = 原余- 提现金额
// Assert: 提现成功后,用户余额 = 原余<EFBFBD>?- 提现金额
if (!result.Success) return false;
// 重新查询用户余额
@ -715,7 +718,7 @@ public class InviteServicePropertyTests
/// Property 7: 提现记录的BeforeBalance和AfterBalance正确记录变化
/// *For any* successful withdrawal, withdrawal record SHALL have correct BeforeBalance and AfterBalance.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -736,7 +739,7 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
@ -750,10 +753,10 @@ public class InviteServicePropertyTests
if (withdrawal == null) return false;
// 验证BeforeBalance等于原余
// 验证BeforeBalance等于原余<EFBFBD>?
if (withdrawal.BeforeBalance != originalBalance) return false;
// 验证AfterBalance等于原余额减去提现金
// 验证AfterBalance等于原余额减去提现金<EFBFBD>?
var expectedAfterBalance = originalBalance - withdrawAmount;
if (withdrawal.AfterBalance != expectedAfterBalance) return false;
@ -762,10 +765,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 7: 提现金额超过余额时提现失
/// Property 7: 提现金额超过余额时提现失<EFBFBD>?
/// *For any* withdrawal where amount > balance, withdrawal SHALL fail.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -775,7 +778,7 @@ public class InviteServicePropertyTests
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
// 设置余额为1-50之间的整数
// 设置余额<EFBFBD>?-50之间的整<E79A84>?
var balance = Math.Max(1, seed.Get % 50 + 1);
// 提现金额大于余额
var withdrawAmount = balance + Math.Max(1, seed.Get % 100 + 1);
@ -786,16 +789,16 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
// Assert: 提现失败,且错误信息包含"超出待提现金额"
// Assert: 提现失败,且错误信息包含"超出待提现金<EFBFBD>?
if (result.Success) return false;
if (string.IsNullOrEmpty(result.ErrorMessage)) return false;
// 验证用户余额未变
// 验证用户余额未变<EFBFBD>?
var updatedUser = dbContext.Users.Find((int)userId);
if (updatedUser == null) return false;
@ -806,7 +809,7 @@ public class InviteServicePropertyTests
/// Property 7: 提现金额小于1元时提现失败
/// *For any* withdrawal where amount < 1, withdrawal SHALL fail.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -816,9 +819,9 @@ public class InviteServicePropertyTests
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
// 设置足够的余
// 设置足够的余<EFBFBD>?
var balance = 100m + (seed.Get % 100);
// 提现金额小于1元0.01 - 0.99
// 提现金额小于1元0.01 - 0.99<EFBFBD>?
var withdrawAmount = (seed.Get % 99 + 1) / 100m;
// 创建用户
@ -827,7 +830,7 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
@ -845,10 +848,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 7: 提现金额不是整数时提现失
/// Property 7: 提现金额不是整数时提现失<EFBFBD>?
/// *For any* withdrawal where amount is not an integer, withdrawal SHALL fail.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -858,9 +861,9 @@ public class InviteServicePropertyTests
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
// 设置足够的余
// 设置足够的余<EFBFBD>?
var balance = 100m + (seed.Get % 100);
// 提现金额为非整数1.01 - 99.99
// 提现金额为非整数<EFBFBD>?.01 - 99.99<EFBFBD>?
var integerPart = Math.Max(1, seed.Get % 99 + 1);
var decimalPart = (seed.Get % 99 + 1) / 100m;
var withdrawAmount = integerPart + decimalPart;
@ -871,7 +874,7 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
@ -881,7 +884,7 @@ public class InviteServicePropertyTests
if (string.IsNullOrEmpty(result.ErrorMessage)) return false;
if (!result.ErrorMessage.Contains("整数")) return false;
// 验证用户余额未变
// 验证用户余额未变<EFBFBD>?
var updatedUser = dbContext.Users.Find((int)userId);
if (updatedUser == null) return false;
@ -889,10 +892,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 7: 提现成功后创建提现记
/// Property 7: 提现成功后创建提现记<EFBFBD>?
/// *For any* successful withdrawal, a withdrawal record SHALL be created.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -913,15 +916,15 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
// 记录提现前的提现记录
// 记录提现前的提现记录<EFBFBD>?
var beforeCount = dbContext.Withdrawals.Count(w => w.UserId == userId);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
// Assert: 提现成功后,提现记录数增加1
// Assert: 提现成功后,提现记录数增<EFBFBD>?
if (!result.Success) return false;
var afterCount = dbContext.Withdrawals.Count(w => w.UserId == userId);
@ -929,10 +932,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 7: 提现失败时不创建提现记录且余额不
/// Property 7: 提现失败时不创建提现记录且余额不<EFBFBD>?
/// *For any* failed withdrawal, no withdrawal record SHALL be created and balance SHALL remain unchanged.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 100)]
@ -944,7 +947,7 @@ public class InviteServicePropertyTests
// 设置余额
var balance = 50m + (seed.Get % 50);
// 提现金额超过余额(会失败
// 提现金额超过余额(会失败<EFBFBD>?
var withdrawAmount = balance + 100;
// 创建用户
@ -953,10 +956,10 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
// 记录提现前的提现记录
// 记录提现前的提现记录<EFBFBD>?
var beforeCount = dbContext.Withdrawals.Count(w => w.UserId == userId);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act
var result = service.ApplyWithdrawAsync(userId, withdrawAmount).GetAwaiter().GetResult();
@ -968,7 +971,7 @@ public class InviteServicePropertyTests
var afterCount = dbContext.Withdrawals.Count(w => w.UserId == userId);
if (afterCount != beforeCount) return false;
// 验证用户余额未变
// 验证用户余额未变<EFBFBD>?
var updatedUser = dbContext.Users.Find((int)userId);
if (updatedUser == null) return false;
@ -976,10 +979,10 @@ public class InviteServicePropertyTests
}
/// <summary>
/// Property 7: 多次提现后余额累计正
/// Property 7: 多次提现后余额累计正<EFBFBD>?
/// *For any* multiple successful withdrawals, final balance SHALL equal original balance minus sum of all withdrawals.
///
/// **Feature: miniapp-api, Property 7: 提现余额一致性**
/// **Feature: miniapp-api, Property 7: 提现余额一致<EFBFBD>?*
/// **Validates: Requirements 13.1**
/// </summary>
[Property(MaxTest = 50)]
@ -989,7 +992,7 @@ public class InviteServicePropertyTests
using var dbContext = CreateDbContext();
var userId = (long)seed.Get;
// 设置较大的初始余
// 设置较大的初始余<EFBFBD>?
var originalBalance = 500m + (seed.Get % 500);
// 创建用户
@ -998,11 +1001,11 @@ public class InviteServicePropertyTests
dbContext.Users.Add(user);
dbContext.SaveChanges();
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object);
var service = new InviteService(dbContext, _mockLogger.Object, _mockWechatService.Object, _httpClient, _mockRedisService.Object);
// Act: 进行多次提现
var withdrawAmounts = new List<decimal>();
var withdrawCount = Math.Max(1, seed.Get % 3 + 1); // 1-3次提
var withdrawCount = Math.Max(1, seed.Get % 3 + 1); // 1-3次提<EFBFBD>?
for (int i = 0; i < withdrawCount; i++)
{
@ -1015,7 +1018,7 @@ public class InviteServicePropertyTests
}
}
// Assert: 最终余额 = 原余额 - 所有成功提现金额之和
// Assert: 最终余<EFBFBD>?= 原余<E58E9F>?- 所有成功提现金额之<E9A29D>?
var totalWithdrawn = withdrawAmounts.Sum();
var expectedBalance = originalBalance - totalWithdrawn;

View File

@ -4,8 +4,8 @@
import { setupRouteGuard } from './utils/routeGuard.js'
export default {
onLaunch: function() {
console.log('App Launch')
onLaunch: function(options) {
console.log('App Launch', options)
//
setupRouteGuard()
@ -17,6 +17,9 @@
const userStore = useUserStore()
userStore.restoreFromStorage()
// scene query
this.parseInviterFromLaunch(options)
//
if (userStore.isLoggedIn) {
userStore.fetchUserInfo()
@ -27,8 +30,40 @@
},
onHide: function() {
console.log('App Hide')
},
methods: {
/**
* 解析邀请人参数
* 来源1: 扫描小程序码 scene 参数格式 inviter={userId}
* 来源2: 分享链接 query 参数 inviteUid={uid}
*/
parseInviterFromLaunch(options) {
if (!options) return
let inviterId = null
// 1. scene inviter={userId}
if (options.scene) {
const scene = decodeURIComponent(options.scene)
console.log('扫码 scene:', scene)
const match = scene.match(/inviter=(\d+)/)
if (match) {
inviterId = match[1]
}
}
// 2. inviterId
if (!inviterId && options.inviterId) {
inviterId = options.inviterId
}
// ID
if (inviterId) {
console.log('检测到邀请人ID:', inviterId)
uni.setStorageSync('inviterId', inviterId)
}
}
}
}
</script>
<style>

View File

@ -36,9 +36,18 @@ export function getContactInfo() {
return get('/system/getContactInfo')
}
/**
* 获取小程序信息名称和简介
* @returns {Promise<Object>}
*/
export function getMiniappInfo() {
return get('/system/getMiniappInfo')
}
export default {
getAgreement,
getPrivacy,
getAbout,
getContactInfo
getContactInfo,
getMiniappInfo
}

File diff suppressed because it is too large Load Diff

View File

@ -121,14 +121,25 @@ async function handleGetPhoneNumber(e) {
uni.showLoading({ title: '登录中...' })
// 2. code code
const res = await post('/login', {
const loginData = {
code: loginRes.code,
phoneCode: e.detail.code // code
}, { needAuth: false })
}
// ID
const inviterId = uni.getStorageSync('inviterId')
if (inviterId) {
loginData.pid = String(inviterId)
}
const res = await post('/login', loginData, { needAuth: false })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// ID
uni.removeStorageSync('inviterId')
// LoginResponse token userId
userStore.login({
token: res.data.token,
@ -204,13 +215,22 @@ async function handleLogin() {
uni.showLoading({ title: '登录中...' })
// 2.
const res = await post('/login', {
code: loginRes.code
}, { needAuth: false })
const loginData = { code: loginRes.code }
// ID
const inviterId = uni.getStorageSync('inviterId')
if (inviterId) {
loginData.pid = String(inviterId)
}
const res = await post('/login', loginData, { needAuth: false })
uni.hideLoading()
if (res && res.code === 0 && res.data) {
// ID
uni.removeStorageSync('inviterId')
// LoginResponse token userId
userStore.login({
token: res.data.token,

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB