diff --git a/.kiro/hooks/figma-code-connect.kiro.hook b/.kiro/hooks/figma-code-connect.kiro.hook deleted file mode 100644 index f9ea67c..0000000 --- a/.kiro/hooks/figma-code-connect.kiro.hook +++ /dev/null @@ -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" -} diff --git a/server/MiAssessment/src/MiAssessment.Api/Controllers/SystemController.cs b/server/MiAssessment/src/MiAssessment.Api/Controllers/SystemController.cs index d3a36a7..69fc4d6 100644 --- a/server/MiAssessment/src/MiAssessment.Api/Controllers/SystemController.cs +++ b/server/MiAssessment/src/MiAssessment.Api/Controllers/SystemController.cs @@ -137,4 +137,30 @@ public class SystemController : ControllerBase return ApiResponse.Fail("获取联系我们信息失败"); } } + + /// + /// 获取小程序信息 + /// + /// + /// GET /api/system/getMiniappInfo + /// + /// 从配置表读取小程序名称和简介 + /// 不需要用户登录认证 + /// + /// 小程序名称和简介 + [HttpGet("getMiniappInfo")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> GetMiniappInfo() + { + try + { + var info = await _systemService.GetMiniappInfoAsync(); + return ApiResponse.Success(info); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get miniapp info"); + return ApiResponse.Fail("获取小程序信息失败"); + } + } } diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/ISystemService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/ISystemService.cs index 8641415..876eb0f 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Interfaces/ISystemService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/ISystemService.cs @@ -42,4 +42,13 @@ public interface ISystemService /// /// 联系我们数据 Task GetContactInfoAsync(); + + /// + /// 获取小程序信息 + /// + /// + /// 从配置表读取小程序名称(config_key: miniapp_name)和简介(config_key: miniapp_intro) + /// + /// 小程序信息数据 + Task GetMiniappInfoAsync(); } diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs index e8af761..f080b1e 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs @@ -13,6 +13,15 @@ public interface IUploadConfigService /// 文件MIME类型 /// 预签名URL信息,null表示不支持COS直传 Task GetPresignedUploadUrlAsync(string fileName, string contentType); + + /// + /// 后端直传文件到COS + /// + /// 文件字节数据 + /// 文件名(含扩展名) + /// MIME类型 + /// 文件访问URL,失败返回null + Task UploadFileAsync(byte[] fileBytes, string fileName, string contentType); } /// diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/InviteService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/InviteService.cs index b1e3ec3..e3f3e8b 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/InviteService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/InviteService.cs @@ -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 _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"; /// /// 构造函数 @@ -31,14 +38,20 @@ public class InviteService : IInviteService /// 数据库上下文 /// 日志记录器 /// 微信服务 + /// HTTP客户端 + /// 上传配置服务 public InviteService( MiAssessmentDbContext dbContext, ILogger logger, - IWechatService wechatService) + IWechatService wechatService, + HttpClient httpClient, + IUploadConfigService uploadConfigService) { _dbContext = dbContext; _logger = logger; _wechatService = wechatService; + _httpClient = httpClient; + _uploadConfigService = uploadConfigService; } /// @@ -104,13 +117,11 @@ public class InviteService : IInviteService /// public async Task 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 }; } /// diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/SystemService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/SystemService.cs index 81576fb..53b41e9 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/SystemService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/SystemService.cs @@ -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 }; } + + /// + public async Task 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 + }; + } } diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs index 34d18d0..64fca44 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs @@ -18,6 +18,7 @@ public class UploadConfigService : IUploadConfigService private readonly AdminConfigReadDbContext _adminConfigDbContext; private readonly IRedisService _redisService; private readonly ILogger _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 logger) + ILogger logger, + HttpClient httpClient) { _adminConfigDbContext = adminConfigDbContext; _redisService = redisService; _logger = logger; + _httpClient = httpClient; } /// @@ -74,6 +77,64 @@ public class UploadConfigService : IUploadConfigService }; } + /// + public async Task 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; + } + } + /// /// 生成腾讯云COS预签名URL(PUT方式) /// diff --git a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs index 29d24f7..9180d4e 100644 --- a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs +++ b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs @@ -227,7 +227,9 @@ public class ServiceModule : Module var dbContext = c.Resolve(); var logger = c.Resolve>(); var wechatService = c.Resolve(); - return new InviteService(dbContext, logger, wechatService); + var httpClientFactory = c.Resolve(); + var uploadConfigService = c.Resolve(); + return new InviteService(dbContext, logger, wechatService, httpClientFactory.CreateClient(), uploadConfigService); }).As().InstancePerLifetimeScope(); // ========== 小程序系统模块服务注册 ========== @@ -248,7 +250,8 @@ public class ServiceModule : Module var adminConfigDbContext = c.Resolve(); var redisService = c.Resolve(); var logger = c.Resolve>(); - return new UploadConfigService(adminConfigDbContext, redisService, logger); + var httpClientFactory = c.Resolve(); + return new UploadConfigService(adminConfigDbContext, redisService, logger, httpClientFactory.CreateClient()); }).As().InstancePerLifetimeScope(); // ========== 小程序团队模块服务注册 ========== diff --git a/server/MiAssessment/src/MiAssessment.Model/Entities/User.cs b/server/MiAssessment/src/MiAssessment.Model/Entities/User.cs index 29845bb..38ca8a8 100644 --- a/server/MiAssessment/src/MiAssessment.Model/Entities/User.cs +++ b/server/MiAssessment/src/MiAssessment.Model/Entities/User.cs @@ -125,6 +125,12 @@ public partial class User /// public DateTime UpdateTime { get; set; } + /// + /// 邀请二维码URL(COS存储地址) + /// + [MaxLength(500)] + public string? InviteQrcodeUrl { get; set; } + /// /// 软删除标记 /// diff --git a/server/MiAssessment/src/MiAssessment.Model/Models/System/MiniappInfoDto.cs b/server/MiAssessment/src/MiAssessment.Model/Models/System/MiniappInfoDto.cs new file mode 100644 index 0000000..0a8e57c --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Model/Models/System/MiniappInfoDto.cs @@ -0,0 +1,17 @@ +namespace MiAssessment.Model.Models.System; + +/// +/// 小程序信息数据传输对象 +/// +public class MiniappInfoDto +{ + /// + /// 小程序名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 小程序简介 + /// + public string Intro { get; set; } = string.Empty; +} diff --git a/server/MiAssessment/tests/MiAssessment.Tests/Services/InviteServicePropertyTests.cs b/server/MiAssessment/tests/MiAssessment.Tests/Services/InviteServicePropertyTests.cs index 4706f60..5954fe7 100644 --- a/server/MiAssessment/tests/MiAssessment.Tests/Services/InviteServicePropertyTests.cs +++ b/server/MiAssessment/tests/MiAssessment.Tests/Services/InviteServicePropertyTests.cs @@ -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; /// -/// InviteService 属性测试 -/// 验证分销服务的分页查询一致性 +/// InviteService 属性测�? +/// 验证分销服务的分页查询一致�? /// public class InviteServicePropertyTests { private readonly Mock> _mockLogger = new(); private readonly Mock _mockWechatService = new(); + private readonly HttpClient _httpClient = new(); + private readonly Mock _mockRedisService = new(); - #region Property 3: 分页查询一致性 - GetRecordListAsync + #region Property 3: 分页查询一致�?- GetRecordListAsync /// /// 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Property(MaxTest = 100)] @@ -41,7 +44,7 @@ public class InviteServicePropertyTests var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建多个下级用户(超过pageSize) + // 创建多个下级用户(超过pageSize�? 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Property(MaxTest = 100)] @@ -80,11 +83,11 @@ public class InviteServicePropertyTests var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建另一个用户 + // 创建另一个用�? var otherUser = CreateUser(otherUserId, seed.Get + 50000); dbContext.Users.Add(otherUser); - // 为当前用户创建下级用户 + // 为当前用户创建下级用�? 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�? 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 } /// - /// Property 3: 遍历所有页面能获取所有满足条件的邀请记录 + /// Property 3: 遍历所有页面能获取所有满足条件的邀请记�? /// *For any* paginated query, traversing all pages SHALL return all matching records. /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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以测试多�? // 创建当前用户 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: 遍历所有页�? var allRetrievedIds = new HashSet(); 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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 } /// - /// Property 3: 邀请记录分页查询不同页面返回不重复的记录 + /// Property 3: 邀请记录分页查询不同页面返回不重复的记�? /// *For any* paginated query, different pages SHALL return non-overlapping records. /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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以确保多�? // 创建当前用户 var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建足够多的下级用户以产生多页 + // 创建足够多的下级用户以产生多�? 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: 获取前两�? var page1 = service.GetRecordListAsync(userId, 1, pageSize).GetAwaiter().GetResult(); var page2 = service.GetRecordListAsync(userId, 2, pageSize).GetAwaiter().GetResult(); - // Assert: 两页的记录ID不重叠 + // Assert: 两页的记录ID不重�? 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: 分页查询一致�?- GetWithdrawListAsync /// /// 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Property(MaxTest = 100)] @@ -275,7 +278,7 @@ public class InviteServicePropertyTests var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建多条提现记录(超过pageSize) + // 创建多条提现记录(超过pageSize�? 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Property(MaxTest = 100)] @@ -312,24 +315,24 @@ public class InviteServicePropertyTests var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建另一个用户 + // 创建另一个用�? var otherUser = CreateUser(otherUserId, seed.Get + 50000); dbContext.Users.Add(otherUser); - // 为当前用户创建提现记录 + // 为当前用户创建提现记�? 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�? for (int i = 0; i < 5; i++) { dbContext.Withdrawals.Add(CreateWithdrawal(seed.Get + 1000 + i, otherUserId)); } - // 创建已删除的提现记录(不应计入Total) + // 创建已删除的提现记录(不应计入Total�? 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等于当前用户未删除的提现记录�? 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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以测试多�? // 创建当前用户 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: 遍历所有页�? var allRetrievedIds = new HashSet(); 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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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以确保多�? // 创建当前用户 var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建足够多的提现记录以产生多页 + // 创建足够多的提现记录以产生多�? 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: 获取前两�? var page1 = service.GetWithdrawListAsync(userId, 1, pageSize).GetAwaiter().GetResult(); var page2 = service.GetWithdrawListAsync(userId, 2, pageSize).GetAwaiter().GetResult(); - // Assert: 两页的记录ID不重叠 + // Assert: 两页的记录ID不重�? 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 } /// - /// Property 3: 提现记录分页查询不返回已删除的记录 + /// Property 3: 提现记录分页查询不返回已删除的记�? /// *For any* paginated query, deleted records (IsDeleted=true) SHALL not be returned. /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Property(MaxTest = 100)] @@ -501,7 +504,7 @@ public class InviteServicePropertyTests var currentUser = CreateUser(userId, seed.Get); dbContext.Users.Add(currentUser); - // 创建正常的提现记录 + // 创建正常的提现记�? var normalWithdrawals = new List(); 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. 返回的记录数等于正常记录�? if (result.List.Count != 3) return false; - // 2. 返回的记录中不包含已删除的记录 + // 2. 返回的记录中不包含已删除的记�? 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 综合属性测�? /// - /// Property 3: 空数据库返回空列表 - 邀请记录 + /// Property 3: 空数据库返回空列�?- 邀请记�? /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Fact] @@ -554,12 +557,12 @@ public class InviteServicePropertyTests // Arrange using var dbContext = CreateDbContext(); - // 创建用户但没有下级 + // 创建用户但没有下�? 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 } /// - /// Property 3: 空数据库返回空列表 - 提现记录 + /// Property 3: 空数据库返回空列�?- 提现记录 /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [Fact] @@ -581,12 +584,12 @@ public class InviteServicePropertyTests // Arrange using var dbContext = CreateDbContext(); - // 创建用户但没有提现记录 + // 创建用户但没有提现记�? 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 } /// - /// Property 3: 分页参数边界值处理 - 邀请记录 + /// Property 3: 分页参数边界值处�?- 邀请记�? /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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: 测试边界�?- page=0 应该被处理为 page=1 var result = service.GetRecordListAsync(userId, 0, 10).GetAwaiter().GetResult(); // Assert: 应该返回第一页的数据 @@ -633,9 +636,9 @@ public class InviteServicePropertyTests } /// - /// Property 3: 分页参数边界值处理 - 提现记录 + /// Property 3: 分页参数边界值处�?- 提现记录 /// - /// **Feature: miniapp-api, Property 3: 分页查询一致性** + /// **Feature: miniapp-api, Property 3: 分页查询一致�?* /// **Validates: Requirements 12.1, 13.5** /// [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: 测试边界�?- 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: 提现余额一致�? /// /// Property 7: 提现成功后用户余额等于原余额减去提现金额 /// *For any* successful withdrawal, user.Balance = originalBalance - withdrawalAmount. /// - /// **Feature: miniapp-api, Property 7: 提现余额一致性** + /// **Feature: miniapp-api, Property 7: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [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: 提现成功后,用户余额 = 原余�?- 提现金额 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: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [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等于原余�? if (withdrawal.BeforeBalance != originalBalance) return false; - // 验证AfterBalance等于原余额减去提现金额 + // 验证AfterBalance等于原余额减去提现金�? var expectedAfterBalance = originalBalance - withdrawAmount; if (withdrawal.AfterBalance != expectedAfterBalance) return false; @@ -762,10 +765,10 @@ public class InviteServicePropertyTests } /// - /// Property 7: 提现金额超过余额时提现失败 + /// Property 7: 提现金额超过余额时提现失�? /// *For any* withdrawal where amount > balance, withdrawal SHALL fail. /// - /// **Feature: miniapp-api, Property 7: 提现余额一致性** + /// **Feature: miniapp-api, Property 7: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [Property(MaxTest = 100)] @@ -775,7 +778,7 @@ public class InviteServicePropertyTests using var dbContext = CreateDbContext(); var userId = (long)seed.Get; - // 设置余额为1-50之间的整数 + // 设置余额�?-50之间的整�? 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: 提现失败,且错误信息包含"超出待提现金�? if (result.Success) return false; if (string.IsNullOrEmpty(result.ErrorMessage)) return false; - // 验证用户余额未变化 + // 验证用户余额未变�? 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: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [Property(MaxTest = 100)] @@ -816,9 +819,9 @@ public class InviteServicePropertyTests using var dbContext = CreateDbContext(); var userId = (long)seed.Get; - // 设置足够的余额 + // 设置足够的余�? var balance = 100m + (seed.Get % 100); - // 提现金额小于1元(0.01 - 0.99) + // 提现金额小于1元(0.01 - 0.99�? 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 } /// - /// Property 7: 提现金额不是整数时提现失败 + /// Property 7: 提现金额不是整数时提现失�? /// *For any* withdrawal where amount is not an integer, withdrawal SHALL fail. /// - /// **Feature: miniapp-api, Property 7: 提现余额一致性** + /// **Feature: miniapp-api, Property 7: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [Property(MaxTest = 100)] @@ -858,9 +861,9 @@ public class InviteServicePropertyTests using var dbContext = CreateDbContext(); var userId = (long)seed.Get; - // 设置足够的余额 + // 设置足够的余�? var balance = 100m + (seed.Get % 100); - // 提现金额为非整数(1.01 - 99.99) + // 提现金额为非整数�?.01 - 99.99�? 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; - // 验证用户余额未变化 + // 验证用户余额未变�? var updatedUser = dbContext.Users.Find((int)userId); if (updatedUser == null) return false; @@ -889,10 +892,10 @@ public class InviteServicePropertyTests } /// - /// Property 7: 提现成功后创建提现记录 + /// Property 7: 提现成功后创建提现记�? /// *For any* successful withdrawal, a withdrawal record SHALL be created. /// - /// **Feature: miniapp-api, Property 7: 提现余额一致性** + /// **Feature: miniapp-api, Property 7: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [Property(MaxTest = 100)] @@ -913,15 +916,15 @@ public class InviteServicePropertyTests dbContext.Users.Add(user); dbContext.SaveChanges(); - // 记录提现前的提现记录数 + // 记录提现前的提现记录�? 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: 提现成功后,提现记录数增�? if (!result.Success) return false; var afterCount = dbContext.Withdrawals.Count(w => w.UserId == userId); @@ -929,10 +932,10 @@ public class InviteServicePropertyTests } /// - /// Property 7: 提现失败时不创建提现记录且余额不变 + /// Property 7: 提现失败时不创建提现记录且余额不�? /// *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: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [Property(MaxTest = 100)] @@ -944,7 +947,7 @@ public class InviteServicePropertyTests // 设置余额 var balance = 50m + (seed.Get % 50); - // 提现金额超过余额(会失败) + // 提现金额超过余额(会失败�? var withdrawAmount = balance + 100; // 创建用户 @@ -953,10 +956,10 @@ public class InviteServicePropertyTests dbContext.Users.Add(user); dbContext.SaveChanges(); - // 记录提现前的提现记录数 + // 记录提现前的提现记录�? 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; - // 验证用户余额未变化 + // 验证用户余额未变�? var updatedUser = dbContext.Users.Find((int)userId); if (updatedUser == null) return false; @@ -976,10 +979,10 @@ public class InviteServicePropertyTests } /// - /// Property 7: 多次提现后余额累计正确 + /// Property 7: 多次提现后余额累计正�? /// *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: 提现余额一致�?* /// **Validates: Requirements 13.1** /// [Property(MaxTest = 50)] @@ -989,7 +992,7 @@ public class InviteServicePropertyTests using var dbContext = CreateDbContext(); var userId = (long)seed.Get; - // 设置较大的初始余额 + // 设置较大的初始余�? 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(); - var withdrawCount = Math.Max(1, seed.Get % 3 + 1); // 1-3次提现 + var withdrawCount = Math.Max(1, seed.Get % 3 + 1); // 1-3次提�? for (int i = 0; i < withdrawCount; i++) { @@ -1015,7 +1018,7 @@ public class InviteServicePropertyTests } } - // Assert: 最终余额 = 原余额 - 所有成功提现金额之和 + // Assert: 最终余�?= 原余�?- 所有成功提现金额之�? var totalWithdrawn = withdrawAmounts.Sum(); var expectedBalance = originalBalance - totalWithdrawn; diff --git a/uniapp/App.vue b/uniapp/App.vue index 7b5debe..e9333c1 100644 --- a/uniapp/App.vue +++ b/uniapp/App.vue @@ -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) + } + } } - }