更新上号系统需求文档,添加版本信息、项目概述、角色与权限、术语与规则及业务流程等内容,同时修改链接生成规则和请求拦截器以支持公开API的身份验证,新增路由配置以支持上号任务页面。
This commit is contained in:
299
docs/需求文档.md
299
docs/需求文档.md
@@ -1,21 +1,288 @@
|
||||
明白✅
|
||||
# 上号系统需求文档(PRD/SRS)v0.9
|
||||
|
||||
1. 二界面 URL 模板
|
||||
我已把配置改成:**`域名/{codeNo}`**(文档已更新为 `https://你的域名/{codeNo}`,按运行环境替换域名即可)。
|
||||
> 基于《安装步骤.docx》与用户确认的补充说明整理。本文作为首版“可评审草案”,确认后进入详细设计与开发。
|
||||
|
||||
2. 关于 `http://36.138.184.60:12345/编号/二维码.png`
|
||||
这个是“**脚本端**”暴露出来的**静态图片**地址——**正常情况下会直接返回 PNG 图片**。如果你在页面里看不到图,常见原因有:
|
||||
---
|
||||
|
||||
## 0. 版本信息
|
||||
|
||||
* 文档版本:v0.9(草案)
|
||||
* 更新时间:2025-08-23
|
||||
* 负责人:待定
|
||||
* 评审人:待定
|
||||
* 目标里程碑:首版上线(TBD)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
提供“上号任务”的一站式系统,生成带签名与加密的用户链接,用户进入后进行选区、扫码上号、实时进度与二界面跳转;面向商家(管理员、代理商)提供链接批量生成与额度(点数)管理、退单与公告等功能。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
* 终端用户侧:通过单个加密链接完成选区 → 扫码上号 → 进入二界面。
|
||||
* 商家侧:
|
||||
|
||||
* 管理员:创建代理商账号、为代理商添加点数(余额)、批量生成链接、退单、公告管理、数据导出。
|
||||
* 代理商:查看自身额度、批量生成链接、退单、数据导出、查看公告。
|
||||
|
||||
### 1.3 不在范围(本期)
|
||||
|
||||
* 对账、统计、风控(均不做)。
|
||||
* 富文本公告、多语言、公告定时上线下线。
|
||||
* 数据导入(仅导出)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 角色与权限
|
||||
|
||||
* **终端用户**:仅访问加密链接完成流程。
|
||||
* **管理员**:代理商管理(创建/启停/加点)、批量生成链接、退单、公告发布、导出。
|
||||
* **代理商**:批量生成链接(前提是额度充足)、退单、导出、查看公告。
|
||||
|
||||
### 2.1 权限矩阵(简表)
|
||||
|
||||
| 功能 | 终端用户 | 代理商 | 管理员 |
|
||||
| ---------- | ---- | ------- | ------ |
|
||||
| 访问加密链接进行上号 | ✔️ | — | — |
|
||||
| 批量生成链接 | — | ✔️ | ✔️ |
|
||||
| 查看/导出链接 | — | ✔️(仅自己) | ✔️(全量) |
|
||||
| 退单 | — | ✔️(仅自己) | ✔️(全量) |
|
||||
| 公告查看 | — | ✔️ | ✔️ |
|
||||
| 公告发布 | — | — | ✔️ |
|
||||
| 代理商账号管理/加点 | — | — | ✔️ |
|
||||
|
||||
---
|
||||
|
||||
## 3. 术语与规则
|
||||
|
||||
* **链接(Link)**:一次任务的入口地址,带签名与加密;默认有效期 **24 小时**(可配)。
|
||||
* **编号(CodeNo)**:由后端自动生成,**全局唯一**、可追踪;用于日志与二界面模板替换。
|
||||
* **数量(Quantity)**:一次执行产生的奖励数,例如 50。
|
||||
* **次数(Times)**:重复执行次数,例如 20。
|
||||
* **总点数(TotalPoints)**:`Times × Quantity`,例如 20×50=1000。
|
||||
* **批量生成**:同一组设置一次生成 N 个链接(默认 10 个,可配)。
|
||||
* **二界面**:外部提供的页面或资源,系统在上号成功后 **用编号动态替换**后跳转。
|
||||
* **脚本端**:运行在手机上的脚本与相关资源(例如二维码图片、进度截图、上号判断等),由系统通过适配器服务进行对接。
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务流程
|
||||
|
||||
### 4.1 链接生命周期(简述)
|
||||
|
||||
1. 商家侧生成链接:校验额度 → 扣减点数(`Times×Quantity`)→ 生成编号 → 生成**加密+签名**链接 → 设置过期时间(24h)。
|
||||
2. 终端用户访问链接:若未过期,进入“选区→扫码上号”流程;成功则跳转二界面;若退单或过期则提示不可用。
|
||||
3. 退单:释放机器(不回滚额度),记录日志。
|
||||
|
||||
### 4.2 终端用户流程(含二次识别/刷新约束)
|
||||
|
||||
* **首次进入**:
|
||||
|
||||
1. 选区(安卓QQ/安卓WX)。
|
||||
2. 触发脚本端生成二维码;前端按返回的 `qrCreatedAt/qrExpireAt` 本地计算倒计时并轮询上号状态。
|
||||
3. 上号成功 → 跳转二界面(按模板替换编号)。
|
||||
* **未进入二界面即退出/刷新/返回**:
|
||||
|
||||
* 规则:**必须先调用“刷新”接口** → 等待 **Y 秒(默认 10s,可配)** → 才允许再次选区与扫码。
|
||||
|
||||
### 4.3 退单流程
|
||||
|
||||
* 商家提交编号 → 系统释放关联机器(若有)→ 标记退单 → 记录日志;额度**不回滚**。
|
||||
|
||||
### 4.4 扣费时机与计算
|
||||
|
||||
* **生成链接时即扣费**:`扣减点数 = Times × Quantity × 批量个数`。
|
||||
* 额度不足:拒绝生成。
|
||||
|
||||
### 4.5 公告
|
||||
|
||||
* 简单文本公告(可配置是否展示、展示位置),无定时/富文本/多语言。
|
||||
|
||||
### 4.6 用户端(扫码登录)规范(单链接模式 · 定稿)
|
||||
|
||||
> 本节**仅**描述用户访问与后端处理逻辑;不涉及商家功能。**一切界面都在同一 URL**:`https://yourdomain.com/play?code={codeNo}`。前端通过该 URL 内的 `code` 调用后端查询状态并渲染对应页面(首界面/扫码界面/二界面)。
|
||||
|
||||
#### 4.6.1 URL 与安全
|
||||
|
||||
* 入口:`GET https://yourdomain.com/play?code={opaque}`,`code` 为**加密+签名**的**不透明**令牌(包含 linkId/iat/exp 等),不可被推断。
|
||||
* 后端在**每次**接口调用时校验:解密→签名→有效期(默认 24h)→状态;失败返回 401/403/410(过期)。
|
||||
|
||||
#### 4.6.2 状态模型(后端对前端暴露)
|
||||
|
||||
* `NEW`:未进入扫码流程(未选区)。
|
||||
* `USING`:已进入扫码流程(已选区,二维码有效期内)。
|
||||
* `LOGGED_IN`:已上号(展示二界面)。
|
||||
* `REFUNDED`:已退单(不可用)。
|
||||
* `EXPIRED`:已过期(24h)。
|
||||
* `needRefresh: boolean`:是否必须先刷新→等待 10 秒→再允许选区与扫码(只要进入过扫码但未到二界面时生效)。
|
||||
|
||||
#### 4.6.3 用户端 API(仅用户用)
|
||||
|
||||
> 路径示例以 `/api/link/*` 说明;实际以最终路由为准。
|
||||
|
||||
1. **获取状态**
|
||||
`GET /api/link/status?code={code}` →
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "NEW | USING | LOGGED_IN | REFUNDED | EXPIRED",
|
||||
"needRefresh": false,
|
||||
"region": "Q | V | null",
|
||||
"qr": { "url": "http://ip:12345/{编号}/二维码.png", "createdAt": 1730000000000, "expireAt": 1730000060000 },
|
||||
"view": "FIRST | SCAN | SECOND",
|
||||
"assets": {
|
||||
"base": "http://ip:12345/{编号}/",
|
||||
"firstHome": "首次主页.png",
|
||||
"firstBonus": "首次赏金.png",
|
||||
"midBonus": "中途赏金.png",
|
||||
"endBonus": "结束赏金.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* 说明:
|
||||
|
||||
* `qr` 仅在 `status=USING` 时返回;若脚本端无 TTL,后端以默认 **60s** 回填。
|
||||
* `view` 是前端渲染建议:`FIRST(选区) / SCAN(扫码) / SECOND(二界面)`;实际以 `status/needRefresh` 为准。
|
||||
* `assets` 为二界面所需的静态资源信息(路径中需要自动替换编号)。
|
||||
|
||||
2. **选区**
|
||||
`POST /api/link/select-region`
|
||||
Body: `{ code: string, region: "Q" | "V" }` → 返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "USING",
|
||||
"needRefresh": false,
|
||||
"qr": { "url": "http://ip:12345/{编号}/二维码.png", "createdAt": 1730000000000, "expireAt": 1730000060000 }
|
||||
}
|
||||
```
|
||||
|
||||
* 后端行为:按需分配机器、下发任务参数、请求脚本端生成二维码;将状态置为 `USING`。
|
||||
|
||||
3. **刷新**
|
||||
`POST /api/link/refresh`
|
||||
Body: `{ code: string }` → 返回:
|
||||
|
||||
```json
|
||||
{ "needRefresh": true, "waitSeconds": 10 }
|
||||
```
|
||||
|
||||
* 规则:**未到二界面前**,再次尝试必须先刷新并在前端**等待 10 秒**后才允许重新选区与扫码。
|
||||
|
||||
4. **轮询上号**
|
||||
`GET /api/link/poll-login?code={code}` →
|
||||
|
||||
* 未上号:`{ "success": false, "status": "USING" }`
|
||||
* 已上号:
|
||||
|
||||
```json
|
||||
{ "success": true, "status": "LOGGED_IN", "view": "SECOND", "assets": { "base": "http://ip:12345/{编号}/", "endBonus": "结束赏金.png" } }
|
||||
```
|
||||
|
||||
* 前端据返回改为渲染**二界面**(同一个 URL,不跳转)。
|
||||
|
||||
> 可选:`GET /api/link/qr.png?code={code}` 由后端代理二维码,确保整链路 HTTPS,避免浏览器混合内容拦截。
|
||||
|
||||
#### 4.6.4 前端渲染流程(同一路由)
|
||||
|
||||
1. **首访**:调用 `/api/link/status` 渲染:
|
||||
|
||||
* `NEW` → 显示选区页(两个按钮 Q/V)。
|
||||
* `USING` → 显示扫码页(二维码+倒计时),并开始轮询 `/poll-login`(每 1s)。
|
||||
* `LOGGED_IN` → 显示二界面(依 `assets` 取图/信息)。
|
||||
* `REFUNDED/EXPIRED` → 显示不可用页。
|
||||
2. **选区**:`POST /select-region` 成功后进入扫码页,开始轮询。
|
||||
3. **二维码过期**:倒计时到 0 或轮询返回需刷新 → 显示“请刷新”按钮;调用 `/refresh` 并禁用按钮 **10s**,倒计时结束后恢复“选区”按钮。
|
||||
4. **上号成功**:轮询返回 `LOGGED_IN` → 在**同一 URL** 切换到二界面视图;前端可停止所有轮询与倒计时。
|
||||
|
||||
#### 4.6.5 后端内部处理要点
|
||||
|
||||
* **校验**:对所有接口按 `code` 做鉴权(签名/解密/过期/撤销/退单)。
|
||||
* **状态流转**:
|
||||
|
||||
* `NEW → USING`:选区成功并生成二维码。
|
||||
* `USING → LOGGED_IN`:脚本端返回“已上号”。
|
||||
* `NEW/USING → REFUNDED | EXPIRED`:退单或超时达 24h。
|
||||
* **刷新约束**:对进入过扫码但未上号的会话,标记 `needRefresh=true`;`/refresh` 后写入“刷新时间”,后续 **10s** 内拒绝 `select-region`。
|
||||
* **脚本端适配**:分配/释放机器、生成二维码、查询“是否上号”、可选的分数读取;超时重试与错误兜底(不把脚本端错误直接暴露给前端)。
|
||||
* **幂等**:`select-region`/`refresh`/`poll-login` 按 `code` 幂等处理,避免重复侧效应。
|
||||
* **日志**:记录编号、选区、二维码下发/失效、上号结果、刷新与错误(保留 90 天)。
|
||||
|
||||
#### 4.6.6 错误与前端文案(示例)
|
||||
|
||||
* 401/403:链接无效或已篡改 → “链接无效,请联系商家”。
|
||||
* 410:链接已过期 → “链接已过期,请联系商家重新获取”。
|
||||
* 423:需刷新等待 → “请先刷新并等待 10 秒后再试”。
|
||||
* 504:脚本端超时 → “当前排队人数较多,请稍后重试”。
|
||||
|
||||
> 以上约束确保“同一链接承载首界面/扫码/二界面”的产品体验,前端仅根据后端状态渲染视图,无需路由跳转。
|
||||
|
||||
|
||||
|
||||
次数链接
|
||||
http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=总次数&编号=次数
|
||||
|
||||
后台操作流程: 后台管理员在后台输入框里面填写了一个次数5, 然后点击了生成, 平台会自动生成一个链接,格式如同:域名/加密
|
||||
|
||||
批量生成可以搞成下载文件或者复制粘贴都可以
|
||||
|
||||
后台功能:
|
||||
1.后台可以直接单生成链接或者批量生成链接给用户使用如图
|
||||
2.代理系统, 代理也可单独生成链接或批量生成, 后台可帮他设置金额, 代理使用多少,自动扣除多少
|
||||
3.退单: 后台只需填写正确的编号运行退单链接,即可直接退单
|
||||
4.用户选区之后等待几秒 出现二维码到期时间 刷新时间 用户使用有效期 设置
|
||||
5.次数等于数量 设置
|
||||
6.可以设置公告弹窗, 代理也可以自己生成自己的, 不会互通! 只有确定和取消按钮,点确定会跳转某网页
|
||||
|
||||
退单链接()
|
||||
http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断退单&编号=T
|
||||
|
||||
第一步;用户界面 1和2
|
||||
2
|
||||
判断空的设备链接
|
||||
http://36.138.184.60:1234/yijianwan_netfile/readAllMsg?文件名=判断分数
|
||||
|
||||
选区链接
|
||||
http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断系统&编号=Q或V
|
||||
|
||||
刷新链接
|
||||
http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断刷新&编号=刷新
|
||||
|
||||
二维码的http://36.138.184.60:12345/编号/二维码.png
|
||||
|
||||
判断是否上号
|
||||
http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=编号 状态: 未上号 已上号 【已上号跳转第二个界面】
|
||||
|
||||
讲解:后台生成了一个链接,填写了次数, 用户拿到链接之后,在浏览器运行, 后台会自动获取一个空的编号, 然后用户可以自己选QQ区或微信区, 如果用户选择QQ区,那么直接执行选区链接执行QQ的, 微信也一样, 根据后台设置的等待,几秒后获取二维码链接 如同第三张图, 点击之后你可以观看第三张图,有一个倒数时间,也可在后台设置时间, 如果用户在我设置的60秒内没扫码,自动会跳出扫码超时,如第二张图, 用户可以继续选区, 后台会首先执行刷新链接, 然后根据后台设置的刷新时间,等待多少秒后再执行选择QQ区或微信区的链接, 根据用户选择, 作者可以写一个日志或者其他把把这个些链接的使用流程记录下来,方便第二次识别,
|
||||
|
||||
假如:用户他第一次已经选过了区,但是他直接第二次刷新了浏览器, 导致没办法确认我后台对接的脚本是否在首页, 所以作者必须写个记录, 来判断这个链接是否被使用过一次,如果被使用过一次, 那么用户不管是刷新浏览器,还是等待超时扫码, 都判定为第二次! 所以必须先执行刷新根据后台设计的时间等待 再选区!
|
||||
|
||||
用户只要选区了,出现了二维码,那么后台只需要一直识别判断它是否上号那条链接,如果出现已上号,那么直接跳转第二个界面,未上号就一直在第一个界面!
|
||||
|
||||
|
||||
|
||||
用户扫码后跳转的:第二个界面
|
||||
|
||||
|
||||
|
||||
②http://36.138.184.60:12345/编号/首次主页.png
|
||||
③http://36.138.184.60:12345/编号/首次赏金.png
|
||||
④http://36.138.184.60:12345/编号/中途赏金.png
|
||||
⑤http://36.138.184.60:12345/编号/结束赏金.png
|
||||
|
||||
讲解:
|
||||
1.选择大厅这个就是用户自己选的区域显示
|
||||
2.状态=已打完 空闲, 只要显示这两个状态,余地都是已打完,其他都是代练中
|
||||
3.目标点数0/1000如图1000就是后台设置的次数x次数等于数量=1000 [假如后台设置的次数等于总量为50,那么必须设置20次才等于1000
|
||||
|
||||
使用过的所有链接总有效期为24小时 可设置
|
||||
|
||||
目标点数
|
||||
http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断分数&对象名=编号
|
||||
|
||||
* **混合内容被拦截**:你的前端如果是 **HTTPS**,而这张图是 **HTTP**,浏览器会拦截(安全策略)。
|
||||
解决:给 `36.138.184.60:12345` 配 **HTTPS**,或让后端提供一个 **HTTPS 代理端点**(例如:`GET /api/link/{token}/qr.png`)去转发这张图片,前端只请求自己的 HTTPS 域名即可。(我已在文档里加了这条注意事项)
|
||||
* **二维码尚未生成 / 已过期**:脚本端还没把 `二维码.png` 写到对应 `编号/` 目录,或已被轮转清理 → 会 404 或显示旧图。
|
||||
解决:确保先触发“生成二维码”动作,并用返回的 `createdAt/expireAt` 做本地倒计时;必要时加 `?t=时间戳` 防缓存。
|
||||
* **编号不匹配**:前端用的 `编号` 与实际分配的机器编号不同。
|
||||
解决:以后端返回的 `machineId/编号` 为准。
|
||||
* **端口/防火墙**:12345 端口未对外放通或临时不可达。
|
||||
解决:主机/网关放行该端口并做健康检查。
|
||||
* **跨域非问题**:`<img>` 加载图片不受 CORS 限制,但若被混合内容拦截,同样会显示不出。
|
||||
|
||||
如果你愿意最省心的做法:我们在后端新增一个**图片代理接口**(HTTPS),前端统一拿这个接口的 URL,当脚本端换 IP/端口或做 HTTPS,这边都不用改前端。
|
||||
|
||||
需要的话我可以顺手把这个代理端点的 Spring Boot 代码骨架也给你(带缓存/超时/错误降级)。
|
||||
|
||||
47
src/api/play.js
Normal file
47
src/api/play.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import http from '@/plugins/http'
|
||||
|
||||
/**
|
||||
* 用户端游戏页面 API
|
||||
*/
|
||||
|
||||
// 获取链接状态
|
||||
export function getLinkStatus(code) {
|
||||
return http.get('/api/link/status', {
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
|
||||
// 选择区域
|
||||
export function selectRegion(payload) {
|
||||
// payload: { code: string, region: string }
|
||||
return http.post('/api/link/select-region', payload)
|
||||
}
|
||||
|
||||
// 刷新链接
|
||||
export function refreshLink(code) {
|
||||
return http.post('/api/link/refresh', {
|
||||
code
|
||||
})
|
||||
}
|
||||
|
||||
// 轮询登录状态
|
||||
export function pollLoginStatus(code) {
|
||||
return http.get('/api/link/poll-login', {
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取游戏进度
|
||||
export function getGameProgress(code) {
|
||||
return http.get('/api/link/progress', {
|
||||
params: { code }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取二维码图片(可选,用于代理二维码避免混合内容问题)
|
||||
export function getQRCode(code) {
|
||||
return http.get('/api/link/qr.png', {
|
||||
params: { code },
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// 链接地址生成规则
|
||||
export const LINK_CONFIG = {
|
||||
// 基础域名
|
||||
BASE_URL: 'https://yourdomain.com',
|
||||
BASE_URL: 'http://localhost:5173',
|
||||
|
||||
// 游戏页面路径
|
||||
GAME_PATH: '/play',
|
||||
|
||||
@@ -27,12 +27,23 @@ function onRefreshed(newToken, tokenType) {
|
||||
// 请求拦截:附加 Authorization
|
||||
http.interceptors.request.use(
|
||||
(config) => {
|
||||
// 跳过公开API的身份验证(只跳过用户端游戏相关的公开接口)
|
||||
const publicAPIs = [
|
||||
'/api/link/status',
|
||||
'/api/link/select-region',
|
||||
'/api/link/poll-login',
|
||||
'/api/link/progress'
|
||||
]
|
||||
const isPublicAPI = publicAPIs.some(api => config.url?.includes(api))
|
||||
|
||||
if (!isPublicAPI) {
|
||||
const token = getAccessToken()
|
||||
const type = getTokenType()
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers.Authorization = `${type} ${token}`
|
||||
}
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
@@ -58,10 +69,17 @@ http.interceptors.response.use(
|
||||
const status = response?.status
|
||||
const url = config?.url || ''
|
||||
|
||||
// 避免对登录/刷新自身进行重复刷新
|
||||
// 避免对登录/刷新自身和公开API进行重复刷新
|
||||
const isAuthPath = /\/auth\/(login|refresh)/.test(url || '')
|
||||
const publicAPIs = [
|
||||
'/api/link/status',
|
||||
'/api/link/select-region',
|
||||
'/api/link/poll-login',
|
||||
'/api/link/progress'
|
||||
]
|
||||
const isPublicAPI = publicAPIs.some(api => url?.includes(api))
|
||||
|
||||
if (status === 401 && !isAuthPath) {
|
||||
if (status === 401 && !isAuthPath && !isPublicAPI) {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
|
||||
@@ -14,10 +14,12 @@ const Settings = () => import('@/views/settings/Settings.vue')
|
||||
const LinkGenerate = () => import('@/views/links/LinkGenerate.vue')
|
||||
const ErrorTest = () => import('@/views/ErrorTest.vue')
|
||||
const PermissionTest = () => import('@/views/PermissionTest.vue')
|
||||
const Play = () => import('@/views/Play.vue')
|
||||
const NotFound = () => import('@/views/NotFound.vue')
|
||||
|
||||
export const routes = [
|
||||
{ path: '/login', name: 'Login', component: Login, meta: { public: true, title: '登录' } },
|
||||
{ path: '/play', name: 'Play', component: Play, meta: { public: true, title: '上号任务' } },
|
||||
{
|
||||
path: '/',
|
||||
component: AdminLayout,
|
||||
|
||||
1171
src/views/Play.vue
Normal file
1171
src/views/Play.vue
Normal file
File diff suppressed because it is too large
Load Diff
141
test-play.html
Normal file
141
test-play.html
Normal file
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>测试用户端页面</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.test-link {
|
||||
display: block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.test-link:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
.description {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.note {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.features li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.features li:before {
|
||||
content: "✓";
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h1>🎮 用户端页面测试</h1>
|
||||
|
||||
<div class="description">
|
||||
<h3>页面功能说明:</h3>
|
||||
<ul class="features">
|
||||
<li>支持链接状态识别(NEW/USING/LOGGED_IN/REFUNDED/EXPIRED)</li>
|
||||
<li>选区界面:QQ区/微信区选择</li>
|
||||
<li>扫码界面:二维码显示、倒计时、轮询上号状态</li>
|
||||
<li>二界面:游戏进度显示、状态监控</li>
|
||||
<li>错误处理:链接无效/过期/退单等状态处理</li>
|
||||
<li>刷新机制:防止重复操作的冷却时间</li>
|
||||
<li>响应式设计:支持移动端和桌面端</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>测试链接:</h3>
|
||||
|
||||
<a href="http://localhost:5173/play?code=66L8NM3L" class="test-link" target="_blank">
|
||||
🔗 测试链接 1 - code=66L8NM3L
|
||||
</a>
|
||||
|
||||
<a href="http://localhost:5173/play?code=SAMPLE123" class="test-link" target="_blank">
|
||||
🔗 测试链接 2 - code=SAMPLE123
|
||||
</a>
|
||||
|
||||
<a href="http://localhost:5173/play?code=INVALID" class="test-link" target="_blank">
|
||||
🔗 测试链接 3 - code=INVALID (测试错误处理)
|
||||
</a>
|
||||
|
||||
<div class="description">
|
||||
<h3>测试步骤:</h3>
|
||||
<ol>
|
||||
<li>确保开发服务器运行在 localhost:5173</li>
|
||||
<li>点击上面的测试链接</li>
|
||||
<li>观察页面状态切换和UI表现</li>
|
||||
<li>测试选区功能(需要后端API支持)</li>
|
||||
<li>测试刷新和错误处理</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<h3>API端点说明:</h3>
|
||||
<ul>
|
||||
<li><code>GET /api/link/status?code={code}</code> - 获取链接状态</li>
|
||||
<li><code>POST /api/link/select-region</code> - 选择区域</li>
|
||||
<li><code>POST /api/link/refresh</code> - 刷新链接</li>
|
||||
<li><code>GET /api/link/poll-login?code={code}</code> - 轮询登录状态</li>
|
||||
<li><code>GET /api/link/progress?code={code}</code> - 获取游戏进度</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p class="note">
|
||||
⚠️ 注意:测试需要后端API支持,如果API未实现,页面会显示相应的错误信息。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 检查开发服务器状态
|
||||
fetch('http://localhost:5173/')
|
||||
.then(() => {
|
||||
console.log('✅ 开发服务器运行正常');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('❌ 开发服务器未运行,请先启动: npm run dev');
|
||||
document.querySelector('.note').innerHTML =
|
||||
'❌ 开发服务器未运行,请先在项目目录运行: <code>npm run dev</code>';
|
||||
document.querySelector('.note').style.color = '#dc3545';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user