diff --git a/.kiro/specs/refresh-token/design.md b/.kiro/specs/refresh-token/design.md
new file mode 100644
index 00000000..6dfd3ea3
--- /dev/null
+++ b/.kiro/specs/refresh-token/design.md
@@ -0,0 +1,315 @@
+# Design Document: Refresh Token 机制
+
+## Overview
+
+为 HoneyBox 前端 API 实现双 Token 认证机制,包括:
+- 后端:修改登录响应、新增刷新接口、新增数据库表
+- 前端:实现自动刷新逻辑、请求队列管理
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ Token 流程图 │
+├─────────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────┐ 登录请求 ┌──────────────┐ │
+│ │ 前端 │ ──────────────> │ AuthController │ │
+│ │ UniApp │ │ /api/login │ │
+│ └──────────┘ └────────┬────────┘ │
+│ │ │ │
+│ │ 返回 accessToken │ 生成双 Token │
+│ │ + refreshToken ▼ │
+│ │ <────────────────── ┌──────────────────┐ │
+│ │ │ AuthService │ │
+│ │ │ - GenerateTokens│ │
+│ │ │ - SaveRefreshToken │
+│ │ └────────┬─────────┘ │
+│ │ │ │
+│ │ ▼ │
+│ │ ┌──────────────────┐ │
+│ │ │ user_refresh_tokens │ │
+│ │ │ (Database) │ │
+│ │ └──────────────────┘ │
+│ │ │
+│ │ API 请求 (携带 accessToken) │
+│ │ ──────────────────────────────────────────> │
+│ │ │
+│ │ 401 Unauthorized (Token 过期) │
+│ │ <────────────────────────────────────────── │
+│ │ │
+│ │ 刷新请求 (携带 refreshToken) │
+│ │ ──────────────────> ┌──────────────────┐ │
+│ │ │ /api/refresh │ │
+│ │ └────────┬─────────┘ │
+│ │ │ │
+│ │ 返回新的双 Token │ Token 轮换 │
+│ │ <────────────────────────────┘ │
+│ │ │
+│ │ 重试原始请求 (新 accessToken) │
+│ │ ──────────────────────────────────────────> │
+│ │ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+## Components and Interfaces
+
+### 1. 后端组件
+
+#### 1.1 UserRefreshToken 实体 (新增)
+
+```csharp
+// server/HoneyBox/src/HoneyBox.Model/Entities/UserRefreshToken.cs
+[Table("user_refresh_tokens")]
+public class UserRefreshToken
+{
+ [Key]
+ public long Id { get; set; }
+
+ public int UserId { get; set; }
+
+ [Required]
+ [MaxLength(256)]
+ public string TokenHash { get; set; } = null!;
+
+ public DateTime ExpiresAt { get; set; }
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+ [MaxLength(50)]
+ public string? CreatedByIp { get; set; }
+
+ public DateTime? RevokedAt { get; set; }
+
+ [MaxLength(50)]
+ public string? RevokedByIp { get; set; }
+
+ [MaxLength(256)]
+ public string? ReplacedByToken { get; set; }
+
+ // 计算属性
+ [NotMapped]
+ public bool IsExpired => DateTime.Now >= ExpiresAt;
+
+ [NotMapped]
+ public bool IsRevoked => RevokedAt != null;
+
+ [NotMapped]
+ public bool IsActive => !IsRevoked && !IsExpired;
+
+ // 导航属性
+ [ForeignKey("UserId")]
+ public virtual User User { get; set; } = null!;
+}
+```
+
+#### 1.2 登录响应模型 (修改)
+
+```csharp
+// server/HoneyBox/src/HoneyBox.Model/Models/Auth/LoginResponse.cs
+public class LoginResponse
+{
+ ///
+ /// Access Token (JWT),有效期 30 分钟
+ ///
+ public string AccessToken { get; set; } = null!;
+
+ ///
+ /// Refresh Token,有效期 7 天
+ ///
+ public string RefreshToken { get; set; } = null!;
+
+ ///
+ /// Access Token 过期时间(秒)
+ ///
+ public long ExpiresIn { get; set; }
+}
+```
+
+#### 1.3 IAuthService 接口 (扩展)
+
+```csharp
+// 新增方法
+Task RefreshTokenAsync(string refreshToken, string? ipAddress);
+Task RevokeTokenAsync(string refreshToken, string? ipAddress);
+Task RevokeAllUserTokensAsync(int userId, string? ipAddress);
+```
+
+#### 1.4 AuthController (新增刷新接口)
+
+```csharp
+// POST /api/refresh
+[HttpPost("refresh")]
+[AllowAnonymous]
+public async Task> RefreshToken([FromBody] RefreshTokenRequest request)
+```
+
+### 2. 前端组件
+
+#### 2.1 RequestManager (修改)
+
+```javascript
+// honey_box/common/request.js
+class RequestManager {
+ // 新增:刷新状态标记
+ static isRefreshing = false;
+
+ // 新增:等待刷新的请求队列
+ static refreshQueue = [];
+
+ // 新增:刷新 Token 方法
+ static async refreshToken() { ... }
+
+ // 修改:request 方法,处理 401 自动刷新
+ static async request(param) { ... }
+}
+```
+
+## Data Models
+
+### 数据库表结构
+
+```sql
+-- 新增 user_refresh_tokens 表
+CREATE TABLE user_refresh_tokens (
+ Id BIGINT IDENTITY(1,1) PRIMARY KEY,
+ UserId INT NOT NULL,
+ TokenHash NVARCHAR(256) NOT NULL,
+ ExpiresAt DATETIME2 NOT NULL,
+ CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
+ CreatedByIp NVARCHAR(50) NULL,
+ RevokedAt DATETIME2 NULL,
+ RevokedByIp NVARCHAR(50) NULL,
+ ReplacedByToken NVARCHAR(256) NULL,
+
+ CONSTRAINT FK_UserRefreshTokens_Users
+ FOREIGN KEY (UserId) REFERENCES users(id) ON DELETE CASCADE,
+
+ INDEX IX_UserRefreshTokens_UserId (UserId),
+ INDEX IX_UserRefreshTokens_TokenHash (TokenHash)
+);
+```
+
+### 前端存储结构
+
+```javascript
+// uni.storage 存储
+{
+ "accessToken": "eyJhbGciOiJIUzI1NiIs...", // JWT Access Token
+ "refreshToken": "a1b2c3d4e5f6...", // Refresh Token
+ "tokenExpireTime": 1706000000000 // Access Token 过期时间戳
+}
+```
+
+## Correctness Properties
+
+*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
+
+### Property 1: 登录响应结构完整性
+
+*For any* 成功的登录请求(微信或手机号),返回的响应对象 SHALL 包含非空的 accessToken、非空的 refreshToken 和正整数的 expiresIn。
+
+**Validates: Requirements 1.1, 1.2**
+
+### Property 2: Access Token 有效期正确性
+
+*For any* 生成的 Access Token,解析其 JWT payload 中的 exp claim,该值与当前时间的差值 SHALL 在 29-31 分钟范围内(允许 1 分钟误差)。
+
+**Validates: Requirements 1.3**
+
+### Property 3: Refresh Token 存储正确性
+
+*For any* 成功的登录请求,数据库中 SHALL 存在对应的 UserRefreshToken 记录,且 ExpiresAt 与当前时间的差值在 6.9-7.1 天范围内。
+
+**Validates: Requirements 1.4, 1.5**
+
+### Property 4: Token 刷新有效性
+
+*For any* 有效的 Refresh Token,调用刷新接口 SHALL 返回新的有效 accessToken 和 refreshToken,且新旧 Token 不相同。
+
+**Validates: Requirements 2.1**
+
+### Property 5: 无效 Token 拒绝
+
+*For any* 随机生成的无效字符串作为 Refresh Token,调用刷新接口 SHALL 返回错误响应。
+
+**Validates: Requirements 2.4**
+
+### Property 6: Token 轮换完整性
+
+*For any* 成功的 Token 刷新操作,旧的 Refresh Token SHALL 被标记为已撤销(RevokedAt 非空),且其 ReplacedByToken 字段 SHALL 指向新 Token 的哈希值。
+
+**Validates: Requirements 2.5, 2.6**
+
+### Property 7: Token 哈希存储安全性
+
+*For any* 存储在数据库中的 TokenHash,其值 SHALL 不等于原始 Refresh Token 明文,且长度为 64 字符(SHA256 十六进制)。
+
+**Validates: Requirements 4.1**
+
+### Property 8: Token 有效性检查完整性
+
+*For any* 已过期或已撤销的 Refresh Token,调用刷新接口 SHALL 返回错误响应,不返回新 Token。
+
+**Validates: Requirements 5.3**
+
+### Property 9: UserAccount 兼容性维护
+
+*For any* 成功的登录请求,UserAccount 表中对应用户的 account_token 字段 SHALL 被更新。
+
+**Validates: Requirements 6.3**
+
+## Error Handling
+
+### 后端错误处理
+
+| 错误场景 | HTTP 状态码 | 错误消息 |
+|---------|------------|---------|
+| Refresh Token 为空 | 400 | 刷新令牌不能为空 |
+| Refresh Token 无效 | 401 | 无效的刷新令牌 |
+| Refresh Token 已过期 | 401 | 刷新令牌已过期 |
+| Refresh Token 已撤销 | 401 | 刷新令牌已失效 |
+| 用户不存在 | 401 | 用户不存在 |
+| 用户已禁用 | 401 | 账号已被禁用 |
+
+### 前端错误处理
+
+| 错误场景 | 处理方式 |
+|---------|---------|
+| 刷新成功 | 更新本地 Token,重试原请求 |
+| 刷新失败 (401) | 清除本地存储,跳转登录页 |
+| 刷新失败 (网络错误) | 显示网络错误提示 |
+| 并发刷新 | 加入队列等待 |
+
+## Testing Strategy
+
+### 单元测试
+
+1. **AuthService 测试**
+ - 登录返回双 Token
+ - Token 刷新成功
+ - Token 刷新失败(各种错误场景)
+ - Token 撤销
+
+2. **JwtService 测试**
+ - Access Token 生成和验证
+ - Token 过期时间正确
+
+### 属性测试
+
+使用 FsCheck 或类似的属性测试库:
+
+1. **登录响应属性测试** - 验证响应结构完整性
+2. **Token 有效期属性测试** - 验证 Token 过期时间
+3. **Token 轮换属性测试** - 验证轮换机制正确性
+4. **Token 哈希属性测试** - 验证存储安全性
+
+### 集成测试
+
+1. **完整登录流程测试**
+2. **Token 刷新流程测试**
+3. **并发刷新测试**
+
+### 测试框架
+
+- 后端:xUnit + FsCheck
+- 前端:手动测试(UniApp 环境限制)
diff --git a/.kiro/specs/refresh-token/requirements.md b/.kiro/specs/refresh-token/requirements.md
new file mode 100644
index 00000000..b7319f2a
--- /dev/null
+++ b/.kiro/specs/refresh-token/requirements.md
@@ -0,0 +1,85 @@
+# Requirements Document
+
+## Introduction
+
+为 HoneyBox 前端 API 实现双 Token 认证机制(Access Token + Refresh Token),解决 Token 过期后用户需要重新登录的问题,提升用户体验。当 Access Token 过期时,前端可以使用 Refresh Token 自动获取新的 Token,实现无感刷新。
+
+## Glossary
+
+- **Access_Token**: 短期有效的 JWT 令牌,用于 API 请求认证,有效期 30 分钟
+- **Refresh_Token**: 长期有效的刷新令牌,用于获取新的 Access Token,有效期 7 天
+- **Token_Rotation**: Token 轮换机制,每次刷新时生成新的 Refresh Token 并废弃旧的
+- **Auth_Service**: 认证服务,处理登录、Token 生成和刷新逻辑
+- **Request_Manager**: 前端网络请求管理器,封装统一的请求方法
+- **User_Refresh_Token**: 用户刷新令牌数据库实体,存储 Refresh Token 信息
+
+## Requirements
+
+### Requirement 1: 登录接口返回双 Token
+
+**User Story:** As a 用户, I want 登录成功后获取 Access Token 和 Refresh Token, so that 我可以在 Access Token 过期后使用 Refresh Token 刷新而无需重新登录。
+
+#### Acceptance Criteria
+
+1. WHEN 用户通过微信小程序登录成功, THE Auth_Service SHALL 返回包含 accessToken、refreshToken 和 expiresIn 的响应对象
+2. WHEN 用户通过手机号验证码登录成功, THE Auth_Service SHALL 返回包含 accessToken、refreshToken 和 expiresIn 的响应对象
+3. THE Auth_Service SHALL 生成有效期为 30 分钟的 Access_Token
+4. THE Auth_Service SHALL 生成有效期为 7 天的 Refresh_Token
+5. THE Auth_Service SHALL 将 Refresh_Token 的哈希值存储到 User_Refresh_Token 表中
+
+### Requirement 2: Token 刷新接口
+
+**User Story:** As a 用户, I want 使用 Refresh Token 获取新的 Access Token, so that 我可以在 Access Token 过期后继续使用应用而无需重新登录。
+
+#### Acceptance Criteria
+
+1. WHEN 用户携带有效的 Refresh_Token 请求刷新接口, THE Auth_Service SHALL 返回新的 accessToken、refreshToken 和 expiresIn
+2. WHEN 用户携带已过期的 Refresh_Token 请求刷新接口, THE Auth_Service SHALL 返回错误信息 "刷新令牌已过期"
+3. WHEN 用户携带已撤销的 Refresh_Token 请求刷新接口, THE Auth_Service SHALL 返回错误信息 "刷新令牌已失效"
+4. WHEN 用户携带无效的 Refresh_Token 请求刷新接口, THE Auth_Service SHALL 返回错误信息 "无效的刷新令牌"
+5. WHEN Token 刷新成功, THE Auth_Service SHALL 撤销旧的 Refresh_Token 并生成新的 Refresh_Token(Token 轮换)
+6. THE Auth_Service SHALL 记录 Token 轮换的关联关系用于安全审计
+
+### Requirement 3: 前端自动刷新机制
+
+**User Story:** As a 用户, I want 前端在 Access Token 过期时自动刷新, so that 我可以无感知地继续使用应用。
+
+#### Acceptance Criteria
+
+1. WHEN 前端收到 HTTP 401 响应, THE Request_Manager SHALL 自动使用 Refresh_Token 调用刷新接口
+2. WHEN Token 刷新成功, THE Request_Manager SHALL 更新本地存储的 accessToken 和 refreshToken
+3. WHEN Token 刷新成功, THE Request_Manager SHALL 使用新的 Access_Token 重试原始请求
+4. WHEN Token 刷新失败(Refresh_Token 也过期), THE Request_Manager SHALL 清除本地存储并跳转到登录页
+5. WHILE 正在刷新 Token, THE Request_Manager SHALL 将其他请求加入队列等待刷新完成
+6. WHEN Token 刷新完成, THE Request_Manager SHALL 使用新 Token 依次执行队列中的请求
+
+### Requirement 4: Token 存储安全
+
+**User Story:** As a 系统管理员, I want Token 安全存储, so that 用户的认证信息不会被泄露。
+
+#### Acceptance Criteria
+
+1. THE Auth_Service SHALL 使用 SHA256 哈希存储 Refresh_Token,不存储明文
+2. THE Request_Manager SHALL 将 accessToken 和 refreshToken 存储在 uni.storage 中
+3. WHEN 用户退出登录, THE Request_Manager SHALL 清除本地存储的所有 Token
+4. WHEN 用户退出登录, THE Auth_Service SHALL 撤销该用户的 Refresh_Token
+
+### Requirement 5: 数据库支持
+
+**User Story:** As a 开发者, I want 数据库支持 Refresh Token 存储, so that 系统可以管理和验证 Refresh Token。
+
+#### Acceptance Criteria
+
+1. THE User_Refresh_Token 表 SHALL 包含字段:Id、UserId、TokenHash、ExpiresAt、CreatedAt、CreatedByIp、RevokedAt、RevokedByIp、ReplacedByToken
+2. THE User_Refresh_Token 表 SHALL 与 User 表建立外键关联
+3. WHEN 查询 Refresh_Token 时, THE Auth_Service SHALL 同时检查是否过期和是否已撤销
+
+### Requirement 6: 向后兼容
+
+**User Story:** As a 开发者, I want 新的认证机制向后兼容, so that 现有的前端代码可以平滑迁移。
+
+#### Acceptance Criteria
+
+1. THE Auth_Service SHALL 继续支持旧的登录响应格式(仅返回 token 字符串)直到前端完全迁移
+2. WHEN 前端未传递 refreshToken, THE Auth_Service SHALL 按照旧逻辑处理 401 错误
+3. THE Auth_Service SHALL 继续维护 UserAccount 表中的 account_token 用于兼容旧系统
diff --git a/.kiro/specs/refresh-token/tasks.md b/.kiro/specs/refresh-token/tasks.md
new file mode 100644
index 00000000..91119c00
--- /dev/null
+++ b/.kiro/specs/refresh-token/tasks.md
@@ -0,0 +1,142 @@
+# Implementation Plan: Refresh Token 机制
+
+## Overview
+
+实现双 Token 认证机制,分为后端改动和前端改动两个阶段。后端先完成数据库、实体、服务和接口的改动,前端再实现自动刷新逻辑。
+
+## Tasks
+
+- [x] 1. 后端:数据库和实体层
+ - [x] 1.1 创建 UserRefreshToken 实体类
+ - 在 `HoneyBox.Model/Entities/` 目录下创建 `UserRefreshToken.cs`
+ - 包含 Id、UserId、TokenHash、ExpiresAt、CreatedAt、CreatedByIp、RevokedAt、RevokedByIp、ReplacedByToken 字段
+ - 添加计算属性 IsExpired、IsRevoked、IsActive
+ - 添加与 User 表的外键关联
+ - _Requirements: 5.1, 5.2_
+
+ - [x] 1.2 更新 DbContext 添加 UserRefreshTokens DbSet
+ - 在 `HoneyBoxDbContext.cs` 中添加 `DbSet UserRefreshTokens`
+ - 配置表名和索引
+ - _Requirements: 5.1_
+
+ - [x] 1.3 创建数据库迁移脚本
+ - 创建 `create_user_refresh_tokens.sql` 脚本
+ - 包含表创建、索引和外键约束
+ - _Requirements: 5.1, 5.2_
+
+- [x] 2. 后端:模型和接口层
+ - [x] 2.1 创建 LoginResponse 模型
+ - 在 `HoneyBox.Model/Models/Auth/` 目录下创建 `LoginResponse.cs`
+ - 包含 AccessToken、RefreshToken、ExpiresIn 字段
+ - _Requirements: 1.1, 1.2_
+
+ - [x] 2.2 创建 RefreshTokenRequest 和 RefreshTokenResult 模型
+ - 创建请求模型包含 RefreshToken 字段
+ - 创建结果模型包含 Success、LoginResponse、ErrorMessage 字段
+ - _Requirements: 2.1_
+
+ - [x] 2.3 扩展 IAuthService 接口
+ - 添加 `RefreshTokenAsync` 方法
+ - 添加 `RevokeTokenAsync` 方法
+ - 添加 `RevokeAllUserTokensAsync` 方法
+ - _Requirements: 2.1, 4.4_
+
+- [-] 3. 后端:服务层实现
+ - [x] 3.1 实现 Refresh Token 生成和存储逻辑
+ - 在 AuthService 中添加 `GenerateRefreshToken` 私有方法
+ - 使用 SHA256 哈希存储 Token
+ - 设置 7 天有效期
+ - _Requirements: 1.4, 1.5, 4.1_
+
+ - [x] 3.2 修改登录方法返回双 Token
+ - 修改 `WechatMiniProgramLoginAsync` 返回 LoginResponse
+ - 修改 `MobileLoginAsync` 返回 LoginResponse
+ - 调用 GenerateRefreshToken 生成并存储 Refresh Token
+ - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
+
+ - [x] 3.3 实现 RefreshTokenAsync 方法
+ - 验证 Refresh Token 有效性(未过期、未撤销)
+ - 实现 Token 轮换(撤销旧 Token,生成新 Token)
+ - 记录 ReplacedByToken 关联关系
+ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 5.3_
+
+ - [x] 3.4 实现 RevokeTokenAsync 方法
+ - 根据 Token 哈希查找并撤销
+ - 记录撤销时间和 IP
+ - _Requirements: 4.4_
+
+ - [ ]* 3.5 编写属性测试:登录响应结构完整性
+ - **Property 1: 登录响应结构完整性**
+ - **Validates: Requirements 1.1, 1.2**
+
+ - [ ]* 3.6 编写属性测试:Token 轮换完整性
+ - **Property 6: Token 轮换完整性**
+ - **Validates: Requirements 2.5, 2.6**
+
+- [x] 4. Checkpoint - 后端服务层完成
+ - 确保所有测试通过,ask the user if questions arise.
+
+- [x] 5. 后端:控制器层
+ - [x] 5.1 修改 AuthController 登录接口返回格式
+ - 修改 `WechatMiniProgramLogin` 返回 `ApiResponse`
+ - 修改 `MobileLogin` 返回 `ApiResponse`
+ - 保持向后兼容(同时返回 token 字段)
+ - _Requirements: 1.1, 1.2, 6.1_
+
+ - [x] 5.2 新增 Token 刷新接口
+ - 添加 `POST /api/refresh` 端点
+ - 接收 RefreshTokenRequest,返回 LoginResponse
+ - 处理各种错误情况
+ - _Requirements: 2.1, 2.2, 2.3, 2.4_
+
+ - [x] 5.3 新增 Token 撤销接口(可选,用于退出登录)
+ - 添加 `POST /api/logout` 端点
+ - 撤销用户的 Refresh Token
+ - _Requirements: 4.4_
+
+- [x] 6. Checkpoint - 后端完成
+ - 确保所有测试通过,ask the user if questions arise.
+ - 使用 Postman 或 curl 测试接口
+
+- [x] 7. 前端:Token 存储改造
+ - [x] 7.1 修改登录成功后的 Token 存储逻辑
+ - 存储 accessToken 和 refreshToken 到 uni.storage
+ - 计算并存储 tokenExpireTime
+ - _Requirements: 4.2_
+
+ - [x] 7.2 修改退出登录清除 Token 逻辑
+ - 清除 accessToken、refreshToken、tokenExpireTime
+ - 调用后端撤销接口(可选)
+ - _Requirements: 4.3_
+
+- [x] 8. 前端:自动刷新机制
+ - [x] 8.1 实现 refreshToken 方法
+ - 调用 `/api/refresh` 接口
+ - 更新本地存储的 Token
+ - 返回刷新结果
+ - _Requirements: 3.1, 3.2_
+
+ - [x] 8.2 实现请求队列机制
+ - 添加 isRefreshing 状态标记
+ - 添加 refreshQueue 请求队列
+ - 刷新完成后依次执行队列中的请求
+ - _Requirements: 3.5, 3.6_
+
+ - [x] 8.3 修改 request 方法处理 401 自动刷新
+ - 检测 401 响应触发刷新
+ - 刷新成功后重试原请求
+ - 刷新失败后跳转登录页
+ - _Requirements: 3.1, 3.3, 3.4_
+
+- [x] 9. Final Checkpoint - 全部完成
+ - 确保所有测试通过,ask the user if questions arise.
+ - 端到端测试:登录 → 等待过期 → 自动刷新 → 继续使用
+
+## Notes
+
+- Tasks marked with `*` are optional and can be skipped for faster MVP
+- 后端改动需要先执行数据库迁移脚本
+- 前端改动需要在后端接口完成后进行
+- 建议先在测试环境验证,再部署到生产环境
+- Access Token 有效期设置为 30 分钟,可根据需要调整
+- Refresh Token 有效期设置为 7 天,可根据需要调整
diff --git a/honey_box/common/config.js b/honey_box/common/config.js
index 10eba427..83767a95 100644
--- a/honey_box/common/config.js
+++ b/honey_box/common/config.js
@@ -193,7 +193,9 @@ const defaultConfig = {
}],
"app_setting": {
"key": "app_setting",
- "app_name": "友达赏",
+ "app_name": "哈尼盲盒",
+ "share_title": "哈尼盲盒上线,来就送!",
+ "share_title_detail": "哈尼盲盒,正版潮玩手办一番赏",
"purchase_popup": "1",
"exchange_times": "2",
"balance_name": "钻石",
diff --git a/honey_box/common/platform/BasePlatform.js b/honey_box/common/platform/BasePlatform.js
index bbc29f87..1789787e 100644
--- a/honey_box/common/platform/BasePlatform.js
+++ b/honey_box/common/platform/BasePlatform.js
@@ -4,6 +4,7 @@ import {
import {
getPlatform, getAdvert, getConfig, getDanYe
} from '../server/config';
+import { logout, clearLoginTokens } from '../server/auth';
import ConfigManager from '../config';
/**
* 多端平台抽象基类(父类)
@@ -227,16 +228,32 @@ class BasePlatform {
/**
* 处理退出登录
+ * 清除所有Token(accessToken、refreshToken、tokenExpireTime)
+ * 可选调用后端撤销接口
*/
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
- success: (res) => {
+ success: async (res) => {
if (res.confirm) {
- uni.removeStorageSync('token');
- uni.removeStorageSync('userinfo');
+ try {
+ // 调用后端撤销接口并清除本地存储
+ await logout();
+ } catch (error) {
+ console.log('退出登录时发生错误:', error);
+ // 即使后端调用失败,也确保清除本地存储
+ clearLoginTokens();
+ }
+ // 刷新页面
+ // #ifdef H5
window.location.href = window.location.href;
+ // #endif
+ // #ifndef H5
+ uni.reLaunch({
+ url: '/pages/shouye/index'
+ });
+ // #endif
}
}
});
diff --git a/honey_box/common/request.js b/honey_box/common/request.js
index a564b4c1..a6c44d35 100644
--- a/honey_box/common/request.js
+++ b/honey_box/common/request.js
@@ -10,6 +10,12 @@ import { apiWhiteList } from '@/common/config.js'
import RouterManager from '@/common/router.js'
import { platform } from '@/common/platform/PlatformFactory'
class RequestManager {
+ // Token 刷新状态标记
+ static isRefreshing = false;
+
+ // 等待刷新的请求队列
+ static refreshQueue = [];
+
// 缓存对象
static cache = {
data: new Map(),
@@ -43,6 +49,149 @@ class RequestManager {
this.cache.timestamps.set(cacheKey, Date.now());
}
+ /**
+ * 刷新 Token
+ * 使用 Refresh Token 获取新的 Access Token
+ * @returns {Promise} 刷新是否成功
+ */
+ static async refreshToken() {
+ const refreshToken = uni.getStorageSync('refreshToken');
+
+ if (!refreshToken) {
+ console.log('没有 refreshToken,无法刷新');
+ return false;
+ }
+
+ try {
+ const apiBaseUrl = EnvConfig.apiBaseUrl;
+ const baseUrlWithSlash = apiBaseUrl.endsWith('/') ? apiBaseUrl : apiBaseUrl + '/';
+ const requestUrl = baseUrlWithSlash + 'api/refresh';
+
+ console.log('开始刷新 Token...');
+
+ const response = await new Promise((resolve, reject) => {
+ uni.request({
+ url: requestUrl,
+ method: 'POST',
+ header: {
+ 'content-type': 'application/json'
+ },
+ data: {
+ refreshToken: refreshToken
+ },
+ success: (res) => resolve(res),
+ fail: (err) => reject(err)
+ });
+ });
+
+ // 检查 HTTP 状态码
+ if (response.statusCode === 200 && response.data && response.data.status === 1) {
+ const data = response.data.data;
+
+ // 更新本地存储的 Token
+ if (data.accessToken) {
+ uni.setStorageSync('token', data.accessToken);
+ uni.setStorageSync('accessToken', data.accessToken);
+ }
+
+ if (data.refreshToken) {
+ uni.setStorageSync('refreshToken', data.refreshToken);
+ }
+
+ if (data.expiresIn) {
+ // 计算过期时间戳(当前时间 + expiresIn 秒)
+ const expireTime = Date.now() + (data.expiresIn * 1000);
+ uni.setStorageSync('tokenExpireTime', expireTime);
+ }
+
+ console.log('Token 刷新成功');
+ return true;
+ } else {
+ console.log('Token 刷新失败:', response.data?.msg || '未知错误');
+ return false;
+ }
+ } catch (error) {
+ console.error('Token 刷新请求失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 将请求加入等待队列
+ * @param {Function} resolve Promise resolve 函数
+ * @param {Function} reject Promise reject 函数
+ * @param {Object} param 原始请求参数
+ */
+ static addToRefreshQueue(resolve, reject, param) {
+ this.refreshQueue.push({ resolve, reject, param });
+ }
+
+ /**
+ * 处理等待队列中的请求
+ * 刷新成功后,使用新 Token 重新执行队列中的所有请求
+ * @param {boolean} success 刷新是否成功
+ */
+ static processRefreshQueue(success) {
+ console.log(`处理刷新队列,共 ${this.refreshQueue.length} 个请求,刷新${success ? '成功' : '失败'}`);
+
+ this.refreshQueue.forEach(({ resolve, reject, param }) => {
+ if (success) {
+ // 刷新成功,重新执行请求
+ this.request(param)
+ .then(resolve)
+ .catch(reject);
+ } else {
+ // 刷新失败,拒绝所有等待的请求
+ reject({ status: -1, msg: '登录已过期' });
+ }
+ });
+
+ // 清空队列
+ this.refreshQueue = [];
+ }
+
+ /**
+ * 清除所有 Token 并跳转登录页
+ */
+ static clearTokensAndRedirect() {
+ // 清除所有 Token 相关存储
+ uni.removeStorageSync('token');
+ uni.removeStorageSync('accessToken');
+ uni.removeStorageSync('refreshToken');
+ uni.removeStorageSync('tokenExpireTime');
+ uni.removeStorageSync('userinfo');
+
+ // 保存当前页面用于登录后跳转
+ var pages = getCurrentPages();
+ var currentPage = pages[pages.length - 1];
+ if (currentPage) {
+ var currentRoute = currentPage.route;
+ var currentParams = currentPage.options || {};
+ if (currentRoute && currentRoute !== 'pages/user/login') {
+ var redirectPath = '/' + currentRoute;
+ if (Object.keys(currentParams).length > 0) {
+ var paramString = Object.keys(currentParams)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(currentParams[key])}`)
+ .join('&');
+ redirectPath += '?' + paramString;
+ }
+ uni.setStorageSync('redirect', redirectPath);
+ }
+ }
+
+ setTimeout(() => {
+ uni.showToast({
+ title: '登录已过期,请重新登录',
+ icon: 'none'
+ });
+ }, 100);
+
+ RouterManager.navigateTo('/pages/user/login', {}, 'navigateTo')
+ .catch(err => {
+ console.error('登录页面跳转失败:', err);
+ });
+ }
+
/**
* 发送带缓存的GET请求
* @param {String} url 请求地址
@@ -247,7 +396,73 @@ class RequestManager {
header: header,
data: data,
success: res => {
- console.log("res.data.status", res.data.status)
+ console.log("res.data.status", res.data.status, "res.statusCode", res.statusCode)
+
+ // 处理 HTTP 401 未授权(Token 过期或无效)
+ if (res.statusCode === 401) {
+ console.log('Token过期或无效,尝试自动刷新');
+
+ // 白名单接口不进行自动刷新,直接拒绝
+ if (RequestManager.isUrlInWhitelist(requestUrl)) {
+ reject({ status: -1, msg: '登录已过期' });
+ return;
+ }
+
+ // 检查是否有 refreshToken
+ const refreshToken = uni.getStorageSync('refreshToken');
+ if (!refreshToken) {
+ console.log('没有 refreshToken,直接跳转登录页');
+ RequestManager.clearTokensAndRedirect();
+ reject({ status: -1, msg: '登录已过期' });
+ return;
+ }
+
+ // 如果正在刷新中,将当前请求加入队列等待
+ if (RequestManager.isRefreshing) {
+ console.log('Token 正在刷新中,将请求加入队列');
+ RequestManager.addToRefreshQueue(resolve, reject, param);
+ return;
+ }
+
+ // 标记开始刷新
+ RequestManager.isRefreshing = true;
+
+ // 尝试刷新 Token
+ RequestManager.refreshToken()
+ .then(success => {
+ // 刷新完成,重置标记
+ RequestManager.isRefreshing = false;
+
+ if (success) {
+ console.log('Token 刷新成功,重试原请求');
+ // 刷新成功,重试原请求
+ RequestManager.request(param)
+ .then(resolve)
+ .catch(reject);
+
+ // 处理队列中的其他请求
+ RequestManager.processRefreshQueue(true);
+ } else {
+ console.log('Token 刷新失败,跳转登录页');
+ // 刷新失败,清除 Token 并跳转登录页
+ RequestManager.clearTokensAndRedirect();
+ reject({ status: -1, msg: '登录已过期' });
+
+ // 拒绝队列中的所有请求
+ RequestManager.processRefreshQueue(false);
+ }
+ })
+ .catch(error => {
+ console.error('Token 刷新异常:', error);
+ RequestManager.isRefreshing = false;
+ RequestManager.clearTokensAndRedirect();
+ reject({ status: -1, msg: '登录已过期' });
+ RequestManager.processRefreshQueue(false);
+ });
+
+ return;
+ }
+
var pages = getCurrentPages()
if (res.data.status == 1) {
// 请求成功
@@ -292,7 +507,11 @@ class RequestManager {
var pages = getCurrentPages()
if (res.data.msg != null) {
if (res.data.msg.indexOf("没有找到用户信息") > -1) {
+ // 清除所有Token相关存储
uni.removeStorageSync('token');
+ uni.removeStorageSync('accessToken');
+ uni.removeStorageSync('refreshToken');
+ uni.removeStorageSync('tokenExpireTime');
uni.removeStorageSync('userinfo');
}
}
@@ -334,7 +553,11 @@ class RequestManager {
icon: 'none'
})
}, 100)
+ // 清除所有Token相关存储
uni.removeStorageSync('token');
+ uni.removeStorageSync('accessToken');
+ uni.removeStorageSync('refreshToken');
+ uni.removeStorageSync('tokenExpireTime');
uni.removeStorageSync('userinfo');
// 使用新的路由守卫方法进行跳转
RouterManager.navigateTo('/pages/user/login', {}, 'navigateTo')
diff --git a/honey_box/common/server/auth.js b/honey_box/common/server/auth.js
index 96aa6b88..4d5f0b5d 100644
--- a/honey_box/common/server/auth.js
+++ b/honey_box/common/server/auth.js
@@ -3,6 +3,89 @@
*/
import RequestManager from '../request';
+/**
+ * 保存登录Token到本地存储
+ * 支持新版双Token格式和旧版单Token格式
+ * @param {Object|String} data 登录响应数据
+ * @param {String} data.accessToken 访问令牌(新版)
+ * @param {String} data.refreshToken 刷新令牌(新版)
+ * @param {Number} data.expiresIn 过期时间(秒)(新版)
+ */
+export const saveLoginTokens = (data) => {
+ if (typeof data === 'string') {
+ // 旧版格式:直接返回token字符串
+ uni.setStorageSync('token', data);
+ } else if (data && typeof data === 'object') {
+ // 新版格式:返回包含accessToken、refreshToken、expiresIn的对象
+ if (data.accessToken) {
+ uni.setStorageSync('token', data.accessToken);
+ uni.setStorageSync('accessToken', data.accessToken);
+ }
+ if (data.refreshToken) {
+ uni.setStorageSync('refreshToken', data.refreshToken);
+ }
+ if (data.expiresIn) {
+ // 计算过期时间戳(当前时间 + expiresIn秒)
+ const tokenExpireTime = Date.now() + (data.expiresIn * 1000);
+ uni.setStorageSync('tokenExpireTime', tokenExpireTime);
+ }
+ // 兼容旧版:如果响应中有token字段(向后兼容)
+ if (data.token && !data.accessToken) {
+ uni.setStorageSync('token', data.token);
+ }
+ }
+};
+
+/**
+ * 清除所有登录Token
+ */
+export const clearLoginTokens = () => {
+ uni.removeStorageSync('token');
+ uni.removeStorageSync('accessToken');
+ uni.removeStorageSync('refreshToken');
+ uni.removeStorageSync('tokenExpireTime');
+ uni.removeStorageSync('userinfo');
+};
+
+/**
+ * 获取当前存储的Token信息
+ * @returns {Object} Token信息对象
+ */
+export const getStoredTokens = () => {
+ return {
+ token: uni.getStorageSync('token'),
+ accessToken: uni.getStorageSync('accessToken'),
+ refreshToken: uni.getStorageSync('refreshToken'),
+ tokenExpireTime: uni.getStorageSync('tokenExpireTime')
+ };
+};
+
+/**
+ * 检查Access Token是否即将过期(提前5分钟)
+ * @returns {Boolean} 是否即将过期
+ */
+export const isTokenExpiringSoon = () => {
+ const tokenExpireTime = uni.getStorageSync('tokenExpireTime');
+ if (!tokenExpireTime) {
+ return false;
+ }
+ // 提前5分钟(300秒)认为即将过期
+ const bufferTime = 5 * 60 * 1000;
+ return Date.now() >= (tokenExpireTime - bufferTime);
+};
+
+/**
+ * 检查Access Token是否已过期
+ * @returns {Boolean} 是否已过期
+ */
+export const isTokenExpired = () => {
+ const tokenExpireTime = uni.getStorageSync('tokenExpireTime');
+ if (!tokenExpireTime) {
+ return false;
+ }
+ return Date.now() >= tokenExpireTime;
+};
+
/**
* 微信登录
* @param {Object} params 登录参数
@@ -62,6 +145,36 @@ export const bindMobileByCode = async (mobile, code) => {
});
};
+/**
+ * 刷新Token
+ * @param {String} refreshToken 刷新令牌
+ * @returns {Promise} 刷新结果,包含新的accessToken、refreshToken、expiresIn
+ */
+export const refreshToken = async (refreshToken) => {
+ return await RequestManager.post('/refresh', {
+ refreshToken
+ });
+};
+
+/**
+ * 退出登录(撤销Refresh Token)
+ * @returns {Promise} 退出结果
+ */
+export const logout = async () => {
+ const storedRefreshToken = uni.getStorageSync('refreshToken');
+ if (storedRefreshToken) {
+ try {
+ await RequestManager.post('/logout', {
+ refreshToken: storedRefreshToken
+ });
+ } catch (error) {
+ console.log('撤销Token失败,继续清除本地存储:', error);
+ }
+ }
+ // 无论后端调用是否成功,都清除本地存储
+ clearLoginTokens();
+};
+
/**
* 记录登录
* @returns {Promise} 记录结果
diff --git a/honey_box/components/coupon-pop/coupon-pop.vue b/honey_box/components/coupon-pop/coupon-pop.vue
index 42803b81..c508a18f 100644
--- a/honey_box/components/coupon-pop/coupon-pop.vue
+++ b/honey_box/components/coupon-pop/coupon-pop.vue
@@ -30,7 +30,7 @@
-