更新 RefundManagement.vue 组件,优化链接编号输入提示,新增输入格式说明,支持从URL提取链接编号,提升用户体验。
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(git add:*)"
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
318
docs/公告管理使用说明.md
318
docs/公告管理使用说明.md
@@ -1,117 +1,259 @@
|
|||||||
# 公告管理使用说明
|
基础信息
|
||||||
|
|
||||||
## 功能概述
|
- 基础路径: /api/admin/announcement
|
||||||
|
- 认证方式: JWT Token(必须在Header中添加:Authorization: Bearer <token>)
|
||||||
|
- 内容类型: application/json
|
||||||
|
|
||||||
公告管理模块提供了完整的公告信息管理功能,包括公告的创建、编辑、删除、启用/禁用等操作。
|
---
|
||||||
|
1. 创建公告
|
||||||
|
|
||||||
## 功能特性
|
POST /api/admin/announcement
|
||||||
|
|
||||||
- ✅ 公告列表查看(支持分页)
|
请求头
|
||||||
- ✅ 按标题/内容关键词搜索
|
|
||||||
- ✅ 按启用状态筛选
|
|
||||||
- ✅ 创建新公告
|
|
||||||
- ✅ 编辑现有公告
|
|
||||||
- ✅ 删除公告
|
|
||||||
- ✅ 一键启用/禁用公告
|
|
||||||
- ✅ 支持跳转链接设置
|
|
||||||
- ✅ 权限控制(管理员和代理商不同权限)
|
|
||||||
|
|
||||||
## 权限说明
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
### 管理员权限
|
请求参数
|
||||||
- 查看公告列表
|
|
||||||
- 创建新公告
|
|
||||||
- 编辑现有公告
|
|
||||||
- 删除公告
|
|
||||||
- 启用/禁用公告
|
|
||||||
|
|
||||||
### 代理商权限
|
{
|
||||||
- 查看公告列表(只读)
|
"title": "string", // 必填 - 公告标题,最大长度100字符
|
||||||
|
"content": "string", // 必填 - 公告内容
|
||||||
|
"enabled": boolean, // 必填 - 是否启用
|
||||||
|
"jumpUrl": "string" // 可选 - 跳转链接
|
||||||
|
}
|
||||||
|
|
||||||
## 使用步骤
|
注意: belongId 字段已从请求参数中移除,系统会自动从JWT token中解析当前用户ID并设置为 belongId
|
||||||
|
|
||||||
### 1. 访问公告管理
|
请求示例
|
||||||
- 登录管理后台
|
|
||||||
- 在左侧导航菜单中点击"公告管理"
|
|
||||||
|
|
||||||
### 2. 查看公告列表
|
{
|
||||||
- 公告列表显示所有公告信息
|
"title": "系统维护通知",
|
||||||
- 支持按关键词搜索(标题或内容)
|
"content": "系统将于今晚22:00-24:00进行维护升级,请提前保存工作内容",
|
||||||
- 支持按启用状态筛选
|
"enabled": true,
|
||||||
- 支持分页浏览
|
"jumpUrl": "https://example.com/maintenance"
|
||||||
|
}
|
||||||
|
|
||||||
### 3. 创建新公告
|
响应结果
|
||||||
1. 点击"新增公告"按钮
|
|
||||||
2. 填写公告信息:
|
|
||||||
- **标题**:必填,公告标题
|
|
||||||
- **内容**:必填,公告详细内容
|
|
||||||
- **跳转链接**:可选,点击公告后跳转的URL
|
|
||||||
- **状态**:选择是否启用
|
|
||||||
3. 点击"保存"完成创建
|
|
||||||
|
|
||||||
### 4. 编辑公告
|
{
|
||||||
1. 在公告列表中找到要编辑的公告
|
"success": true,
|
||||||
2. 点击"编辑"按钮
|
"message": "公告创建成功",
|
||||||
3. 修改公告信息
|
"id": 1
|
||||||
4. 点击"保存"完成修改
|
}
|
||||||
|
|
||||||
### 5. 删除公告
|
错误响应
|
||||||
1. 在公告列表中找到要删除的公告
|
|
||||||
2. 点击"删除"按钮
|
|
||||||
3. 确认删除操作
|
|
||||||
|
|
||||||
### 6. 启用/禁用公告
|
{
|
||||||
- 在公告列表中,直接点击状态开关即可快速启用或禁用公告
|
"success": false,
|
||||||
- 只有启用的公告才会对用户可见
|
"message": "用户未认证"
|
||||||
|
}
|
||||||
|
|
||||||
## 字段说明
|
---
|
||||||
|
2. 获取公告列表(分页)
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
GET /api/admin/announcement/list
|
||||||
|------|------|------|------|
|
|
||||||
| 标题 | 文本 | 是 | 公告标题,用于列表显示 |
|
|
||||||
| 内容 | 文本 | 是 | 公告详细内容 |
|
|
||||||
| 跳转链接 | URL | 否 | 点击公告后跳转的链接地址 |
|
|
||||||
| 状态 | 布尔 | 是 | 是否启用,只有启用的公告用户才能看到 |
|
|
||||||
|
|
||||||
## 注意事项
|
查询参数
|
||||||
|
|
||||||
1. **权限控制**:只有具备相应权限的用户才能进行相应操作
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
2. **数据验证**:标题和内容为必填字段
|
|----------|---------|-----|-----|---------|
|
||||||
3. **URL验证**:跳转链接必须是有效的URL格式(以http://或https://开头)
|
| page | integer | 否 | 1 | 页码 |
|
||||||
4. **状态控制**:只有启用的公告才会在前端显示给用户
|
| size | integer | 否 | 20 | 每页大小 |
|
||||||
5. **操作记录**:所有操作都会记录创建时间和更新时间
|
| enabled | boolean | 否 | - | 按启用状态筛选 |
|
||||||
|
| belongId | integer | 否 | - | 按归属ID筛选 |
|
||||||
|
|
||||||
## API接口
|
请求示例
|
||||||
|
|
||||||
公告管理使用以下API接口:
|
GET /api/admin/announcement/list?page=1&size=10&enabled=true&belongId=123
|
||||||
|
|
||||||
- `GET /api/admin/announcement/list` - 获取公告列表
|
响应结果
|
||||||
- `POST /api/admin/announcement` - 创建公告
|
|
||||||
- `PUT /api/admin/announcement/{id}` - 更新公告
|
|
||||||
- `DELETE /api/admin/announcement/{id}` - 删除公告
|
|
||||||
- `PUT /api/admin/announcement/{id}/enabled` - 更新启用状态
|
|
||||||
- `GET /api/admin/announcement/enabled` - 获取启用的公告
|
|
||||||
|
|
||||||
详细的API文档请参考项目根目录的接口文档。
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "系统维护通知",
|
||||||
|
"content": "系统将于今晚22:00-24:00进行维护升级",
|
||||||
|
"enabled": true,
|
||||||
|
"jumpUrl": "https://example.com/maintenance",
|
||||||
|
"belongId": 123,
|
||||||
|
"createdAt": "2025-08-29T10:30:00",
|
||||||
|
"updatedAt": "2025-08-29T10:30:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"size": 10
|
||||||
|
}
|
||||||
|
|
||||||
## 故障排除
|
---
|
||||||
|
3. 获取公告详情
|
||||||
|
|
||||||
### 常见问题
|
GET /api/admin/announcement/{id}
|
||||||
|
|
||||||
1. **无法访问公告管理页面**
|
路径参数
|
||||||
- 检查用户是否有相应权限
|
|
||||||
- 确认已正确登录
|
|
||||||
|
|
||||||
2. **创建公告失败**
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
- 检查标题和内容是否已填写
|
|-----|------|-----|------|
|
||||||
- 检查跳转链接格式是否正确
|
| id | long | 是 | 公告ID |
|
||||||
|
|
||||||
3. **状态切换失败**
|
请求示例
|
||||||
- 检查网络连接
|
|
||||||
- 确认用户有编辑权限
|
|
||||||
|
|
||||||
4. **搜索无结果**
|
GET /api/admin/announcement/1
|
||||||
- 检查搜索关键词是否正确
|
|
||||||
- 尝试重置搜索条件
|
|
||||||
|
|
||||||
如有其他问题,请联系系统管理员。
|
响应结果
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "系统维护通知",
|
||||||
|
"content": "系统将于今晚22:00-24:00进行维护升级,请提前保存工作内容",
|
||||||
|
"enabled": true,
|
||||||
|
"jumpUrl": "https://example.com/maintenance",
|
||||||
|
"belongId": 123,
|
||||||
|
"createdAt": "2025-08-29T10:30:00",
|
||||||
|
"updatedAt": "2025-08-29T10:30:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
4. 更新公告
|
||||||
|
|
||||||
|
PUT /api/admin/announcement/{id}
|
||||||
|
|
||||||
|
请求头
|
||||||
|
|
||||||
|
Authorization: Bearer <JWT_TOKEN>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
路径参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| id | long | 是 | 公告ID |
|
||||||
|
|
||||||
|
请求参数
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "string", // 可选 - 公告标题
|
||||||
|
"content": "string", // 可选 - 公告内容
|
||||||
|
"enabled": boolean, // 可选 - 是否启用
|
||||||
|
"jumpUrl": "string" // 可选 - 跳转链接
|
||||||
|
}
|
||||||
|
|
||||||
|
注意: 更新时 belongId 会自动从JWT token中获取并更新
|
||||||
|
|
||||||
|
请求示例
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "系统维护通知(更新)",
|
||||||
|
"content": "系统维护时间调整为23:00-01:00",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
|
||||||
|
响应结果
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "公告更新成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
5. 删除公告
|
||||||
|
|
||||||
|
DELETE /api/admin/announcement/{id}
|
||||||
|
|
||||||
|
路径参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| id | long | 是 | 公告ID |
|
||||||
|
|
||||||
|
请求示例
|
||||||
|
|
||||||
|
DELETE /api/admin/announcement/1
|
||||||
|
|
||||||
|
响应结果
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "公告删除成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
6. 更新公告启用状态
|
||||||
|
|
||||||
|
PUT /api/admin/announcement/{id}/enabled
|
||||||
|
|
||||||
|
路径参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|-----|------|-----|------|
|
||||||
|
| id | long | 是 | 公告ID |
|
||||||
|
|
||||||
|
查询参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|---------|---------|-----|------|
|
||||||
|
| enabled | boolean | 是 | 启用状态 |
|
||||||
|
|
||||||
|
请求示例
|
||||||
|
|
||||||
|
PUT /api/admin/announcement/1/enabled?enabled=false
|
||||||
|
|
||||||
|
响应结果
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "公告已禁用"
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
7. 获取启用的公告
|
||||||
|
|
||||||
|
GET /api/admin/announcement/enabled
|
||||||
|
|
||||||
|
查询参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|----------|---------|-----|---------|
|
||||||
|
| belongId | integer | 否 | 按归属ID筛选 |
|
||||||
|
|
||||||
|
请求示例
|
||||||
|
|
||||||
|
GET /api/admin/announcement/enabled?belongId=123
|
||||||
|
|
||||||
|
响应结果
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "系统维护通知",
|
||||||
|
"content": "系统将于今晚22:00-24:00进行维护升级",
|
||||||
|
"enabled": true,
|
||||||
|
"jumpUrl": "https://example.com/maintenance",
|
||||||
|
"belongId": 123,
|
||||||
|
"createdAt": "2025-08-29T10:30:00",
|
||||||
|
"updatedAt": "2025-08-29T10:30:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
---
|
||||||
|
主要变更说明
|
||||||
|
|
||||||
|
🔒 安全改进
|
||||||
|
|
||||||
|
- 自动用户识别: 创建和更新公告时,系统自动从JWT token中解析当前用户ID
|
||||||
|
- 防止伪造: 前端无法伪造 belongId,确保公告只能归属于当前登录用户
|
||||||
|
|
||||||
|
📝 请求参数变更
|
||||||
|
|
||||||
|
- 移除: 创建和更新公告的请求中不再需要传递 belongId 参数
|
||||||
|
- 简化: 前端调用更加简洁,无需关心用户ID的传递
|
||||||
|
|
||||||
|
🎯 使用场景
|
||||||
|
|
||||||
|
1. 管理员: 创建的公告 belongId 为管理员用户ID
|
||||||
|
2. 代理商: 创建的公告 belongId 为代理商用户ID
|
||||||
|
3. 权限隔离: 通过 belongId 实现不同角色的公告隔离
|
||||||
|
|
||||||
|
这样的设计既保证了安全性,又提供了灵活的公告管理能力!
|
||||||
@@ -27,9 +27,11 @@ export function refundLink(codeNo) {
|
|||||||
return http.post(`/api/link/${codeNo}/refund`)
|
return http.post(`/api/link/${codeNo}/refund`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询链接状态
|
// 查询链接状态 - 使用GET请求,codeNo作为查询参数
|
||||||
export function getLinkStatus(codeNo) {
|
export function getLinkStatus(codeNo) {
|
||||||
return http.get(`/api/link/${codeNo}/status`)
|
return http.get('/api/link/status', {
|
||||||
|
params: { codeNo }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按状态批量删除链接
|
// 按状态批量删除链接
|
||||||
|
|||||||
@@ -3,52 +3,73 @@ import { getAccessToken, getRefreshToken, getTokenType, setTokens, clearTokens }
|
|||||||
import { showErrorMessage } from '@/utils/error'
|
import { showErrorMessage } from '@/utils/error'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
|
|
||||||
const baseURL = import.meta.env?.VITE_API_BASE || '/'
|
// —— 关键改动 1:默认用 /api,确保走 Vite 代理 ——
|
||||||
|
// 配合 .env.development 里:VITE_API_BASE=/api
|
||||||
|
const baseURL = import.meta.env?.VITE_API_BASE || '/api'
|
||||||
|
|
||||||
|
// 业务实例
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 专用刷新客户端,避免拦截器递归
|
// 刷新专用实例(不挂业务拦截器,避免递归)
|
||||||
const refreshClient = axios.create({ baseURL, timeout: 15000 })
|
const refreshClient = axios.create({
|
||||||
|
baseURL,
|
||||||
|
timeout: 15000,
|
||||||
|
})
|
||||||
|
|
||||||
let isRefreshing = false
|
let isRefreshing = false
|
||||||
let pendingQueue = []
|
let pendingQueue = []
|
||||||
|
|
||||||
function subscribeTokenRefresh(cb) {
|
function subscribeTokenRefresh(cb) { pendingQueue.push(cb) }
|
||||||
pendingQueue.push(cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRefreshed(newToken, tokenType) {
|
function onRefreshed(newToken, tokenType) {
|
||||||
pendingQueue.forEach((cb) => cb(newToken, tokenType))
|
pendingQueue.forEach(cb => cb(newToken, tokenType))
|
||||||
pendingQueue = []
|
pendingQueue = []
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRefreshFailed() {
|
function onRefreshFailed() {
|
||||||
pendingQueue.forEach((cb) => cb(null, null))
|
pendingQueue.forEach(cb => cb(null, null))
|
||||||
pendingQueue = []
|
pendingQueue = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 请求拦截:附加 Authorization
|
/** 统一获取“最终请求路径”(含 baseURL 的路径部分),用于可靠匹配公开接口 */
|
||||||
|
function getPathname(config) {
|
||||||
|
const url = config?.url || ''
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
try { return new URL(url).pathname } catch { return url }
|
||||||
|
}
|
||||||
|
const base = config?.baseURL ?? baseURL
|
||||||
|
let basePath = ''
|
||||||
|
try { basePath = new URL(base, 'http://local').pathname } catch { basePath = base || '' }
|
||||||
|
|
||||||
|
const joined =
|
||||||
|
(basePath.endsWith('/') ? basePath.slice(0, -1) : basePath) +
|
||||||
|
(url.startsWith('/') ? '' : '/') + url
|
||||||
|
return joined.replace(/\/{2,}/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 关键改动 2:以“尾部路径”匹配公开接口,不受是否带 /api 影响 ——
|
||||||
|
// 注意最后一个是动态路径:/link/{code}/game-interface
|
||||||
|
const PUBLIC_PATTERNS = [
|
||||||
|
/^\/(?:api\/)?link\/status$/,
|
||||||
|
/^\/(?:api\/)?link\/select-region$/,
|
||||||
|
/^\/(?:api\/)?link\/poll-login$/,
|
||||||
|
/^\/(?:api\/)?link\/progress$/,
|
||||||
|
/^\/(?:api\/)?link\/refresh$/,
|
||||||
|
/^\/(?:api\/)?link\/qr\.png$/,
|
||||||
|
/^\/(?:api\/)?link\/[^/]+\/game-interface$/,
|
||||||
|
]
|
||||||
|
function isPublicPathname(pathname) {
|
||||||
|
return PUBLIC_PATTERNS.some(re => re.test(pathname))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求拦截:附加 Authorization(公开接口不带)
|
||||||
http.interceptors.request.use(
|
http.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
// 跳过公开API的身份验证(只跳过用户端游戏相关的公开接口)
|
const pathname = getPathname(config)
|
||||||
const publicAPIs = [
|
if (!isPublicPathname(pathname)) {
|
||||||
'/api/link/status',
|
|
||||||
'/api/link/select-region',
|
|
||||||
'/api/link/poll-login',
|
|
||||||
'/api/link/progress',
|
|
||||||
'/api/link/refresh',
|
|
||||||
'/api/link/qr.png'
|
|
||||||
]
|
|
||||||
// 游戏界面接口使用动态路径,需要特殊处理
|
|
||||||
const isGameInterfaceAPI = /\/api\/link\/[^\/]+\/game-interface/.test(config.url || '')
|
|
||||||
const isPublicAPI = publicAPIs.some(api => config.url?.includes(api)) || isGameInterfaceAPI
|
|
||||||
|
|
||||||
if (!isPublicAPI) {
|
|
||||||
const token = getAccessToken()
|
const token = getAccessToken()
|
||||||
const type = getTokenType()
|
const type = getTokenType() || 'Bearer'
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers = config.headers || {}
|
config.headers = config.headers || {}
|
||||||
config.headers.Authorization = `${type} ${token}`
|
config.headers.Authorization = `${type} ${token}`
|
||||||
@@ -59,43 +80,52 @@ http.interceptors.request.use(
|
|||||||
(error) => Promise.reject(error)
|
(error) => Promise.reject(error)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 刷新 token 请求
|
// 刷新 token 请求(确保不携带 Authorization)
|
||||||
async function refreshTokenRequest() {
|
async function refreshTokenRequest() {
|
||||||
const refreshToken = getRefreshToken()
|
const refreshToken = getRefreshToken()
|
||||||
if (!refreshToken) throw new Error('NO_REFRESH_TOKEN')
|
if (!refreshToken) throw new Error('NO_REFRESH_TOKEN')
|
||||||
const { data } = await refreshClient.post('/auth/refresh', { refreshToken })
|
const { data } = await refreshClient.post(
|
||||||
// 兼容 { code, data }
|
'/auth/refresh',
|
||||||
|
{ refreshToken },
|
||||||
|
{
|
||||||
|
transformRequest: [(payload, headers) => {
|
||||||
|
if (headers && 'Authorization' in headers) delete headers.Authorization
|
||||||
|
return JSON.stringify(payload)
|
||||||
|
}],
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
)
|
||||||
const payload = data?.code === 0 && data?.data ? data.data : data
|
const payload = data?.code === 0 && data?.data ? data.data : data
|
||||||
if (!payload?.accessToken) throw new Error('INVALID_REFRESH_RESPONSE')
|
if (!payload?.accessToken) throw new Error('INVALID_REFRESH_RESPONSE')
|
||||||
setTokens(payload)
|
setTokens(payload) // 内部应同时保存 tokenType/refreshToken 等
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应拦截:处理 401 自动刷新
|
function logStatus(status, config, tag = '') {
|
||||||
|
const method = (config?.method || '').toUpperCase()
|
||||||
|
const url = getPathname?.(config) || config?.url || ''
|
||||||
|
console.log(`[HTTP${tag ? ' ' + tag : ''}] status=${status} ${method} ${url}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应拦截:401 自动刷新 + 重放 + 打印状态码
|
||||||
http.interceptors.response.use(
|
http.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => {
|
||||||
|
// 成功响应也打印
|
||||||
|
logStatus(response?.status, response?.config, 'OK')
|
||||||
|
return response
|
||||||
|
},
|
||||||
async (error) => {
|
async (error) => {
|
||||||
|
console.error("HTTP 响应拦截器捕获到错误:", error)
|
||||||
const { config, response } = error || {}
|
const { config, response } = error || {}
|
||||||
const status = response?.status
|
const status = response?.status
|
||||||
const url = config?.url || ''
|
const pathname = getPathname(config)
|
||||||
|
const isAuthPath = /\/auth\/(login|refresh)$/.test(pathname)
|
||||||
|
const publicAPI = isPublicPathname(pathname)
|
||||||
|
|
||||||
// 避免对登录/刷新自身和公开API进行重复刷新
|
if (status === 401 && !isAuthPath && !publicAPI) {
|
||||||
const isAuthPath = /\/auth\/(login|refresh)/.test(url || '')
|
console.log("检测到401错误,尝试刷新token")
|
||||||
const publicAPIs = [
|
// 有些后端会带 Basic 的挑战头,删除以避免奇怪行为(XHR一般不会弹窗,但删除更干净)
|
||||||
'/api/link/status',
|
if (error.response?.headers?.['www-authenticate']) {
|
||||||
'/api/link/select-region',
|
|
||||||
'/api/link/poll-login',
|
|
||||||
'/api/link/progress',
|
|
||||||
'/api/link/refresh',
|
|
||||||
'/api/link/qr.png'
|
|
||||||
]
|
|
||||||
// 游戏界面接口使用动态路径,需要特殊处理
|
|
||||||
const isGameInterfaceAPI = /\/api\/link\/[^\/]+\/game-interface/.test(url || '')
|
|
||||||
const isPublicAPI = publicAPIs.some(api => url?.includes(api)) || isGameInterfaceAPI
|
|
||||||
|
|
||||||
if (status === 401 && !isAuthPath && !isPublicAPI) {
|
|
||||||
// 阻止浏览器显示基本认证弹窗
|
|
||||||
if (error.response && error.response.headers) {
|
|
||||||
delete error.response.headers['www-authenticate']
|
delete error.response.headers['www-authenticate']
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,35 +139,26 @@ http.interceptors.response.use(
|
|||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
clearTokens()
|
clearTokens()
|
||||||
onRefreshFailed()
|
onRefreshFailed()
|
||||||
// 跳转到登录页面,保存当前路径用于登录后重定向
|
|
||||||
const currentPath = router.currentRoute.value.fullPath
|
const currentPath = router.currentRoute.value.fullPath
|
||||||
if (currentPath !== '/login') {
|
if (currentPath !== '/login') {
|
||||||
router.replace({
|
router.replace({ name: 'Login', query: { redirect: currentPath } })
|
||||||
name: 'Login',
|
|
||||||
query: { redirect: currentPath }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回一个新的 Promise,等待刷新完成后重试原请求
|
// 等待刷新结果后重放原请求
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
subscribeTokenRefresh((newToken, tokenType) => {
|
subscribeTokenRefresh((newToken, tokenType) => {
|
||||||
if (newToken) {
|
if (!newToken) return reject(error)
|
||||||
const retryConfig = { ...config }
|
const retry = { ...config }
|
||||||
retryConfig.headers = retryConfig.headers || {}
|
retry.headers = { ...(config.headers || {}), Authorization: `${tokenType || 'Bearer'} ${newToken}` }
|
||||||
retryConfig.headers.Authorization = `${tokenType || 'Bearer'} ${newToken}`
|
resolve(http.request(retry))
|
||||||
resolve(http.request(retryConfig))
|
|
||||||
} else {
|
|
||||||
// token刷新失败,拒绝请求
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于非401错误,显示错误消息
|
// 其它错误直接提示
|
||||||
if (status !== 401) {
|
if (status !== 401) {
|
||||||
showErrorMessage(error)
|
showErrorMessage(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
v-model="searchForm.codeNo"
|
v-model="searchForm.codeNo"
|
||||||
size="large"
|
size="large"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入链接编号(支持粘贴后回车)"
|
placeholder="请输入链接编号或完整链接URL(支持粘贴后回车)"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -185,6 +185,15 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="输入格式说明" name="5">
|
||||||
|
<ul class="help-list">
|
||||||
|
<li>支持直接输入链接编号,如:<code class="mono">RUC74NCV</code></li>
|
||||||
|
<li>支持输入完整链接URL,如:<code class="mono">http://localhost:5173/play?code=RUC74NCV</code></li>
|
||||||
|
<li>系统会自动从URL中提取链接编号</li>
|
||||||
|
<li>支持使用粘贴按钮或Ctrl+V快速粘贴</li>
|
||||||
|
</ul>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
<el-collapse-item title="注意事项" name="4">
|
<el-collapse-item title="注意事项" name="4">
|
||||||
<ul class="help-list">
|
<ul class="help-list">
|
||||||
<li>退单操作不可逆,请谨慎操作</li>
|
<li>退单操作不可逆,请谨慎操作</li>
|
||||||
@@ -293,7 +302,23 @@ export default {
|
|||||||
ElMessage.info('剪贴板为空')
|
ElMessage.info('剪贴板为空')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
searchForm.codeNo = text.trim()
|
|
||||||
|
// 提取实际的代码编号
|
||||||
|
const actualCode = extractCodeFromInput(text)
|
||||||
|
if (!actualCode) {
|
||||||
|
ElMessage.warning('剪贴板内容中未找到有效的链接编号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchForm.codeNo = actualCode
|
||||||
|
|
||||||
|
// 如果提取出的代码与原文本不同,提示用户
|
||||||
|
if (actualCode !== text.trim()) {
|
||||||
|
ElMessage.success(`已自动提取链接编号: ${actualCode}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('已粘贴链接编号')
|
||||||
|
}
|
||||||
|
|
||||||
// 自动触发查询(可按需注释)
|
// 自动触发查询(可按需注释)
|
||||||
if (searchForm.codeNo) handleSearch()
|
if (searchForm.codeNo) handleSearch()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -311,17 +336,76 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从URL中提取代码编号
|
||||||
|
const extractCodeFromInput = (input) => {
|
||||||
|
if (!input) return ''
|
||||||
|
|
||||||
|
// 移除首尾空格
|
||||||
|
const trimmed = input.trim()
|
||||||
|
|
||||||
|
// 如果是URL,尝试提取code参数
|
||||||
|
try {
|
||||||
|
if (trimmed.includes('play?code=')) {
|
||||||
|
const url = new URL(trimmed)
|
||||||
|
const code = url.searchParams.get('code')
|
||||||
|
if (code) return code
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 如果URL解析失败,继续下面的处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从各种URL格式中提取代码
|
||||||
|
if (trimmed.includes('code=')) {
|
||||||
|
const match = trimmed.match(/code=([^&\s]+)/)
|
||||||
|
if (match) return match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果包含路径分隔符,取最后一个部分
|
||||||
|
if (trimmed.includes('/')) {
|
||||||
|
const parts = trimmed.split('/')
|
||||||
|
const lastPart = parts[parts.length - 1]
|
||||||
|
if (lastPart && lastPart.length >= 6) {
|
||||||
|
return lastPart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回输入(假设就是代码)
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
// 查询
|
// 查询
|
||||||
const handleSearch = async () => {
|
const handleSearch = async () => {
|
||||||
if (!searchForm.codeNo.trim()) {
|
if (!searchForm.codeNo.trim()) {
|
||||||
ElMessage.warning('请输入链接编号')
|
ElMessage.warning('请输入链接编号')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从输入中提取实际的代码编号
|
||||||
|
const actualCode = extractCodeFromInput(searchForm.codeNo)
|
||||||
|
if (!actualCode) {
|
||||||
|
ElMessage.warning('无法从输入中提取有效的链接编号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果提取出的代码与输入不同,更新输入框显示
|
||||||
|
if (actualCode !== searchForm.codeNo.trim()) {
|
||||||
|
searchForm.codeNo = actualCode
|
||||||
|
ElMessage.info(`已自动提取链接编号: ${actualCode}`)
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await getLinkStatus(searchForm.codeNo.trim())
|
const response = await getLinkStatus(actualCode)
|
||||||
linkInfo.value = response.data
|
linkInfo.value = response.data
|
||||||
|
|
||||||
|
// 调试:检查返回的数据结构
|
||||||
|
console.log('getLinkStatus response:', response.data)
|
||||||
|
|
||||||
|
// 确保 codeNo 字段存在
|
||||||
|
if (linkInfo.value && !linkInfo.value.codeNo && !linkInfo.value.code) {
|
||||||
|
linkInfo.value.codeNo = actualCode
|
||||||
|
}
|
||||||
|
|
||||||
// 如果选择了状态筛选,仅做前端提示
|
// 如果选择了状态筛选,仅做前端提示
|
||||||
if (linkInfo.value && searchForm.status && linkInfo.value.status !== searchForm.status) {
|
if (linkInfo.value && searchForm.status && linkInfo.value.status !== searchForm.status) {
|
||||||
ElMessage.info(`已找到链接,但状态为「${getStatusText(linkInfo.value.status)}」`)
|
ElMessage.info(`已找到链接,但状态为「${getStatusText(linkInfo.value.status)}」`)
|
||||||
@@ -363,10 +447,22 @@ export default {
|
|||||||
|
|
||||||
// 确认退单
|
// 确认退单
|
||||||
const confirmRefund = async () => {
|
const confirmRefund = async () => {
|
||||||
if (!linkInfo.value) return
|
if (!linkInfo.value) {
|
||||||
|
ElMessage.error('链接信息不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实际的代码编号,支持多种字段名
|
||||||
|
const codeNo = linkInfo.value.codeNo || linkInfo.value.code || searchForm.codeNo
|
||||||
|
if (!codeNo) {
|
||||||
|
ElMessage.error('无法获取链接编号,请重新查询')
|
||||||
|
console.error('linkInfo:', linkInfo.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
refunding.value = true
|
refunding.value = true
|
||||||
try {
|
try {
|
||||||
await refundLink(linkInfo.value.codeNo)
|
await refundLink(codeNo)
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
const nowISO = new Date().toISOString()
|
const nowISO = new Date().toISOString()
|
||||||
linkInfo.value = {
|
linkInfo.value = {
|
||||||
@@ -569,6 +665,15 @@ export default {
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 代码示例样式 */
|
||||||
|
.help-list code.mono {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
/* 退单确认对话框样式 */
|
/* 退单确认对话框样式 */
|
||||||
.refund-confirm {
|
.refund-confirm {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user