diff --git a/.drone.yml b/.drone.yml index bc4119c..7fb90a8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -26,6 +26,7 @@ steps: password: from_secret: harbor_password insecure: true + no_cache: true # ==================== 构建并推送 Admin API 镜像 ==================== - name: build-admin @@ -43,6 +44,7 @@ steps: password: from_secret: harbor_password insecure: true + no_cache: true # ==================== 部署到服务器 ==================== - name: deploy diff --git a/.kiro/specs/invite-page-enhancement/requirements.md b/.kiro/specs/invite-page-enhancement/requirements.md index 3f0ed32..036dd56 100644 --- a/.kiro/specs/invite-page-enhancement/requirements.md +++ b/.kiro/specs/invite-page-enhancement/requirements.md @@ -2,7 +2,7 @@ ## 简介 -本需求针对现有邀请新用户页面(`uniapp/pages/invite/index.vue`)进行 UI 增强和功能完善,使其对齐设计图。现有页面已具备基本功能(规则说明、二维码生成、分享链接、佣金展示、提现申请、提现记录、邀请记录),但 UI 布局和交互细节与设计稿存在差异。本次增强聚焦于视觉还原和交互优化,不涉及后端接口变更。 +本需求针对现有邀请新用户页面(`uniapp/pages/invite/index.vue`)进行 UI 增强和功能完善,使其对齐设计图。现有页面已具备基本功能(规则说明 、二维码生成、分享链接、佣金展示、提现申请、提现记录、邀请记录),但 UI 布局和交互细节与设计稿存在差异。本次增强聚焦于视觉还原和交互优化,不涉及后端接口变更。 ## 术语表 diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/DistributionController.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/DistributionController.cs index 4c5b35a..cc68adf 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/DistributionController.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/DistributionController.cs @@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Models.Distribution; using MiAssessment.Admin.Business.Services.Interfaces; +using ClosedXML.Excel; using Microsoft.AspNetCore.Mvc; namespace MiAssessment.Admin.Business.Controllers; @@ -91,11 +92,42 @@ public class DistributionController : BusinessControllerBase { var items = await _distributionService.ExportInviteCodesAsync(request); - // 生成CSV内容 - var csvContent = GenerateInviteCodeCsv(items); - var fileName = $"invite_codes_{DateTime.Now:yyyyMMddHHmmss}.csv"; + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("邀请码列表"); - return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName); + var headers = new[] { "ID", "邀请码", "批次号", "分配用户", "分配时间", "使用用户", "使用订单ID", "使用时间", "状态", "创建时间" }; + for (var i = 0; i < headers.Length; i++) + worksheet.Cell(1, i + 1).Value = headers[i]; + + var headerRow = worksheet.Range(1, 1, 1, headers.Length); + headerRow.Style.Font.Bold = true; + headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2"); + headerRow.Style.Font.FontColor = XLColor.White; + + for (var i = 0; i < items.Count; i++) + { + var row = i + 2; + var item = items[i]; + worksheet.Cell(row, 1).Value = item.Id; + worksheet.Cell(row, 2).Value = item.Code; + worksheet.Cell(row, 3).Value = item.BatchNo ?? ""; + worksheet.Cell(row, 4).Value = item.AssignUserNickname ?? ""; + worksheet.Cell(row, 5).Value = item.AssignTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + worksheet.Cell(row, 6).Value = item.UseUserNickname ?? ""; + worksheet.Cell(row, 7).Value = item.UseOrderId?.ToString() ?? ""; + worksheet.Cell(row, 8).Value = item.UseTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + worksheet.Cell(row, 9).Value = item.StatusName; + worksheet.Cell(row, 10).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + + worksheet.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + + return File(stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"邀请码列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } #endregion @@ -162,11 +194,43 @@ public class DistributionController : BusinessControllerBase { var items = await _distributionService.ExportCommissionsAsync(request); - // 生成CSV内容 - var csvContent = GenerateCommissionCsv(items); - var fileName = $"commissions_{DateTime.Now:yyyyMMddHHmmss}.csv"; + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("佣金记录"); - return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName); + var headers = new[] { "ID", "获佣用户", "来源用户", "订单编号", "订单金额", "佣金比例", "佣金金额", "层级", "状态", "结算时间", "创建时间" }; + for (var i = 0; i < headers.Length; i++) + worksheet.Cell(1, i + 1).Value = headers[i]; + + var headerRow = worksheet.Range(1, 1, 1, headers.Length); + headerRow.Style.Font.Bold = true; + headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2"); + headerRow.Style.Font.FontColor = XLColor.White; + + for (var i = 0; i < items.Count; i++) + { + var row = i + 2; + var item = items[i]; + worksheet.Cell(row, 1).Value = item.Id; + worksheet.Cell(row, 2).Value = item.UserNickname ?? ""; + worksheet.Cell(row, 3).Value = item.FromUserNickname ?? ""; + worksheet.Cell(row, 4).Value = item.OrderNo ?? ""; + worksheet.Cell(row, 5).Value = item.OrderAmount; + worksheet.Cell(row, 6).Value = item.CommissionRate; + worksheet.Cell(row, 7).Value = item.CommissionAmount; + worksheet.Cell(row, 8).Value = item.LevelName; + worksheet.Cell(row, 9).Value = item.StatusName; + worksheet.Cell(row, 10).Value = item.SettleTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + worksheet.Cell(row, 11).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + + worksheet.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + + return File(stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"佣金记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } #endregion @@ -292,78 +356,44 @@ public class DistributionController : BusinessControllerBase { var items = await _distributionService.ExportWithdrawalsAsync(request); - // 生成CSV内容 - var csvContent = GenerateWithdrawalCsv(items); - var fileName = $"withdrawals_{DateTime.Now:yyyyMMddHHmmss}.csv"; + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("提现记录"); - return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName); - } + var headers = new[] { "提现单号", "用户昵称", "手机号", "提现金额", "提现前余额", "提现后余额", "状态", "审核时间", "审核备注", "打款时间", "打款交易号", "创建时间" }; + for (var i = 0; i < headers.Length; i++) + worksheet.Cell(1, i + 1).Value = headers[i]; - #endregion + var headerRow = worksheet.Range(1, 1, 1, headers.Length); + headerRow.Style.Font.Bold = true; + headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2"); + headerRow.Style.Font.FontColor = XLColor.White; - #region 私有方法 - - /// - /// 生成邀请码CSV内容 - /// - /// 邀请码列表 - /// CSV内容 - private static string GenerateInviteCodeCsv(List items) - { - var sb = new System.Text.StringBuilder(); - - // CSV 头部 - sb.AppendLine("ID,邀请码,批次号,分配用户,分配时间,使用用户,使用订单ID,使用时间,状态,创建时间"); - - // CSV 数据行 - foreach (var item in items) + for (var i = 0; i < items.Count; i++) { - sb.AppendLine($"{item.Id},{item.Code},{item.BatchNo ?? ""},{item.AssignUserNickname ?? ""},{item.AssignTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.UseUserNickname ?? ""},{item.UseOrderId?.ToString() ?? ""},{item.UseTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.StatusName},{item.CreateTime:yyyy-MM-dd HH:mm:ss}"); + var row = i + 2; + var item = items[i]; + worksheet.Cell(row, 1).Value = item.WithdrawalNo; + worksheet.Cell(row, 2).Value = item.UserNickname ?? ""; + worksheet.Cell(row, 3).Value = item.UserPhone ?? ""; + worksheet.Cell(row, 4).Value = item.Amount; + worksheet.Cell(row, 5).Value = item.BeforeBalance; + worksheet.Cell(row, 6).Value = item.AfterBalance; + worksheet.Cell(row, 7).Value = item.StatusName; + worksheet.Cell(row, 8).Value = item.AuditTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + worksheet.Cell(row, 9).Value = item.AuditRemark ?? ""; + worksheet.Cell(row, 10).Value = item.PayTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + worksheet.Cell(row, 11).Value = item.PayTransactionId ?? ""; + worksheet.Cell(row, 12).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"); } - return sb.ToString(); - } + worksheet.Columns().AdjustToContents(); - /// - /// 生成佣金记录CSV内容 - /// - /// 佣金记录列表 - /// CSV内容 - private static string GenerateCommissionCsv(List items) - { - var sb = new System.Text.StringBuilder(); + using var stream = new MemoryStream(); + workbook.SaveAs(stream); - // CSV 头部 - sb.AppendLine("ID,获得佣金用户ID,获得佣金用户,来源用户ID,来源用户,订单ID,订单编号,订单金额,佣金比例,佣金金额,层级,状态,结算时间,创建时间"); - - // CSV 数据行 - foreach (var item in items) - { - sb.AppendLine($"{item.Id},{item.UserId},{item.UserNickname ?? ""},{item.FromUserId},{item.FromUserNickname ?? ""},{item.OrderId},{item.OrderNo ?? ""},{item.OrderAmount},{item.CommissionRate},{item.CommissionAmount},{item.LevelName},{item.StatusName},{item.SettleTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.CreateTime:yyyy-MM-dd HH:mm:ss}"); - } - - return sb.ToString(); - } - - /// - /// 生成提现记录CSV内容 - /// - /// 提现记录列表 - /// CSV内容 - private static string GenerateWithdrawalCsv(List items) - { - var sb = new System.Text.StringBuilder(); - - // CSV 头部 - sb.AppendLine("ID,提现单号,用户ID,用户昵称,用户手机号,提现金额,提现前余额,提现后余额,状态,审核人ID,审核时间,审核备注,打款时间,打款交易号,创建时间"); - - // CSV 数据行 - foreach (var item in items) - { - sb.AppendLine($"{item.Id},{item.WithdrawalNo},{item.UserId},{item.UserNickname ?? ""},{item.UserPhone ?? ""},{item.Amount},{item.BeforeBalance},{item.AfterBalance},{item.StatusName},{item.AuditUserId?.ToString() ?? ""},{item.AuditTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.AuditRemark ?? ""},{item.PayTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""},{item.PayTransactionId ?? ""},{item.CreateTime:yyyy-MM-dd HH:mm:ss}"); - } - - return sb.ToString(); + return File(stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"提现记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } #endregion diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/OrderController.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/OrderController.cs index e8c0b5d..b06866e 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/OrderController.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/OrderController.cs @@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Models.Order; using MiAssessment.Admin.Business.Services.Interfaces; +using ClosedXML.Excel; using Microsoft.AspNetCore.Mvc; namespace MiAssessment.Admin.Business.Controllers; @@ -105,7 +106,7 @@ public class OrderController : BusinessControllerBase /// 导出订单列表 /// /// 查询请求 - /// 导出数据 + /// Excel文件 [HttpGet("export")] [BusinessPermission("order:view")] public async Task Export([FromQuery] OrderQueryRequest request) @@ -113,7 +114,45 @@ public class OrderController : BusinessControllerBase try { var result = await _orderService.ExportOrdersAsync(request); - return Ok(result); + + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("订单列表"); + + var headers = new[] { "订单编号", "用户UID", "用户昵称", "手机号", "订单类型", "商品名称", "订单金额", "实付金额", "支付方式", "状态", "支付时间", "创建时间" }; + for (var i = 0; i < headers.Length; i++) + worksheet.Cell(1, i + 1).Value = headers[i]; + + var headerRow = worksheet.Range(1, 1, 1, headers.Length); + headerRow.Style.Font.Bold = true; + headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2"); + headerRow.Style.Font.FontColor = XLColor.White; + + for (var i = 0; i < result.Count; i++) + { + var row = i + 2; + var o = result[i]; + worksheet.Cell(row, 1).Value = o.OrderNo; + worksheet.Cell(row, 2).Value = o.UserUid; + worksheet.Cell(row, 3).Value = o.UserNickname; + worksheet.Cell(row, 4).Value = o.UserPhone; + worksheet.Cell(row, 5).Value = o.OrderTypeName; + worksheet.Cell(row, 6).Value = o.ProductName; + worksheet.Cell(row, 7).Value = o.Amount; + worksheet.Cell(row, 8).Value = o.PayAmount; + worksheet.Cell(row, 9).Value = o.PayTypeName; + worksheet.Cell(row, 10).Value = o.StatusName; + worksheet.Cell(row, 11).Value = o.PayTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + worksheet.Cell(row, 12).Value = o.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + + worksheet.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + + return File(stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"订单列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } catch (BusinessException ex) { diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/PlannerController.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/PlannerController.cs index 8035915..4198339 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/PlannerController.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/PlannerController.cs @@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Models.Planner; using MiAssessment.Admin.Business.Services.Interfaces; +using ClosedXML.Excel; using Microsoft.AspNetCore.Mvc; namespace MiAssessment.Admin.Business.Controllers; @@ -259,7 +260,7 @@ public class PlannerController : BusinessControllerBase /// 导出预约记录列表 /// /// 查询请求 - /// 导出数据 + /// Excel文件 [HttpGet("booking/export")] [BusinessPermission("planner:view")] public async Task ExportBookings([FromQuery] BookingQueryRequest request) @@ -267,7 +268,46 @@ public class PlannerController : BusinessControllerBase try { var result = await _plannerService.ExportBookingsAsync(request); - return Ok(result); + + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("预约记录"); + + var headers = new[] { "用户UID", "用户昵称", "手机号", "订单编号", "规划师", "预约日期", "预约时间", "学生姓名", "联系电话", "性别", "年级", "状态", "创建时间" }; + for (var i = 0; i < headers.Length; i++) + worksheet.Cell(1, i + 1).Value = headers[i]; + + var headerRow = worksheet.Range(1, 1, 1, headers.Length); + headerRow.Style.Font.Bold = true; + headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2"); + headerRow.Style.Font.FontColor = XLColor.White; + + for (var i = 0; i < result.Count; i++) + { + var row = i + 2; + var item = result[i]; + worksheet.Cell(row, 1).Value = item.UserUid ?? ""; + worksheet.Cell(row, 2).Value = item.UserNickname ?? ""; + worksheet.Cell(row, 3).Value = item.UserPhone ?? ""; + worksheet.Cell(row, 4).Value = item.OrderNo ?? ""; + worksheet.Cell(row, 5).Value = item.PlannerName ?? ""; + worksheet.Cell(row, 6).Value = item.BookingDate.ToString("yyyy-MM-dd"); + worksheet.Cell(row, 7).Value = item.BookingTime; + worksheet.Cell(row, 8).Value = item.Name; + worksheet.Cell(row, 9).Value = item.Phone; + worksheet.Cell(row, 10).Value = item.GenderName; + worksheet.Cell(row, 11).Value = item.GradeName; + worksheet.Cell(row, 12).Value = item.StatusName; + worksheet.Cell(row, 13).Value = item.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } + + worksheet.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + + return File(stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"预约记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } catch (BusinessException ex) { diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs index 8a6aa90..b09c8c7 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Controllers/UserController.cs @@ -3,6 +3,7 @@ using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Models.User; using MiAssessment.Admin.Business.Services.Interfaces; +using ClosedXML.Excel; using Microsoft.AspNetCore.Mvc; namespace MiAssessment.Admin.Business.Controllers; @@ -191,7 +192,7 @@ public class UserController : BusinessControllerBase /// 导出用户列表 /// /// 查询请求 - /// 导出数据 + /// Excel文件 [HttpGet("export")] [BusinessPermission("user:view")] public async Task Export([FromQuery] UserQueryRequest request) @@ -199,7 +200,51 @@ public class UserController : BusinessControllerBase try { var result = await _userService.ExportUsersAsync(request); - return Ok(result); + + // 生成 Excel 文件 + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("用户列表"); + + // 表头 + var headers = new[] { "UID", "手机号", "昵称", "用户等级", "余额", "累计收入", "状态", "创建时间", "最后登录" }; + for (var i = 0; i < headers.Length; i++) + { + worksheet.Cell(1, i + 1).Value = headers[i]; + } + + // 表头样式 + var headerRow = worksheet.Range(1, 1, 1, headers.Length); + headerRow.Style.Font.Bold = true; + headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#4A90E2"); + headerRow.Style.Font.FontColor = XLColor.White; + + // 数据行 + for (var i = 0; i < result.Count; i++) + { + var row = i + 2; + var user = result[i]; + worksheet.Cell(row, 1).Value = user.Uid; + worksheet.Cell(row, 2).Value = user.Phone; + worksheet.Cell(row, 3).Value = user.Nickname; + worksheet.Cell(row, 4).Value = user.UserLevelName; + worksheet.Cell(row, 5).Value = user.Balance; + worksheet.Cell(row, 6).Value = user.TotalIncome; + worksheet.Cell(row, 7).Value = user.StatusName; + worksheet.Cell(row, 8).Value = user.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"); + worksheet.Cell(row, 9).Value = user.LastLoginTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? ""; + } + + // 自动列宽 + worksheet.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + stream.Position = 0; + + var fileName = $"用户列表_{DateTime.Now:yyyyMMddHHmmss}.xlsx"; + return File(stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + fileName); } catch (BusinessException ex) { diff --git a/server/MiAssessment/src/MiAssessment.Admin/Dockerfile b/server/MiAssessment/src/MiAssessment.Admin/Dockerfile index 2957964..018f3c9 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/Dockerfile +++ b/server/MiAssessment/src/MiAssessment.Admin/Dockerfile @@ -2,7 +2,8 @@ FROM 192.168.195.25:19900/library/node:20-alpine AS frontend WORKDIR /app COPY src/MiAssessment.Admin/admin-web/package*.json ./ -RUN rm -f package-lock.json && npm install +RUN npm config set registry https://registry.npmmirror.com && \ + rm -f package-lock.json && npm install --ignore-scripts && npm rebuild esbuild COPY src/MiAssessment.Admin/admin-web/ . RUN npx vite build --outDir dist diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts index c4e1552..70fba6b 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/utils/request.ts @@ -191,6 +191,11 @@ service.interceptors.request.use( // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { + // blob 类型直接返回,不走业务 code 判断 + if (response.config.responseType === 'blob') { + return { code: 0, message: 'success', data: response.data } as any + } + const res = response.data // code 为 0 表示成功 diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/dashboard/index.vue b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/dashboard/index.vue index afb0361..06c1a09 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/dashboard/index.vue +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/dashboard/index.vue @@ -185,7 +185,7 @@
- + 用户管理 - + 订单管理 - + 规划师管理 - + 提现审核 - + 轮播图管理 - + 测评管理
diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/AuthService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/AuthService.cs index 4123f7e..ddfaa14 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/AuthService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/AuthService.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; namespace MiAssessment.Core.Services; /// -/// ��֤����ʵ�� +/// 认证服务实现 /// public class AuthService : IAuthService { @@ -30,7 +30,7 @@ public class AuthService : IAuthService private const string SmsCodeKeyPrefix = "sms:code:"; private const int DebounceSeconds = 3; - // Refresh Token ���� + // Refresh Token 长度 private const int RefreshTokenLength = 64; public AuthService( @@ -57,44 +57,44 @@ public class AuthService : IAuthService /// - /// ΢��С�����¼ + /// 微信小程序登录 /// Requirements: 1.1-1.8 /// public async Task WechatMiniProgramLoginAsync(string code, int? pid, string? clickId, string? phoneCode = null) { - _logger.LogInformation("[AuthService] ΢�ŵ�¼��ʼ��code={Code}, pid={Pid}, hasPhoneCode={HasPhoneCode}", code, pid, !string.IsNullOrWhiteSpace(phoneCode)); + _logger.LogInformation("[AuthService] 微信登录开始,code={Code}, pid={Pid}, hasPhoneCode={HasPhoneCode}", code, pid, !string.IsNullOrWhiteSpace(phoneCode)); if (string.IsNullOrWhiteSpace(code)) { - _logger.LogWarning("[AuthService] ΢�ŵ�¼ʧ�ܣ�codeΪ��"); + _logger.LogWarning("[AuthService] 微信登录失败,code为空"); return new LoginResult { Success = false, - ErrorMessage = "��Ȩcode����Ϊ��" + ErrorMessage = "授权code不能为空" }; } try { - // 1.6 �������� - 3���ڲ������ظ���¼ + // 1.6 防抖处理 - 3秒内不允许重复登录 var debounceKey = $"{LoginDebounceKeyPrefix}wechat:{code}"; - _logger.LogInformation("[AuthService] ��������: {Key}", debounceKey); + _logger.LogInformation("[AuthService] 防抖检查: {Key}", debounceKey); var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds)); if (!lockAcquired) { - _logger.LogWarning("[AuthService] �����������ܾ��ظ���¼����: {Code}", code); + _logger.LogWarning("[AuthService] 防抖处理:拒绝重复登录请求: {Code}", code); return new LoginResult { Success = false, - ErrorMessage = "����Ƶ����¼" + ErrorMessage = "请勿频繁登录" }; } - _logger.LogInformation("[AuthService] ��������ȡ�ɹ�"); + _logger.LogInformation("[AuthService] 防抖锁获取成功"); - // 1.1 ����΢��API��ȡopenid��unionid - _logger.LogInformation("[AuthService] ��ʼ����΢��API��ȡopenid..."); + // 1.1 调用微信API获取openid和unionid + _logger.LogInformation("[AuthService] 开始调用微信API获取openid..."); var wechatResult = await _wechatService.GetOpenIdAsync(code); - _logger.LogInformation("[AuthService] ΢��API������ɣ�Success={Success}, OpenId={OpenId}, UnionId={UnionId}, Error={Error}", + _logger.LogInformation("[AuthService] 微信API调用完成:Success={Success}, OpenId={OpenId}, UnionId={UnionId}, Error={Error}", wechatResult.Success, wechatResult.OpenId ?? "null", wechatResult.UnionId ?? "null", @@ -102,30 +102,30 @@ public class AuthService : IAuthService if (!wechatResult.Success) { - _logger.LogWarning("[AuthService] ΢��API����ʧ��: {Error}", wechatResult.ErrorMessage); + _logger.LogWarning("[AuthService] 微信API调用失败: {Error}", wechatResult.ErrorMessage); return new LoginResult { Success = false, - ErrorMessage = wechatResult.ErrorMessage ?? "��¼ʧ�ܣ����Ժ�����" + ErrorMessage = wechatResult.ErrorMessage ?? "登录失败,请稍后重试" }; } var openId = wechatResult.OpenId!; var unionId = wechatResult.UnionId; - // 1.2 �����û� - ����ͨ��unionid���ң����ͨ��openid���� + // 1.2 查找用户 - 优先通过unionid查找,再通过openid查找 User? user = null; if (!string.IsNullOrWhiteSpace(unionId)) { - _logger.LogInformation("[AuthService] ����ͨ��unionid�����û�: {UnionId}", unionId); + _logger.LogInformation("[AuthService] 尝试通过unionid查找用户: {UnionId}", unionId); user = await _userService.GetUserByUnionIdAsync(unionId); - _logger.LogInformation("[AuthService] unionid���ҽ��: {Found}", user != null ? $"�ҵ��û�ID={user.Id}" : "δ�ҵ�"); + _logger.LogInformation("[AuthService] unionid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到"); } if (user == null) { - _logger.LogInformation("[AuthService] ����ͨ��openid�����û�: {OpenId}", openId); + _logger.LogInformation("[AuthService] 尝试通过openid查找用户: {OpenId}", openId); user = await _userService.GetUserByOpenIdAsync(openId); - _logger.LogInformation("[AuthService] openid���ҽ��: {Found}", user != null ? $"�ҵ��û�ID={user.Id}" : "δ�ҵ�"); + _logger.LogInformation("[AuthService] openid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到"); } if (user == null) @@ -189,36 +189,42 @@ public class AuthService : IAuthService } } - // 1.5 ����˫ Token��Access Token + Refresh Token�� - _logger.LogInformation("[AuthService] ��ʼ����˫ Token: UserId={UserId}", user.Id); + // 1.5 生成双 Token(Access Token + Refresh Token) + _logger.LogInformation("[AuthService] 开始生成双 Token: UserId={UserId}", user.Id); var loginResponse = await GenerateLoginResponseAsync(user, null); - _logger.LogInformation("[AuthService] ˫ Token ���ɳɹ���AccessToken����={Length}", loginResponse.AccessToken?.Length ?? 0); - _logger.LogInformation("[AuthService] ΢�ŵ�¼�ɹ�: UserId={UserId}", user.Id); + // 更新最后登录时间 + user.LastLoginTime = DateTime.Now; + _dbContext.Users.Update(user); + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("[AuthService] 双 Token 生成成功,AccessToken长度={Length}", loginResponse.AccessToken?.Length ?? 0); + + _logger.LogInformation("[AuthService] 微信登录成功: UserId={UserId}", user.Id); return new LoginResult { Success = true, - Token = loginResponse.AccessToken, // ���ݾɰ� + Token = loginResponse.AccessToken, // 兼容旧版 UserId = user.Id, LoginResponse = loginResponse }; } catch (Exception ex) { - _logger.LogError(ex, "[AuthService] ΢�ŵ�¼�쳣: code={Code}, Message={Message}, StackTrace={StackTrace}", + _logger.LogError(ex, "[AuthService] 微信登录异常: code={Code}, Message={Message}, StackTrace={StackTrace}", code, ex.Message, ex.StackTrace); return new LoginResult { Success = false, - ErrorMessage = "������ϣ����Ժ�����" + ErrorMessage = "服务器异常,请稍后重试" }; } } /// - /// �ֻ�����֤���¼ + /// 手机号验证码登录 /// Requirements: 2.1-2.7 /// public async Task MobileLoginAsync(string mobile, string code, int? pid, string? clickId) @@ -228,7 +234,7 @@ public class AuthService : IAuthService return new LoginResult { Success = false, - ErrorMessage = "�ֻ��Ų���Ϊ��" + ErrorMessage = "手机号不能为空" }; } @@ -237,13 +243,13 @@ public class AuthService : IAuthService return new LoginResult { Success = false, - ErrorMessage = "��֤�벻��Ϊ��" + ErrorMessage = "验证码不能为空" }; } try { - // 2.6 �������� - 3���ڲ������ظ���¼ + // 2.6 防抖处理 - 3秒内不允许重复登录 var debounceKey = $"{LoginDebounceKeyPrefix}mobile:{mobile}"; var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds)); if (!lockAcquired) @@ -252,11 +258,11 @@ public class AuthService : IAuthService return new LoginResult { Success = false, - ErrorMessage = "����Ƶ����¼" + ErrorMessage = "请勿频繁登录" }; } - // 2.1 ��Redis��ȡ����֤��֤�� + // 2.1 从Redis获取短信验证码验证 var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}"; var storedCode = await _redisService.GetStringAsync(smsCodeKey); @@ -266,19 +272,19 @@ public class AuthService : IAuthService return new LoginResult { Success = false, - ErrorMessage = "��֤�����" + ErrorMessage = "验证码错误" }; } - // 2.2 ��֤����֤ͨ����ɾ��Redis�е���֤�� + // 2.2 验证码验证通过,删除Redis中的验证码 await _redisService.DeleteAsync(smsCodeKey); - // �����û� + // 查找用户 var user = await _userService.GetUserByMobileAsync(mobile); if (user == null) { - // 2.3 �û������ڣ��������û� + // 2.3 用户不存在,创建新用户 var createDto = new CreateUserDto { Mobile = mobile, @@ -291,15 +297,20 @@ public class AuthService : IAuthService _logger.LogInformation("New user created via mobile login: UserId={UserId}, Mobile={Mobile}", user.Id, MaskMobile(mobile)); } - // 2.4 ����˫ Token��Access Token + Refresh Token�� + // 2.4 生成双 Token(Access Token + Refresh Token) var loginResponse = await GenerateLoginResponseAsync(user, null); + // 更新最后登录时间 + user.LastLoginTime = DateTime.Now; + _dbContext.Users.Update(user); + await _dbContext.SaveChangesAsync(); + _logger.LogInformation("Mobile login successful: UserId={UserId}", user.Id); return new LoginResult { Success = true, - Token = loginResponse.AccessToken, // ���ݾɰ� + Token = loginResponse.AccessToken, // 兼容旧版 UserId = user.Id, LoginResponse = loginResponse }; @@ -310,58 +321,58 @@ public class AuthService : IAuthService return new LoginResult { Success = false, - ErrorMessage = "������ϣ����Ժ�����" + ErrorMessage = "服务器异常,请稍后重试" }; } } /// - /// ��֤����ֻ��� + /// 验证码绑定手机号 /// Requirements: 5.1-5.5 /// public async Task BindMobileAsync(long userId, string mobile, string code) { if (string.IsNullOrWhiteSpace(mobile)) { - throw new ArgumentException("�ֻ��Ų���Ϊ��", nameof(mobile)); + throw new ArgumentException("手机号不能为空", nameof(mobile)); } if (string.IsNullOrWhiteSpace(code)) { - throw new ArgumentException("��֤�벻��Ϊ��", nameof(code)); + throw new ArgumentException("验证码不能为空", nameof(code)); } - // 5.1 ��֤������֤�� + // 5.1 验证短信验证码 var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}"; var storedCode = await _redisService.GetStringAsync(smsCodeKey); if (string.IsNullOrWhiteSpace(storedCode) || storedCode != code) { _logger.LogWarning("SMS code verification failed for bind mobile: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile)); - throw new InvalidOperationException("��֤�����"); + throw new InvalidOperationException("验证码错误"); } - // ��֤����֤ͨ����ɾ�� + // 验证码验证通过,删除 await _redisService.DeleteAsync(smsCodeKey); - // ��ȡ��ǰ�û� + // 获取当前用户 var currentUser = await _userService.GetUserByIdAsync(userId); if (currentUser == null) { - throw new InvalidOperationException("�û�������"); + throw new InvalidOperationException("用户不存在"); } - // ����ֻ����Ƿ��ѱ������û��� + // 检查手机号是否已被其他用户绑定 var existingUser = await _userService.GetUserByMobileAsync(mobile); if (existingUser != null && existingUser.Id != userId) { - // 5.2 �ֻ����ѱ������û��󶨣���Ҫ�ϲ��˻� + // 5.2 手机号已被其他用户绑定,需要合并账户 return await MergeAccountsAsync(currentUser, existingUser); } - // 5.4 �ֻ���δ���󶨣�ֱ�Ӹ��µ�ǰ�û����ֻ��� + // 5.4 手机号未被绑定,直接更新当前用户的手机号 await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile }); _logger.LogInformation("Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile)); @@ -369,43 +380,43 @@ public class AuthService : IAuthService } /// - /// ΢����Ȩ���ֻ��� + /// 微信授权绑定手机号 /// Requirements: 5.1-5.5 /// public async Task WechatBindMobileAsync(long userId, string wechatCode) { if (string.IsNullOrWhiteSpace(wechatCode)) { - throw new ArgumentException("΢����Ȩcode����Ϊ��", nameof(wechatCode)); + throw new ArgumentException("微信授权code不能为空", nameof(wechatCode)); } - // ����΢��API��ȡ�ֻ��� + // 调用微信API获取手机号 var mobileResult = await _wechatService.GetMobileAsync(wechatCode); if (!mobileResult.Success || string.IsNullOrWhiteSpace(mobileResult.Mobile)) { _logger.LogWarning("WeChat get mobile failed: UserId={UserId}, Error={Error}", userId, mobileResult.ErrorMessage); - throw new InvalidOperationException(mobileResult.ErrorMessage ?? "��ȡ�ֻ���ʧ��"); + throw new InvalidOperationException(mobileResult.ErrorMessage ?? "获取手机号失败"); } var mobile = mobileResult.Mobile; - // ��ȡ��ǰ�û� + // 获取当前用户 var currentUser = await _userService.GetUserByIdAsync(userId); if (currentUser == null) { - throw new InvalidOperationException("�û�������"); + throw new InvalidOperationException("用户不存在"); } - // ����ֻ����Ƿ��ѱ������û��� + // 检查手机号是否已被其他用户绑定 var existingUser = await _userService.GetUserByMobileAsync(mobile); if (existingUser != null && existingUser.Id != userId) { - // 5.2 �ֻ����ѱ������û��󶨣���Ҫ�ϲ��˻� + // 5.2 手机号已被其他用户绑定,需要合并账户 return await MergeAccountsAsync(currentUser, existingUser); } - // 5.4 �ֻ���δ���󶨣�ֱ�Ӹ��µ�ǰ�û����ֻ��� + // 5.4 手机号未被绑定,直接更新当前用户的手机号 await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile }); _logger.LogInformation("Mobile bound via WeChat successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile)); @@ -414,7 +425,7 @@ public class AuthService : IAuthService /// - /// ��¼��¼��Ϣ + /// 记录登录信息 /// Requirements: 6.1, 6.3, 6.4 /// public async Task RecordLoginAsync(long userId, string? device, string? deviceInfo) @@ -422,17 +433,17 @@ public class AuthService : IAuthService var user = await _userService.GetUserByIdAsync(userId); if (user == null) { - throw new InvalidOperationException("�û�������"); + throw new InvalidOperationException("用户不存在"); } try { - // ��ȡ�ͻ���IP������ʹ�ÿ��ַ�����Ϊռλ����ʵ��IPӦ��Controller���룩 + // 获取客户端IP(这里使用空字符串作为占位,实际IP应由Controller传入) var clientIp = deviceInfo ?? string.Empty; var now = DateTime.Now; - // 6.1 ��¼��¼��־ + // 6.1 记录登录日志 var loginLog = new UserLoginLog { UserId = userId, @@ -446,7 +457,7 @@ public class AuthService : IAuthService await _dbContext.UserLoginLogs.AddAsync(loginLog); - // �����û�����¼ʱ�� + // 更新用户最后登录时间 user.LastLoginTime = now; user.LastLoginIp = clientIp; _dbContext.Users.Update(user); @@ -455,7 +466,7 @@ public class AuthService : IAuthService _logger.LogInformation("Login recorded: UserId={UserId}, Device={Device}, IP={IP}", userId, device, clientIp); - // 6.4 �����û���uid���dzƺ�ͷ�� + // 6.4 返回用户的uid、昵称和头像 return new RecordLoginResponse { Uid = user.Uid, @@ -471,33 +482,33 @@ public class AuthService : IAuthService } /// - /// H5���ֻ��ţ�������֤�룩 + /// H5绑定手机号(无需验证码) /// Requirements: 13.1 /// public async Task BindMobileH5Async(long userId, string mobile) { if (string.IsNullOrWhiteSpace(mobile)) { - throw new ArgumentException("�ֻ��Ų���Ϊ��", nameof(mobile)); + throw new ArgumentException("手机号不能为空", nameof(mobile)); } - // ��ȡ��ǰ�û� + // 获取当前用户 var currentUser = await _userService.GetUserByIdAsync(userId); if (currentUser == null) { - throw new InvalidOperationException("�û�������"); + throw new InvalidOperationException("用户不存在"); } - // ����ֻ����Ƿ��ѱ������û��� + // 检查手机号是否已被其他用户绑定 var existingUser = await _userService.GetUserByMobileAsync(mobile); if (existingUser != null && existingUser.Id != userId) { - // �ֻ����ѱ������û��󶨣���Ҫ�ϲ��˻� + // 手机号已被其他用户绑定,需要合并账户 return await MergeAccountsAsync(currentUser, existingUser); } - // �ֻ���δ���󶨣�ֱ�Ӹ��µ�ǰ�û����ֻ��� + // 手机号未被绑定,直接更新当前用户的手机号 await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile }); _logger.LogInformation("H5 Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile)); @@ -505,7 +516,7 @@ public class AuthService : IAuthService } /// - /// �˺�ע�� + /// 账号注销 /// Requirements: 7.1-7.3 /// public async Task LogOffAsync(long userId, int type) @@ -513,23 +524,23 @@ public class AuthService : IAuthService var user = await _userService.GetUserByIdAsync(userId); if (user == null) { - throw new InvalidOperationException("�û�������"); + throw new InvalidOperationException("用户不存在"); } try { - // 7.1 ��¼ע��������־ - var action = type == 0 ? "ע���˺�" : "ȡ��ע��"; + // 7.1 记录注销操作日志 + var action = type == 0 ? "注销账号" : "取消注销"; _logger.LogInformation("User log off request: UserId={UserId}, Type={Type}, Action={Action}", userId, type, action); - // ����������Ӹ����ע���߼������磺 - // - ���û�״̬����Ϊ��ע�� - // - �����û���صĻ��� - // - ����֪ͨ�� + // 这里可以添加更多的注销逻辑,比如: + // - 将用户状态设置为已注销 + // - 清除用户相关的缓存 + // - 发送通知等 if (type == 0) { - // ע���˺� - ���������û�״̬Ϊ���� + // 注销账号 - 软删除,设置用户状态为禁用 user.Status = 0; _dbContext.Users.Update(user); await _dbContext.SaveChangesAsync(); @@ -537,14 +548,14 @@ public class AuthService : IAuthService } else if (type == 1) { - // ȡ��ע�� - �ָ��û�״̬ + // 取消注销 - 恢复用户状态 user.Status = 1; _dbContext.Users.Update(user); await _dbContext.SaveChangesAsync(); _logger.LogInformation("User account reactivated: UserId={UserId}", userId); } - // 7.2 ����ע���ɹ�����Ϣ��ͨ�����׳��쳣����ʾ�ɹ��� + // 7.2 返回注销成功的信息(通过不抛出异常来表示成功) } catch (Exception ex) { @@ -557,24 +568,24 @@ public class AuthService : IAuthService #region Refresh Token Methods /// - /// ���� Refresh Token ���洢�����ݿ� + /// 生成 Refresh Token 并存储到数据库 /// Requirements: 1.4, 1.5, 4.1 /// - /// �û�ID - /// �ͻ��� IP ��ַ - /// ���ɵ� Refresh Token ���� + /// 用户ID + /// 客户端 IP 地址 + /// 生成的 Refresh Token 字符串 private async Task GenerateRefreshTokenAsync(long userId, string? ipAddress) { - // ������� Refresh Token + // 生成随机 Refresh Token var refreshToken = GenerateSecureRandomString(RefreshTokenLength); - // ���� SHA256 ��ϣֵ���ڴ洢 + // 计算 SHA256 哈希值用于存储 var tokenHash = ComputeSha256Hash(refreshToken); - // �������ʱ�䣨7�죩 + // 设置过期时间(7天) var expiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays); - // �������ݿ��¼ + // 保存数据库记录 var userRefreshToken = new UserRefreshToken { UserId = userId, @@ -593,21 +604,21 @@ public class AuthService : IAuthService } /// - /// ���ɵ�¼��Ӧ������˫ Token�� + /// 生成登录响应(包含双 Token) /// Requirements: 1.1, 1.2, 1.3, 1.4, 1.5 /// - /// �û�ʵ�� - /// �ͻ��� IP ��ַ - /// ��¼��Ӧ + /// 用户实体 + /// 客户端 IP 地址 + /// 登录响应 private async Task GenerateLoginResponseAsync(User user, string? ipAddress) { - // ���� Access Token (JWT) + // 生成 Access Token (JWT) var accessToken = _jwtService.GenerateToken(user); - // ���� Refresh Token ���洢 + // 生成 Refresh Token 并存储 var refreshToken = await GenerateRefreshTokenAsync(user.Id, ipAddress); - // ���� Access Token ����ʱ�䣨�룩 + // 计算 Access Token 过期时间(秒) var expiresIn = _jwtSettings.ExpirationMinutes * 60; return new LoginResponse @@ -620,7 +631,7 @@ public class AuthService : IAuthService } /// - /// ˢ�� Token + /// 刷新 Token /// Requirements: 2.1-2.6 /// public async Task RefreshTokenAsync(string refreshToken, string? ipAddress) @@ -628,15 +639,15 @@ public class AuthService : IAuthService if (string.IsNullOrWhiteSpace(refreshToken)) { _logger.LogWarning("Refresh token is empty"); - return RefreshTokenResult.Fail("ˢ�����Ʋ���Ϊ��"); + return RefreshTokenResult.Fail("刷新令牌不能为空"); } try { - // ���� Token ��ϣֵ + // 计算 Token 哈希值 var tokenHash = ComputeSha256Hash(refreshToken); - // ���� Token ��¼ + // 查找 Token 记录 var storedToken = await _dbContext.UserRefreshTokens .Include(t => t.User) .FirstOrDefaultAsync(t => t.TokenHash == tokenHash); @@ -644,47 +655,47 @@ public class AuthService : IAuthService if (storedToken == null) { _logger.LogWarning("Refresh token not found: {TokenHash}", tokenHash.Substring(0, 8) + "..."); - return RefreshTokenResult.Fail("��Ч��ˢ������"); + return RefreshTokenResult.Fail("无效的刷新令牌"); } - // ����Ƿ��ѹ��� + // 检查是否已过期 if (storedToken.IsExpired) { _logger.LogWarning("Refresh token expired for user {UserId}", storedToken.UserId); - return RefreshTokenResult.Fail("ˢ�������ѹ���"); + return RefreshTokenResult.Fail("刷新令牌已过期"); } - // ����Ƿ��ѳ��� + // 检查是否已撤销 if (storedToken.IsRevoked) { _logger.LogWarning("Refresh token revoked for user {UserId}", storedToken.UserId); - return RefreshTokenResult.Fail("ˢ��������ʧЧ"); + return RefreshTokenResult.Fail("刷新令牌已失效"); } - // ����û��Ƿ��������Ч + // 检查用户是否存在且有效 var user = storedToken.User; if (user == null) { _logger.LogWarning("User not found for refresh token"); - return RefreshTokenResult.Fail("�û�������"); + return RefreshTokenResult.Fail("用户不存在"); } if (user.Status == 0) { _logger.LogWarning("User {UserId} is disabled", user.Id); - return RefreshTokenResult.Fail("�˺��ѱ�����"); + return RefreshTokenResult.Fail("账号已被禁用"); } - // Token �ֻ��������µ� Refresh Token + // Token 轮换:生成新的 Refresh Token var newRefreshToken = GenerateSecureRandomString(RefreshTokenLength); var newTokenHash = ComputeSha256Hash(newRefreshToken); - // ������ Token ����¼������ϵ + // 标记旧 Token 的记录和替换关系 storedToken.RevokedAt = DateTime.Now; storedToken.RevokedByIp = ipAddress; storedToken.ReplacedByToken = newTokenHash; - // �����µ� Token ��¼ + // 保存新的 Token 记录 var newUserRefreshToken = new UserRefreshToken { UserId = user.Id, @@ -697,7 +708,7 @@ public class AuthService : IAuthService await _dbContext.UserRefreshTokens.AddAsync(newUserRefreshToken); await _dbContext.SaveChangesAsync(); - // �����µ� Access Token + // 生成新的 Access Token var accessToken = _jwtService.GenerateToken(user); var expiresIn = _jwtSettings.ExpirationMinutes * 60; @@ -714,12 +725,12 @@ public class AuthService : IAuthService catch (Exception ex) { _logger.LogError(ex, "Error refreshing token"); - return RefreshTokenResult.Fail("ˢ������ʧ�ܣ����Ժ�����"); + return RefreshTokenResult.Fail("刷新令牌失败,请稍后重试"); } } /// - /// ���� Token + /// 撤销 Token /// Requirements: 4.4 /// public async Task RevokeTokenAsync(string refreshToken, string? ipAddress) @@ -732,10 +743,10 @@ public class AuthService : IAuthService try { - // ���� Token ��ϣֵ + // 计算 Token 哈希值 var tokenHash = ComputeSha256Hash(refreshToken); - // ���� Token ��¼ + // 查找 Token 记录 var storedToken = await _dbContext.UserRefreshTokens .FirstOrDefaultAsync(t => t.TokenHash == tokenHash); @@ -745,14 +756,14 @@ public class AuthService : IAuthService return; } - // ����Ѿ�������ֱ�ӷ��� + // 如果已经撤销,直接返回 if (storedToken.IsRevoked) { _logger.LogInformation("Refresh token already revoked"); return; } - // ���� Token + // 撤销 Token storedToken.RevokedAt = DateTime.Now; storedToken.RevokedByIp = ipAddress; @@ -768,14 +779,14 @@ public class AuthService : IAuthService } /// - /// �����û������� Token + /// 撤销用户所有的 Token /// Requirements: 4.4 /// public async Task RevokeAllUserTokensAsync(long userId, string? ipAddress) { try { - // �����û�������Ч�� Token + // 查找用户所有有效的 Token var activeTokens = await _dbContext.UserRefreshTokens .Where(t => t.UserId == userId && t.RevokedAt == null) .ToListAsync(); @@ -809,7 +820,7 @@ public class AuthService : IAuthService #region Private Helper Methods /// - /// �ϲ��˻� - ����ǰ�û���openidǨ�Ƶ��ֻ����û� + /// 合并账户 - 将当前用户的openid迁移到手机号用户 /// private async Task MergeAccountsAsync(User currentUser, User mobileUser) { @@ -819,7 +830,7 @@ public class AuthService : IAuthService _logger.LogInformation("Merging accounts: CurrentUserId={CurrentUserId}, MobileUserId={MobileUserId}", currentUser.Id, mobileUser.Id); - // 5.2 ����ǰ�û���openidǨ�Ƶ��ֻ����û� + // 5.2 将当前用户的openid迁移到手机号用户 if (!string.IsNullOrWhiteSpace(currentUser.OpenId)) { mobileUser.OpenId = currentUser.OpenId; @@ -831,13 +842,13 @@ public class AuthService : IAuthService mobileUser.UpdateTime = DateTime.Now; _dbContext.Users.Update(mobileUser); - // ɾ����ǰ�û� + // 删除当前用户 _dbContext.Users.Remove(currentUser); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); - // 5.3 �����µ�token + // 5.3 生成新的token var newToken = _jwtService.GenerateToken(mobileUser); _logger.LogInformation("Accounts merged successfully: NewUserId={NewUserId}", mobileUser.Id); @@ -906,18 +917,18 @@ public class AuthService : IAuthService } /// - /// ����Ĭ��ͷ��URL + /// 生成默认头像URL /// private static string GenerateDefaultAvatar(string seed) { - // ʹ����������һ���򵥵�Ĭ��ͷ��URL - // ʵ����Ŀ�п���ʹ��Identicon�������ͷ�����ɷ��� + // 使用随机数生成一个简单的默认头像URL + // 实际项目中可以使用Identicon或其他头像生成方案 var hash = ComputeMd5(seed); return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}"; } /// - /// ����MD5��ϣ + /// 计算MD5哈希 /// private static string ComputeMd5(string input) { @@ -927,7 +938,7 @@ public class AuthService : IAuthService } /// - /// ��������ַ��� + /// 生成随机字符串 /// private static string GenerateRandomString(int length) { @@ -941,7 +952,7 @@ public class AuthService : IAuthService } /// - /// �����ֻ��� + /// 手机号脱敏 /// private static string MaskMobile(string mobile) { @@ -952,7 +963,7 @@ public class AuthService : IAuthService } /// - /// ��ȡ����е����� + /// 获取年份中的周数 /// private static int GetWeekOfYear(DateTime date) { @@ -961,7 +972,7 @@ public class AuthService : IAuthService } /// - /// ���� SHA256 ��ϣֵ + /// 计算 SHA256 哈希值 /// Requirements: 4.1 /// private static string ComputeSha256Hash(string input) @@ -972,7 +983,7 @@ public class AuthService : IAuthService } /// - /// ���ɰ�ȫ������ַ��������� Refresh Token�� + /// 生成安全随机字符串(用于 Refresh Token) /// private static string GenerateSecureRandomString(int length) { diff --git a/uniapp/App.vue b/uniapp/App.vue index cf4e982..4cd26fb 100644 --- a/uniapp/App.vue +++ b/uniapp/App.vue @@ -45,8 +45,14 @@ let inviterId = null // 1. 扫码进入:解析 scene 参数(inviter={userId}) - if (options.scene) { - const scene = decodeURIComponent(options.scene) + // 注意:options.scene 是微信启动场景值(数字,如1048), + // 自定义 scene 参数在 options.query.scene 中(字符串) + const sceneStr = (options.query && options.query.scene) + ? String(options.query.scene) + : (typeof options.scene === 'string' ? options.scene : '') + + if (sceneStr) { + const scene = decodeURIComponent(sceneStr) console.log('[App] 扫码 scene 解码后:', scene) const match = scene.match(/inviter=(\d+)/) if (match) { @@ -57,9 +63,10 @@ } } - // 2. 分享链接进入:解析 inviterId 参数 - if (!inviterId && options.inviterId) { - inviterId = options.inviterId + // 2. 分享链接进入:解析 inviterId 参数(query 中直接带 inviterId) + const queryInviterId = (options.query && options.query.inviterId) || options.inviterId + if (!inviterId && queryInviterId) { + inviterId = queryInviterId console.log('[App] 从query参数获取inviterId:', inviterId) } diff --git a/uniapp/pages/invite/index.vue b/uniapp/pages/invite/index.vue index aa451d0..bb575c3 100644 --- a/uniapp/pages/invite/index.vue +++ b/uniapp/pages/invite/index.vue @@ -277,22 +277,52 @@ function handleSaveQrcode() { uni.showToast({ title: '二维码未加载', icon: 'none' }) return } - uni.saveImageToPhotosAlbum({ - filePath: qrcodeUrl.value, - success: () => { uni.showToast({ title: '保存成功', icon: 'success' }) }, - fail: (err) => { - if (err.errMsg.includes('auth deny')) { - uni.showModal({ - title: '提示', - content: '需要您授权保存图片到相册', - confirmText: '去设置', - success: (res) => { if (res.confirm) uni.openSetting() } - }) - } else { - uni.showToast({ title: '保存失败', icon: 'none' }) + + /** + * 保存图片到相册 + * 需要先将网络图片下载到本地临时路径,再调用保存接口 + */ + function saveToAlbum(tempFilePath) { + uni.saveImageToPhotosAlbum({ + filePath: tempFilePath, + success: () => { uni.showToast({ title: '保存成功', icon: 'success' }) }, + fail: (err) => { + if (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize')) { + uni.showModal({ + title: '提示', + content: '需要您授权保存图片到相册', + confirmText: '去设置', + success: (res) => { if (res.confirm) uni.openSetting() } + }) + } else { + uni.showToast({ title: '保存失败', icon: 'none' }) + } } - } - }) + }) + } + + // 网络图片需要先下载到本地 + if (qrcodeUrl.value.startsWith('http')) { + uni.showLoading({ title: '保存中...' }) + uni.downloadFile({ + url: qrcodeUrl.value, + success: (res) => { + uni.hideLoading() + if (res.statusCode === 200) { + saveToAlbum(res.tempFilePath) + } else { + uni.showToast({ title: '下载图片失败', icon: 'none' }) + } + }, + fail: () => { + uni.hideLoading() + uni.showToast({ title: '下载图片失败', icon: 'none' }) + } + }) + } else { + // 本地路径直接保存 + saveToAlbum(qrcodeUrl.value) + } } function handleShowWithdraw() {