feat: 新增退单操作接口及相关逻辑

主要修改:
1. 在LinkController中新增退单操作接口,支持用户对指定链接进行退单。
2. 在LinkStatusService中实现退单逻辑,确保用户只能退单自己的链接,并更新链接状态。
3. 在ScriptClient中新增调用退单接口的方法,处理与外部系统的交互。

技术细节:
- 通过新增的退单功能,提升了用户对链接的管理能力,确保操作的安全性和有效性。
This commit is contained in:
zyh
2025-08-27 19:07:37 +08:00
parent 1377c25847
commit 02c64b3a38
6 changed files with 543 additions and 1 deletions

View File

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

View File

@@ -253,6 +253,49 @@ public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
});
}
@PostMapping("/{codeNo}/refund")
@Operation(summary = "退单操作", description = "对指定链接进行退单操作,会调用外部退单接口并更新数据库状态")
public Mono<Boolean> 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<BatchDeleteResponse> batchDeleteLinks(@RequestBody BatchDeleteRequest request, Authentication authentication) {

View File

@@ -359,6 +359,29 @@ public class ScriptClient {
log.warn("获取设备状态失败: 设备={}, 错误={}", machineId, e.toString());
});
}
/**
* 调用退单接口
* @param machineId 真实设备编号 (如 f1, ss9)
* @return 退单操作结果
*/
public Mono<String> 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());
});
}
}

View File

@@ -145,6 +145,93 @@ public class LinkStatusService {
.onErrorReturn(false);
}
/**
* 退单操作
* @param codeNo 链接编号
* @param agentId 代理ID确保用户只能退单自己的链接
* @return 退单结果
*/
@Transactional
public Mono<Boolean> 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;
}
}
/**
* 删除链接(确保用户只能删除自己的链接)
*/

View File

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

96
test_refund_order.http Normal file
View File

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