Merge branch 'master' of http://192.168.195.14:3000/outsource/mi-assessment
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-04-19 15:13:30 +08:00
commit 19bb76f5d5
12 changed files with 455 additions and 245 deletions

View File

@ -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

View File

@ -2,7 +2,7 @@
## 简介
本需求针对现有邀请新用户页面(`uniapp/pages/invite/index.vue`)进行 UI 增强和功能完善,使其对齐设计图。现有页面已具备基本功能(规则说明、二维码生成、分享链接、佣金展示、提现申请、提现记录、邀请记录),但 UI 布局和交互细节与设计稿存在差异。本次增强聚焦于视觉还原和交互优化,不涉及后端接口变更。
本需求针对现有邀请新用户页面(`uniapp/pages/invite/index.vue`)进行 UI 增强和功能完善,使其对齐设计图。现有页面已具备基本功能(规则说明 、二维码生成、分享链接、佣金展示、提现申请、提现记录、邀请记录),但 UI 布局和交互细节与设计稿存在差异。本次增强聚焦于视觉还原和交互优化,不涉及后端接口变更。
## 术语表

View File

@ -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
/// <summary>
/// 生成邀请码CSV内容
/// </summary>
/// <param name="items">邀请码列表</param>
/// <returns>CSV内容</returns>
private static string GenerateInviteCodeCsv(List<InviteCodeDto> 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();
/// <summary>
/// 生成佣金记录CSV内容
/// </summary>
/// <param name="items">佣金记录列表</param>
/// <returns>CSV内容</returns>
private static string GenerateCommissionCsv(List<CommissionDto> 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();
}
/// <summary>
/// 生成提现记录CSV内容
/// </summary>
/// <param name="items">提现记录列表</param>
/// <returns>CSV内容</returns>
private static string GenerateWithdrawalCsv(List<WithdrawalDto> 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

View File

@ -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
/// 导出订单列表
/// </summary>
/// <param name="request">查询请求</param>
/// <returns>导出数据</returns>
/// <returns>Excel文件</returns>
[HttpGet("export")]
[BusinessPermission("order:view")]
public async Task<IActionResult> 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)
{

View File

@ -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
/// 导出预约记录列表
/// </summary>
/// <param name="request">查询请求</param>
/// <returns>导出数据</returns>
/// <returns>Excel文件</returns>
[HttpGet("booking/export")]
[BusinessPermission("planner:view")]
public async Task<IActionResult> 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)
{

View File

@ -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
/// 导出用户列表
/// </summary>
/// <param name="request">查询请求</param>
/// <returns>导出数据</returns>
/// <returns>Excel文件</returns>
[HttpGet("export")]
[BusinessPermission("user:view")]
public async Task<IActionResult> 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)
{

View File

@ -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

View File

@ -191,6 +191,11 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
// blob 类型直接返回,不走业务 code 判断
if (response.config.responseType === 'blob') {
return { code: 0, message: 'success', data: response.data } as any
}
const res = response.data
// code 为 0 表示成功

View File

@ -185,7 +185,7 @@
<el-skeleton :loading="state.pendingLoading" animated :rows="3">
<template #default>
<div class="pending-items">
<div class="pending-item" @click="$router.push('/business/distribution/withdrawal')">
<div class="pending-item" @click="$router.push('/distribution/withdrawal')">
<div class="pending-icon pending-icon-warning">
<el-icon size="24"><Wallet /></el-icon>
</div>
@ -195,7 +195,7 @@
</div>
<el-icon class="pending-arrow"><ArrowRight /></el-icon>
</div>
<div class="pending-item" @click="$router.push('/business/planner/booking')">
<div class="pending-item" @click="$router.push('/planner/booking')">
<div class="pending-icon pending-icon-primary">
<el-icon size="24"><Calendar /></el-icon>
</div>
@ -234,22 +234,22 @@
<span>快捷操作</span>
</template>
<div class="quick-actions">
<el-button type="primary" @click="$router.push('/business/user')">
<el-button type="primary" @click="$router.push('/user/list')">
<el-icon><User /></el-icon>
</el-button>
<el-button type="success" @click="$router.push('/business/order')">
<el-button type="success" @click="$router.push('/order/list')">
<el-icon><ShoppingCart /></el-icon>
</el-button>
<el-button type="warning" @click="$router.push('/business/planner/list')">
<el-button type="warning" @click="$router.push('/planner/list')">
<el-icon><UserFilled /></el-icon>
</el-button>
<el-button type="info" @click="$router.push('/business/distribution/withdrawal')">
<el-button type="info" @click="$router.push('/distribution/withdrawal')">
<el-icon><Wallet /></el-icon>
</el-button>
<el-button @click="$router.push('/business/content/banner')">
<el-button @click="$router.push('/content/banner')">
<el-icon><Picture /></el-icon>
</el-button>
<el-button @click="$router.push('/business/assessment/type')">
<el-button @click="$router.push('/assessment/type')">
<el-icon><Document /></el-icon>
</el-button>
</div>

View File

@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>
/// 认证服务实现
/// </summary>
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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// Refresh Token 长度
private const int RefreshTokenLength = 64;
public AuthService(
@ -57,44 +57,44 @@ public class AuthService : IAuthService
/// <summary>
/// ΢<EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼
/// 微信小程序登录
/// Requirements: 1.1-1.8
/// </summary>
public async Task<LoginResult> WechatMiniProgramLoginAsync(string code, int? pid, string? clickId, string? phoneCode = null)
{
_logger.LogInformation("[AuthService] ΢<EFBFBD>ŵ<EFBFBD>¼<EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>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] ΢<EFBFBD>ŵ<EFBFBD>¼ʧ<EFBFBD>ܣ<EFBFBD>codeΪ<EFBFBD><EFBFBD>");
_logger.LogWarning("[AuthService] 微信登录失败code为空");
return new LoginResult
{
Success = false,
ErrorMessage = "<EFBFBD><EFBFBD>Ȩcode<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>"
ErrorMessage = "授权code不能为空"
};
}
try
{
// 1.6 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - 3<><33><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>¼
// 1.6 防抖处理 - 3秒内不允许重复登录
var debounceKey = $"{LoginDebounceKeyPrefix}wechat:{code}";
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Key}", debounceKey);
_logger.LogInformation("[AuthService] 防抖检查: {Key}", debounceKey);
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܾ<EFBFBD><EFBFBD>ظ<EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Code}", code);
_logger.LogWarning("[AuthService] 防抖处理:拒绝重复登录请求: {Code}", code);
return new LoginResult
{
Success = false,
ErrorMessage = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼"
ErrorMessage = "请勿频繁登录"
};
}
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD>ɹ<EFBFBD>");
_logger.LogInformation("[AuthService] 防抖锁获取成功");
// 1.1 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD>ȡopenid<EFBFBD><EFBFBD>unionid
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD>ȡopenid...");
// 1.1 调用微信API获取openid和unionid
_logger.LogInformation("[AuthService] 开始调用微信API获取openid...");
var wechatResult = await _wechatService.GetOpenIdAsync(code);
_logger.LogInformation("[AuthService] ΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>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] ΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>: {Error}", wechatResult.ErrorMessage);
_logger.LogWarning("[AuthService] 微信API调用失败: {Error}", wechatResult.ErrorMessage);
return new LoginResult
{
Success = false,
ErrorMessage = wechatResult.ErrorMessage ?? "<EFBFBD><EFBFBD>¼ʧ<EFBFBD>ܣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
ErrorMessage = wechatResult.ErrorMessage ?? "登录失败,请稍后重试"
};
}
var openId = wechatResult.OpenId!;
var unionId = wechatResult.UnionId;
// 1.2 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD> - <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>unionid<69><64><EFBFBD>ң<EFBFBD><D2A3><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>openid<69><64><EFBFBD><EFBFBD>
// 1.2 查找用户 - 优先通过unionid查找再通过openid查找
User? user = null;
if (!string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD>unionid<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>: {UnionId}", unionId);
_logger.LogInformation("[AuthService] 尝试通过unionid查找用户: {UnionId}", unionId);
user = await _userService.GetUserByUnionIdAsync(unionId);
_logger.LogInformation("[AuthService] unionid<EFBFBD><EFBFBD><EFBFBD>ҽ<EFBFBD><EFBFBD>: {Found}", user != null ? $"<22>ҵ<EFBFBD><D2B5>û<EFBFBD>ID={user.Id}" : <>ҵ<EFBFBD>");
_logger.LogInformation("[AuthService] unionid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到");
}
if (user == null)
{
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD>openid<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>: {OpenId}", openId);
_logger.LogInformation("[AuthService] 尝试通过openid查找用户: {OpenId}", openId);
user = await _userService.GetUserByOpenIdAsync(openId);
_logger.LogInformation("[AuthService] openid<EFBFBD><EFBFBD><EFBFBD>ҽ<EFBFBD><EFBFBD>: {Found}", user != null ? $"<22>ҵ<EFBFBD><D2B5>û<EFBFBD>ID={user.Id}" : <>ҵ<EFBFBD>");
_logger.LogInformation("[AuthService] openid查找结果: {Found}", user != null ? $"找到用户ID={user.Id}" : "未找到");
}
if (user == null)
@ -189,36 +189,42 @@ public class AuthService : IAuthService
}
}
// 1.5 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>Access Token + Refresh Token<65><6E>
_logger.LogInformation("[AuthService] <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token: UserId={UserId}", user.Id);
// 1.5 生成双 TokenAccess Token + Refresh Token
_logger.LogInformation("[AuthService] 开始生成双 Token: UserId={UserId}", user.Id);
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("[AuthService] ˫ Token <20><><EFBFBD>ɳɹ<C9B3><C9B9><EFBFBD>AccessToken<65><6E><EFBFBD><EFBFBD>={Length}", loginResponse.AccessToken?.Length ?? 0);
_logger.LogInformation("[AuthService] ΢<>ŵ<EFBFBD>¼<EFBFBD>ɹ<EFBFBD>: 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, // <20><><EFBFBD>ݾɰ<DDBE>
Token = loginResponse.AccessToken, // 兼容旧版
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[AuthService] ΢<EFBFBD>ŵ<EFBFBD>¼<EFBFBD>: 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 = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
ErrorMessage = "服务器异常,请稍后重试"
};
}
}
/// <summary>
/// <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD>¼
/// 手机号验证码登录
/// Requirements: 2.1-2.7
/// </summary>
public async Task<LoginResult> MobileLoginAsync(string mobile, string code, int? pid, string? clickId)
@ -228,7 +234,7 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "<EFBFBD>ֻ<EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>"
ErrorMessage = "手机号不能为空"
};
}
@ -237,13 +243,13 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>"
ErrorMessage = "验证码不能为空"
};
}
try
{
// 2.6 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - 3<><33><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>¼
// 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 = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼"
ErrorMessage = "请勿频繁登录"
};
}
// 2.1 <EFBFBD><EFBFBD>Redis<EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>
// 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 = "<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
ErrorMessage = "验证码错误"
};
}
// 2.2 <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><EFBFBD>Redis<EFBFBD>е<EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>
// 2.2 验证码验证通过删除Redis中的验证码
await _redisService.DeleteAsync(smsCodeKey);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
// 查找用户
var user = await _userService.GetUserByMobileAsync(mobile);
if (user == null)
{
// 2.3 <EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
// 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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>Access Token + Refresh Token<65><6E>
// 2.4 生成双 TokenAccess 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, // <EFBFBD><EFBFBD><EFBFBD>ݾɰ<EFBFBD>
Token = loginResponse.AccessToken, // 兼容旧版
UserId = user.Id,
LoginResponse = loginResponse
};
@ -310,58 +321,58 @@ public class AuthService : IAuthService
return new LoginResult
{
Success = false,
ErrorMessage = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"
ErrorMessage = "服务器异常,请稍后重试"
};
}
}
/// <summary>
/// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
/// 验证码绑定手机号
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> BindMobileAsync(long userId, string mobile, string code)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("<EFBFBD>ֻ<EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(mobile));
throw new ArgumentException("手机号不能为空", nameof(mobile));
}
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(code));
throw new ArgumentException("验证码不能为空", nameof(code));
}
// 5.1 <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>
// 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("<EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
throw new InvalidOperationException("验证码错误");
}
// <EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɾ<EFBFBD><EFBFBD>
// 验证码验证通过,删除
await _redisService.DeleteAsync(smsCodeKey);
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
// 获取当前用户
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
throw new InvalidOperationException("用户不存在");
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>
// 检查手机号是否已被其他用户绑定
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>󶨣<EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD>
// 5.2 手机号已被其他用户绑定,需要合并账户
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><EFBFBD>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
// 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
}
/// <summary>
/// ΢<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ȩ<EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
/// 微信授权绑定手机号
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> WechatBindMobileAsync(long userId, string wechatCode)
{
if (string.IsNullOrWhiteSpace(wechatCode))
{
throw new ArgumentException("΢<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ȩcode<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(wechatCode));
throw new ArgumentException("微信授权code不能为空", nameof(wechatCode));
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>΢<EFBFBD><EFBFBD>API<EFBFBD><EFBFBD>ȡ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
// 调用微信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 ?? "<EFBFBD><EFBFBD>ȡ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><EFBFBD>");
throw new InvalidOperationException(mobileResult.ErrorMessage ?? "获取手机号失败");
}
var mobile = mobileResult.Mobile;
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
// 获取当前用户
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
throw new InvalidOperationException("用户不存在");
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>
// 检查手机号是否已被其他用户绑定
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>󶨣<EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD>
// 5.2 手机号已被其他用户绑定,需要合并账户
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><EFBFBD>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
// 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
/// <summary>
/// <EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>Ϣ
/// 记录登录信息
/// Requirements: 6.1, 6.3, 6.4
/// </summary>
public async Task<RecordLoginResponse> 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("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
throw new InvalidOperationException("用户不存在");
}
try
{
// <EFBFBD><EFBFBD>ȡ<EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD>IP<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>ÿ<EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊռλ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD>IPӦ<EFBFBD><EFBFBD>Controller<EFBFBD><EFBFBD><EFBFBD>
// 获取客户端IP这里使用空字符串作为占位实际IP应由Controller传入
var clientIp = deviceInfo ?? string.Empty;
var now = DateTime.Now;
// 6.1 <EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>־
// 6.1 记录登录日志
var loginLog = new UserLoginLog
{
UserId = userId,
@ -446,7 +457,7 @@ public class AuthService : IAuthService
await _dbContext.UserLoginLogs.AddAsync(loginLog);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼ʱ<EFBFBD><EFBFBD>
// 更新用户最后登录时间
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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>uid<EFBFBD><EFBFBD><EFBFBD>dzƺ<EFBFBD>ͷ<EFBFBD><EFBFBD>
// 6.4 返回用户的uid、昵称和头像
return new RecordLoginResponse
{
Uid = user.Uid,
@ -471,33 +482,33 @@ public class AuthService : IAuthService
}
/// <summary>
/// H5<EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD>ţ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>
/// H5绑定手机号(无需验证码)
/// Requirements: 13.1
/// </summary>
public async Task<BindMobileResponse> BindMobileH5Async(long userId, string mobile)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("<EFBFBD>ֻ<EFBFBD><EFBFBD>Ų<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>", nameof(mobile));
throw new ArgumentException("手机号不能为空", nameof(mobile));
}
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
// 获取当前用户
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
throw new InvalidOperationException("用户不存在");
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>
// 检查手机号是否已被其他用户绑定
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>󶨣<EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD>
// 手机号已被其他用户绑定,需要合并账户
return await MergeAccountsAsync(currentUser, existingUser);
}
// <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><EFBFBD>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
// 手机号未被绑定,直接更新当前用户的手机号
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
}
/// <summary>
/// <EFBFBD>˺<EFBFBD>ע<EFBFBD><EFBFBD>
/// 账号注销
/// Requirements: 7.1-7.3
/// </summary>
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("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
throw new InvalidOperationException("用户不存在");
}
try
{
// 7.1 <EFBFBD><EFBFBD>¼ע<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
var action = type == 0 ? "ע<EFBFBD><EFBFBD><EFBFBD>˺<EFBFBD>" : <><C8A1>ע<EFBFBD><D7A2>";
// 7.1 记录注销操作日志
var action = type == 0 ? "注销账号" : "取消注销";
_logger.LogInformation("User log off request: UserId={UserId}, Type={Type}, Action={Action}", userId, type, action);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD>߼<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// - <EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>
// - <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>صĻ<EFBFBD><EFBFBD><EFBFBD>
// - <EFBFBD><EFBFBD><EFBFBD><EFBFBD>֪ͨ<EFBFBD><EFBFBD>
// 这里可以添加更多的注销逻辑,比如:
// - 将用户状态设置为已注销
// - 清除用户相关的缓存
// - 发送通知等
if (type == 0)
{
// ע<EFBFBD><EFBFBD><EFBFBD>˺<EFBFBD> - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>״̬Ϊ<CCAC><CEAA><EFBFBD><EFBFBD>
// 注销账号 - 软删除,设置用户状态为禁用
user.Status = 0;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
@ -537,14 +548,14 @@ public class AuthService : IAuthService
}
else if (type == 1)
{
// ȡ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD> - <20>ָ<EFBFBD><D6B8>û<EFBFBD>״̬
// 取消注销 - 恢复用户状态
user.Status = 1;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account reactivated: UserId={UserId}", userId);
}
// 7.2 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>׳<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ<EFBFBD>ɹ<EFBFBD><EFBFBD><EFBFBD>
// 7.2 返回注销成功的信息(通过不抛出异常来表示成功)
}
catch (Exception ex)
{
@ -557,24 +568,24 @@ public class AuthService : IAuthService
#region Refresh Token Methods
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token <20><><EFBFBD><EFBFBD><E6B4A2><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD>
/// 生成 Refresh Token 并存储到数据库
/// Requirements: 1.4, 1.5, 4.1
/// </summary>
/// <param name="userId"><EFBFBD>û<EFBFBD>ID</param>
/// <param name="ipAddress"><EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD> IP <20><>ַ</param>
/// <returns><EFBFBD><EFBFBD><EFBFBD>ɵ<EFBFBD> Refresh Token <20><><EFBFBD><EFBFBD></returns>
/// <param name="userId">用户ID</param>
/// <param name="ipAddress">客户端 IP 地址</param>
/// <returns>生成的 Refresh Token 字符串</returns>
private async Task<string> GenerateRefreshTokenAsync(long userId, string? ipAddress)
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token
// 生成随机 Refresh Token
var refreshToken = GenerateSecureRandomString(RefreshTokenLength);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> SHA256 <20><>ϣֵ<CFA3><D6B5><EFBFBD>ڴ洢
// 计算 SHA256 哈希值用于存储
var tokenHash = ComputeSha256Hash(refreshToken);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>䣨7<EFBFBD>
// 设置过期时间7天
var expiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD><EFBFBD>¼
// 保存数据库记录
var userRefreshToken = new UserRefreshToken
{
UserId = userId,
@ -593,21 +604,21 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD>ɵ<EFBFBD>¼<EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>
/// 生成登录响应(包含双 Token
/// Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
/// </summary>
/// <param name="user"><EFBFBD>û<EFBFBD>ʵ<EFBFBD><EFBFBD></param>
/// <param name="ipAddress"><EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD> IP <20><>ַ</param>
/// <returns><EFBFBD><EFBFBD>¼<EFBFBD><EFBFBD>Ӧ</returns>
/// <param name="user">用户实体</param>
/// <param name="ipAddress">客户端 IP 地址</param>
/// <returns>登录响应</returns>
private async Task<LoginResponse> GenerateLoginResponseAsync(User user, string? ipAddress)
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Access Token (JWT)
// 生成 Access Token (JWT)
var accessToken = _jwtService.GenerateToken(user);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token <20><><EFBFBD>
// 生成 Refresh Token 并存储
var refreshToken = await GenerateRefreshTokenAsync(user.Id, ipAddress);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Access Token <20><><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>
// 计算 Access Token 过期时间(秒)
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
return new LoginResponse
@ -620,7 +631,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// ˢ<EFBFBD><EFBFBD> Token
/// 刷新 Token
/// Requirements: 2.1-2.6
/// </summary>
public async Task<RefreshTokenResult> 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("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ʋ<EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><EFBFBD>");
return RefreshTokenResult.Fail("刷新令牌不能为空");
}
try
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>ϣֵ
// 计算 Token 哈希值
var tokenHash = ComputeSha256Hash(refreshToken);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>¼
// 查找 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("<EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD>ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
return RefreshTokenResult.Fail("无效的刷新令牌");
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD><EFBFBD>
// 检查是否已过期
if (storedToken.IsExpired)
{
_logger.LogWarning("Refresh token expired for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD><EFBFBD>");
return RefreshTokenResult.Fail("刷新令牌已过期");
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD>ѳ<EFBFBD><EFBFBD><EFBFBD>
// 检查是否已撤销
if (storedToken.IsRevoked)
{
_logger.LogWarning("Refresh token revoked for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧЧ");
return RefreshTokenResult.Fail("刷新令牌已失效");
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD>Ƿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч
// 检查用户是否存在且有效
var user = storedToken.User;
if (user == null)
{
_logger.LogWarning("User not found for refresh token");
return RefreshTokenResult.Fail("<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
return RefreshTokenResult.Fail("用户不存在");
}
if (user.Status == 0)
{
_logger.LogWarning("User {UserId} is disabled", user.Id);
return RefreshTokenResult.Fail("<EFBFBD>˺<EFBFBD><EFBFBD>ѱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
return RefreshTokenResult.Fail("账号已被禁用");
}
// Token <EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Refresh Token
// Token 轮换:生成新的 Refresh Token
var newRefreshToken = GenerateSecureRandomString(RefreshTokenLength);
var newTokenHash = ComputeSha256Hash(newRefreshToken);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ
// 标记旧 Token 的记录和替换关系
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
storedToken.ReplacedByToken = newTokenHash;
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Token <20><>¼
// 保存新的 Token 记录
var newUserRefreshToken = new UserRefreshToken
{
UserId = user.Id,
@ -697,7 +708,7 @@ public class AuthService : IAuthService
await _dbContext.UserRefreshTokens.AddAsync(newUserRefreshToken);
await _dbContext.SaveChangesAsync();
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> 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("ˢ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ժ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
return RefreshTokenResult.Fail("刷新令牌失败,请稍后重试");
}
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
/// 撤销 Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeTokenAsync(string refreshToken, string? ipAddress)
@ -732,10 +743,10 @@ public class AuthService : IAuthService
try
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>ϣֵ
// 计算 Token 哈希值
var tokenHash = ComputeSha256Hash(refreshToken);
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><>¼
// 查找 Token 记录
var storedToken = await _dbContext.UserRefreshTokens
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
@ -745,14 +756,14 @@ public class AuthService : IAuthService
return;
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ѿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֱ<EFBFBD>ӷ<EFBFBD><EFBFBD><EFBFBD>
// 如果已经撤销,直接返回
if (storedToken.IsRevoked)
{
_logger.LogInformation("Refresh token already revoked");
return;
}
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
// 撤销 Token
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
@ -768,14 +779,14 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
/// 撤销用户所有的 Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeAllUserTokensAsync(long userId, string? ipAddress)
{
try
{
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><EFBFBD> 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
/// <summary>
/// <EFBFBD>ϲ<EFBFBD><EFBFBD>˻<EFBFBD> - <20><><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD>openidǨ<64>Ƶ<EFBFBD><C6B5>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>û<EFBFBD>
/// 合并账户 - 将当前用户的openid迁移到手机号用户
/// </summary>
private async Task<BindMobileResponse> 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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD><EFBFBD><EFBFBD>openidǨ<EFBFBD>Ƶ<EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
// 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);
// ɾ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
// 删除当前用户
_dbContext.Users.Remove(currentUser);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 5.3 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD>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
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD>ͷ<EFBFBD><EFBFBD>URL
/// 生成默认头像URL
/// </summary>
private static string GenerateDefaultAvatar(string seed)
{
// ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD>򵥵<EFBFBD>Ĭ<EFBFBD><EFBFBD>ͷ<EFBFBD><EFBFBD>URL
// ʵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD>п<EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>Identicon<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɷ<EFBFBD><EFBFBD><EFBFBD>
// 使用随机数生成一个简单的默认头像URL
// 实际项目中可以使用Identicon或其他头像生成方案
var hash = ComputeMd5(seed);
return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}";
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>MD5<EFBFBD><EFBFBD>ϣ
/// 计算MD5哈希
/// </summary>
private static string ComputeMd5(string input)
{
@ -927,7 +938,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD>
/// 生成随机字符串
/// </summary>
private static string GenerateRandomString(int length)
{
@ -941,7 +952,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
/// 手机号脱敏
/// </summary>
private static string MaskMobile(string mobile)
{
@ -952,7 +963,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>е<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
/// 获取年份中的周数
/// </summary>
private static int GetWeekOfYear(DateTime date)
{
@ -961,7 +972,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> SHA256 <20><>ϣֵ
/// 计算 SHA256 哈希值
/// Requirements: 4.1
/// </summary>
private static string ComputeSha256Hash(string input)
@ -972,7 +983,7 @@ public class AuthService : IAuthService
}
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD>ɰ<EFBFBD>ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token<65><6E>
/// 生成安全随机字符串(用于 Refresh Token
/// </summary>
private static string GenerateSecureRandomString(int length)
{

View File

@ -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)
}

View File

@ -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() {