From 02c64b3a383f1ce8b289400babcf5b1b8a4292da Mon Sep 17 00:00:00 2001 From: zyh Date: Wed, 27 Aug 2025 19:07:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=80=80=E5=8D=95?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=8E=A5=E5=8F=A3=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要修改: 1. 在LinkController中新增退单操作接口,支持用户对指定链接进行退单。 2. 在LinkStatusService中实现退单逻辑,确保用户只能退单自己的链接,并更新链接状态。 3. 在ScriptClient中新增调用退单接口的方法,处理与外部系统的交互。 技术细节: - 通过新增的退单功能,提升了用户对链接的管理能力,确保操作的安全性和有效性。 --- docs/退单接口使用说明.md | 293 ++++++++++++++++++ .../controller/link/LinkController.java | 43 +++ .../server/service/external/ScriptClient.java | 23 ++ .../service/link/LinkStatusService.java | 87 ++++++ src/main/resources/application.yml | 2 +- test_refund_order.http | 96 ++++++ 6 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 docs/退单接口使用说明.md create mode 100644 test_refund_order.http diff --git a/docs/退单接口使用说明.md b/docs/退单接口使用说明.md new file mode 100644 index 0000000..8395c38 --- /dev/null +++ b/docs/退单接口使用说明.md @@ -0,0 +1,293 @@ +# 退单接口使用说明 + +## 📋 功能概述 + +退单接口允许代理用户对自己创建的链接进行退单操作。执行退单时,系统会: + +1. 调用外部脚本退单接口:`http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断退单&cc2={machineId}` +2. 将链接状态更新为 `REFUNDED` +3. 记录退单时间 + +## 🔧 接口详情 + +### 基本信息 +- **请求方式**: `POST` +- **请求路径**: `/api/link/{codeNo}/refund` +- **认证方式**: JWT Bearer Token +- **返回类型**: `application/json` + +### 请求参数 + +#### 路径参数 +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| codeNo | String | 是 | 链接编号 | + +#### 请求头 +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| Authorization | String | 是 | JWT认证令牌,格式:`Bearer {token}` | + +### 响应格式 + +#### 成功响应 (HTTP 200) +```json +{ + "data": true, + "success": true, + "message": "操作成功" +} +``` + +#### 错误响应 +```json +{ + "success": false, + "message": "错误详情", + "code": "错误代码" +} +``` + +## 📝 使用示例 + +### 1. 基本退单操作 + +```bash +curl -X POST 'http://localhost:8080/api/link/ABC12345/refund' \ + -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...' \ + -H 'Content-Type: application/json' +``` + +**成功响应:** +```json +{ + "data": true, + "success": true, + "message": "退单操作成功" +} +``` + +### 2. JavaScript 示例 + +```javascript +async function refundOrder(codeNo, jwt_token) { + try { + const response = await fetch(`http://localhost:8080/api/link/${codeNo}/refund`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${jwt_token}`, + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (result.success) { + console.log('退单成功'); + return true; + } else { + console.error('退单失败:', result.message); + return false; + } + } catch (error) { + console.error('退单请求失败:', error); + return false; + } +} + +// 使用示例 +refundOrder('ABC12345', 'your_jwt_token'); +``` + +### 3. Python 示例 + +```python +import requests + +def refund_order(code_no, jwt_token): + """执行退单操作""" + url = f'http://localhost:8080/api/link/{code_no}/refund' + headers = { + 'Authorization': f'Bearer {jwt_token}', + 'Content-Type': 'application/json' + } + + try: + response = requests.post(url, headers=headers) + response.raise_for_status() + + result = response.json() + + if result.get('success'): + print('退单成功') + return True + else: + print(f'退单失败: {result.get("message")}') + return False + + except requests.exceptions.RequestException as e: + print(f'退单请求失败: {e}') + return False + +# 使用示例 +refund_order('ABC12345', 'your_jwt_token') +``` + +## 🚫 错误处理 + +### 常见错误类型 + +| 错误码 | HTTP状态 | 错误信息 | 说明 | +|-------|---------|---------|------| +| AUTH_001 | 401 | 用户未认证 | JWT令牌无效或过期 | +| LINK_001 | 400 | 链接不存在 | 指定的链接编号不存在 | +| LINK_002 | 400 | 权限不足 | 用户无权操作此链接 | +| LINK_003 | 400 | 链接已经退过单 | 不能重复退单 | +| LINK_004 | 400 | 过期链接不允许退单 | 链接已过期 | +| LINK_005 | 400 | 已完成链接不允许退单 | 游戏已完成的链接 | + +### 错误处理示例 + +```javascript +async function handleRefund(codeNo, token) { + try { + const response = await fetch(`/api/link/${codeNo}/refund`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (!result.success) { + switch (result.code) { + case 'LINK_003': + alert('此链接已经退过单了'); + break; + case 'LINK_004': + alert('过期链接无法退单'); + break; + case 'LINK_005': + alert('已完成的游戏无法退单'); + break; + default: + alert('退单失败: ' + result.message); + } + return false; + } + + alert('退单成功'); + return true; + + } catch (error) { + console.error('退单请求失败:', error); + alert('网络错误,请稍后重试'); + return false; + } +} +``` + +## 🔄 状态流转 + +### 允许退单的状态 +- `NEW` (新建) +- `USING` (使用中) +- `LOGGED_IN` (已登录) + +### 不允许退单的状态 +- `REFUNDED` (已退款) - 已经退过单 +- `EXPIRED` (已过期) - 链接已过期 +- `COMPLETED` (已完成) - 游戏已完成 + +### 状态流转图 +``` +NEW/USING/LOGGED_IN → [退单操作] → REFUNDED (最终状态) +``` + +## 📊 业务逻辑 + +### 退单流程 + +1. **身份验证** + - 验证JWT令牌有效性 + - 确认用户有权限操作该链接 + +2. **状态检查** + - 检查链接是否存在 + - 检查链接当前状态是否允许退单 + +3. **外部接口调用** + - 如果链接有关联设备,调用脚本端退单接口 + - URL格式:`http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断退单&cc2={真实设备ID}` + - 即使外部接口调用失败,仍会更新数据库状态 + +4. **数据库更新** + - 更新链接状态为 `REFUNDED` + - 记录退单时间 `refund_at` + - 更新修改时间 `updated_at` + +### 注意事项 + +1. **权限控制**: 用户只能退单自己创建的链接 +2. **状态限制**: 只有特定状态的链接才能退单 +3. **重复操作**: 已退单的链接不能再次退单 +4. **外部接口**: 即使外部接口调用失败,也会更新本地状态,避免用户重复操作 +5. **事务性**: 整个退单操作在事务中执行,确保数据一致性 + +## 🧪 测试示例 + +### 完整测试流程 + +```bash +# 1. 登录获取JWT令牌 +curl -X POST 'http://localhost:8080/api/auth/login' \ + -H 'Content-Type: application/json' \ + -d '{"username":"test_user","password":"password"}' + +# 2. 生成测试链接 +curl -X POST 'http://localhost:8080/api/link/generate' \ + -H 'Authorization: Bearer YOUR_JWT_TOKEN' \ + -H 'Content-Type: application/json' \ + -d '{"times":1,"linkCount":1}' + +# 3. 查看链接状态 (退单前) +curl -X GET 'http://localhost:8080/api/link/YOUR_CODE_NO/status' + +# 4. 执行退单操作 +curl -X POST 'http://localhost:8080/api/link/YOUR_CODE_NO/refund' \ + -H 'Authorization: Bearer YOUR_JWT_TOKEN' + +# 5. 查看链接状态 (退单后,应该是REFUNDED状态) +curl -X GET 'http://localhost:8080/api/link/YOUR_CODE_NO/status' +``` + +## 🔍 日志示例 + +### 成功退单日志 +``` +2024-01-15 14:30:00.123 INFO LinkStatusService - === 开始处理退单操作 === +2024-01-15 14:30:00.124 INFO LinkStatusService - 链接编号: ABC12345, 代理ID: 1001 +2024-01-15 14:30:00.125 INFO LinkStatusService - 链接关联设备: f1, 调用脚本端退单接口 +2024-01-15 14:30:00.234 INFO ScriptClient - 调用退单接口: 设备=f1, url=http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断退单&cc2=f1 +2024-01-15 14:30:00.567 INFO ScriptClient - 退单接口调用成功: 设备=f1, 结果=SUCCESS +2024-01-15 14:30:00.580 INFO LinkStatusService - 退单操作成功: codeNo=ABC12345, 设备=f1, 状态更新为REFUNDED +2024-01-15 14:30:00.581 INFO LinkStatusService - === 退单操作完成 === +``` + +### 失败退单日志 +``` +2024-01-15 14:30:00.123 INFO LinkStatusService - === 开始处理退单操作 === +2024-01-15 14:30:00.124 INFO LinkStatusService - 链接编号: ABC12345, 代理ID: 1001 +2024-01-15 14:30:00.125 WARN LinkStatusService - 链接已经退过单: codeNo=ABC12345, status=REFUNDED +2024-01-15 14:30:00.126 ERROR LinkStatusService - === 退单操作失败 === +2024-01-15 14:30:00.127 ERROR LinkStatusService - codeNo=ABC12345, agentId=1001, 错误详情: 链接已经退过单 +``` + +## 🔗 相关接口 + +- [链接生成接口](./API文档.md#链接生成) +- [链接状态查询接口](./链接状态接口测试.md) +- [链接列表接口](./链接列表接口测试.md) +- [JWT认证说明](./JWT认证使用说明.md) diff --git a/src/main/java/com/gameplatform/server/controller/link/LinkController.java b/src/main/java/com/gameplatform/server/controller/link/LinkController.java index 2557fbf..a062411 100644 --- a/src/main/java/com/gameplatform/server/controller/link/LinkController.java +++ b/src/main/java/com/gameplatform/server/controller/link/LinkController.java @@ -253,6 +253,49 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic }); } + @PostMapping("/{codeNo}/refund") + @Operation(summary = "退单操作", description = "对指定链接进行退单操作,会调用外部退单接口并更新数据库状态") + public Mono refundOrder(@PathVariable("codeNo") String codeNo, Authentication authentication) { + log.info("=== 开始退单操作 ==="); + log.info("链接编号: {}", codeNo); + + if (authentication == null) { + log.error("=== 认证失败:Authentication为空 ==="); + return Mono.error(new IllegalArgumentException("用户未认证:Authentication为空")); + } + + // 获取用户ID + Claims claims = (Claims) authentication.getDetails(); + if (claims == null) { + log.error("=== 认证失败:Claims为空 ==="); + log.error("Authentication details: {}", authentication.getDetails()); + return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); + } + + Long agentId = claims.get("userId", Long.class); + String userType = claims.get("userType", String.class); + + log.info("用户信息: agentId={}, userType={}", agentId, userType); + + if (agentId == null) { + log.error("=== 无法获取用户ID ==="); + return Mono.error(new IllegalArgumentException("无法获取用户ID")); + } + + return linkStatusService.refundOrder(codeNo, agentId) + .doOnSuccess(success -> { + if (success) { + log.info("退单操作成功: codeNo={}, agentId={}", codeNo, agentId); + } else { + log.warn("退单操作失败: codeNo={}, agentId={}", codeNo, agentId); + } + }) + .doOnError(error -> { + log.error("退单操作时发生错误: codeNo={}, agentId={}, error={}", + codeNo, agentId, error.getMessage(), error); + }); + } + @PostMapping("/batch-delete") @Operation(summary = "批量删除链接", description = "批量删除指定的链接,用户只能删除自己创建的链接,最多一次删除100个") public Mono batchDeleteLinks(@RequestBody BatchDeleteRequest request, Authentication authentication) { diff --git a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java index 9c069c2..4e1a6e4 100644 --- a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java +++ b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java @@ -359,6 +359,29 @@ public class ScriptClient { log.warn("获取设备状态失败: 设备={}, 错误={}", machineId, e.toString()); }); } + + /** + * 调用退单接口 + * @param machineId 真实设备编号 (如 f1, ss9) + * @return 退单操作结果 + */ + public Mono refundOrder(String machineId) { + String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断退单&cc2=%s", machineId); + log.info("调用退单接口: 设备={}, url={}", machineId, url); + + return webClient.get() + .uri(url) + .accept(MediaType.TEXT_PLAIN) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(10)) + .doOnSuccess(result -> { + log.info("退单接口调用成功: 设备={}, 结果={}", machineId, result); + }) + .doOnError(e -> { + log.error("退单接口调用失败: 设备={}, 错误={}", machineId, e.toString()); + }); + } } diff --git a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java index f11d1fa..763f68c 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -145,6 +145,93 @@ public class LinkStatusService { .onErrorReturn(false); } + /** + * 退单操作 + * @param codeNo 链接编号 + * @param agentId 代理ID(确保用户只能退单自己的链接) + * @return 退单结果 + */ + @Transactional + public Mono refundOrder(String codeNo, Long agentId) { + return Mono.fromCallable(() -> doRefundOrder(codeNo, agentId)) + .subscribeOn(Schedulers.boundedElastic()); + } + + private Boolean doRefundOrder(String codeNo, Long agentId) { + log.info("=== 开始处理退单操作 ==="); + log.info("链接编号: {}, 代理ID: {}", codeNo, agentId); + + try { + // 1. 查询链接任务 + LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); + if (linkTask == null) { + log.error("链接任务不存在: codeNo={}", codeNo); + throw new IllegalArgumentException("链接不存在"); + } + + // 2. 验证代理ID(确保用户只能退单自己的链接) + if (!linkTask.getAgentId().equals(agentId)) { + log.error("权限不足: 用户{}无权退单链接{}", agentId, codeNo); + throw new IllegalArgumentException("权限不足:您无权操作此链接"); + } + + // 3. 检查链接状态是否允许退单 + String currentStatus = linkTask.getStatus(); + if ("REFUNDED".equals(currentStatus)) { + log.warn("链接已经退过单: codeNo={}, status={}", codeNo, currentStatus); + throw new IllegalStateException("链接已经退过单"); + } + + if ("EXPIRED".equals(currentStatus)) { + log.warn("过期链接不允许退单: codeNo={}, status={}", codeNo, currentStatus); + throw new IllegalStateException("过期链接不允许退单"); + } + + if ("COMPLETED".equals(currentStatus)) { + log.warn("已完成链接不允许退单: codeNo={}, status={}", codeNo, currentStatus); + throw new IllegalStateException("已完成链接不允许退单"); + } + + // 4. 如果链接有关联的设备,调用脚本端退单接口 + String machineId = linkTask.getMachineId(); + if (machineId != null && !machineId.trim().isEmpty()) { + log.info("链接关联设备: {}, 调用脚本端退单接口", machineId); + + try { + // 同步调用脚本端退单接口 + String refundResult = scriptClient.refundOrder(machineId).block(); + log.info("脚本端退单接口调用成功: 设备={}, 结果={}", machineId, refundResult); + } catch (Exception e) { + log.error("脚本端退单接口调用失败: 设备={}, 错误={}", machineId, e.getMessage()); + // 即使脚本端调用失败,我们仍然继续更新数据库状态 + // 这样可以确保用户能够看到退单状态,避免重复退单 + } + } else { + log.info("链接未关联设备,跳过脚本端退单调用"); + } + + // 5. 更新链接状态为REFUNDED + linkTask.setStatus("REFUNDED"); + linkTask.setRefundAt(LocalDateTime.now()); + linkTask.setUpdatedAt(LocalDateTime.now()); + + int updateResult = linkTaskMapper.update(linkTask); + if (updateResult <= 0) { + log.error("更新链接状态失败: codeNo={}", codeNo); + throw new RuntimeException("更新链接状态失败"); + } + + log.info("退单操作成功: codeNo={}, 设备={}, 状态更新为REFUNDED", codeNo, machineId); + log.info("=== 退单操作完成 ==="); + return true; + + } catch (Exception e) { + log.error("=== 退单操作失败 ==="); + log.error("codeNo={}, agentId={}, 错误详情: {}", codeNo, agentId, e.getMessage(), e); + throw e; + } + } + /** * 删除链接(确保用户只能删除自己的链接) */ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index afdca9b..211fb25 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -70,7 +70,7 @@ script: # 服务器配置 app: - base-url: "http://192.140.164.137:18080" # 生产环境需要配置为实际域名 + base-url: "https://2.uzi0.cc" # 生产环境需要配置为实际域名 image-save-path: "./images" # 图片保存路径 link: diff --git a/test_refund_order.http b/test_refund_order.http new file mode 100644 index 0000000..0ac0cab --- /dev/null +++ b/test_refund_order.http @@ -0,0 +1,96 @@ +### 退单接口测试 + +# 变量定义 +@baseUrl = http://localhost:8080 +@jwt_token = YOUR_JWT_TOKEN_HERE +@test_code_no = YOUR_TEST_CODE_NO_HERE + +### 1. 登录获取JWT令牌 (先执行这个获取token) +POST {{baseUrl}}/api/auth/login +Content-Type: application/json + +{ + "username": "your_username", + "password": "your_password" +} + +### + +### 2. 生成测试链接 (获取一个codeNo用于测试) +POST {{baseUrl}}/api/link/generate +Authorization: Bearer {{jwt_token}} +Content-Type: application/json + +{ + "times": 1, + "linkCount": 1 +} + +### + +### 3. 查看链接当前状态 (退单前) +GET {{baseUrl}}/api/link/{{test_code_no}}/status +Accept: application/json + +### + +### 4. 执行退单操作 - 正常情况 +POST {{baseUrl}}/api/link/{{test_code_no}}/refund +Authorization: Bearer {{jwt_token}} +Content-Type: application/json + +### + +### 5. 查看链接退单后状态 (应该是REFUNDED状态) +GET {{baseUrl}}/api/link/{{test_code_no}}/status +Accept: application/json + +### + +### 6. 重复退单测试 (应该返回错误: 链接已经退过单) +POST {{baseUrl}}/api/link/{{test_code_no}}/refund +Authorization: Bearer {{jwt_token}} +Content-Type: application/json + +### + +### 7. 测试不存在的链接退单 (应该返回错误: 链接不存在) +POST {{baseUrl}}/api/link/NOTEXIST123/refund +Authorization: Bearer {{jwt_token}} +Content-Type: application/json + +### + +### 8. 测试无认证退单 (应该返回401) +POST {{baseUrl}}/api/link/{{test_code_no}}/refund +Content-Type: application/json + +### + +### 9. 测试错误的JWT令牌 (应该返回401) +POST {{baseUrl}}/api/link/{{test_code_no}}/refund +Authorization: Bearer invalid_jwt_token +Content-Type: application/json + +### + +### 10. 查看链接列表 (验证退单状态) +GET {{baseUrl}}/api/link/list?page=1&pageSize=10&status=REFUNDED +Authorization: Bearer {{jwt_token}} +Accept: application/json + +### + +### 测试步骤说明: +# 1. 先执行登录接口,获取JWT令牌 +# 2. 将获取到的JWT令牌复制到上面的 @jwt_token 变量中 +# 3. 生成一个测试链接,获取 codeNo +# 4. 将获取到的 codeNo 复制到上面的 @test_code_no 变量中 +# 5. 依次执行后续的测试接口 +# +# 预期结果: +# - 正常退单:返回 true +# - 重复退单:返回错误信息 "链接已经退过单" +# - 不存在链接:返回错误信息 "链接不存在" +# - 无认证访问:返回 401 状态码 +# - 退单后链接状态变为 REFUNDED