feat: 新增按状态批量删除链接功能

主要修改:
1. 在LinkController中新增按状态批量删除链接的接口,允许用户根据指定状态批量删除自己创建的链接。
2. 在LinkStatusService中实现批量删除逻辑,确保用户只能删除自己的链接,并进行状态验证。
3. 更新LinkTaskMapper和对应的XML文件,增加查询和删除链接任务的相关方法。

技术细节:
- 通过新增的批量删除功能,提升了用户对链接的管理能力,确保操作的安全性和有效性,同时优化了数据库操作的灵活性。
This commit is contained in:
zyh
2025-08-28 12:41:44 +08:00
parent 080c55059a
commit 0801394999
7 changed files with 455 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
# 按狀態批量刪除鏈接接口說明
## 功能概述
新增了一個按鏈接狀態批量刪除鏈接的接口,允許用戶根據指定的狀態列表批量刪除自己的鏈接。
## 接口詳情
### 請求路徑
`POST /api/link/batch-delete-by-status`
### 請求參數
```json
{
"statusList": ["EXPIRED", "REFUNDED"],
"confirmDelete": true
}
```
#### 參數說明
- `statusList`: 要刪除的鏈接狀態列表,支持的狀態值:
- `NEW`: 新建
- `USING`: 使用中
- `LOGGED_IN`: 已登錄
- `COMPLETED`: 已完成
- `REFUNDED`: 已退款
- `EXPIRED`: 已過期
- `confirmDelete`: 是否確認刪除操作,必須設置為 `true` 才能執行刪除
#### 驗證規則
- 狀態列表不能為空
- 單次最多指定10個狀態
- 必須確認刪除操作(`confirmDelete = true`
- 只能刪除用戶自己創建的鏈接
### 響應格式
```json
{
"successCount": 15,
"failedCount": 0,
"totalCount": 15,
"successCodeNos": ["ABC123", "DEF456", "..."],
"failedCodeNos": [],
"failedReasons": [],
"allSuccess": true
}
```
#### 響應字段說明
- `successCount`: 成功刪除的數量
- `failedCount`: 刪除失敗的數量
- `totalCount`: 總數量
- `successCodeNos`: 成功刪除的鏈接編號列表
- `failedCodeNos`: 刪除失敗的鏈接編號列表
- `failedReasons`: 刪除失敗的原因列表
- `allSuccess`: 是否全部成功
## 實現細節
### 新增文件
1. `BatchDeleteByStatusRequest.java` - 請求DTO
2. `test_batch_delete_by_status.http` - 接口測試文件
3. `docs/按狀態批量刪除鏈接接口說明.md` - 接口說明文檔
### 修改文件
1. `LinkTaskMapper.java` - 添加數據庫查詢方法
2. `LinkTaskMapper.xml` - 添加SQL映射
3. `LinkStatusService.java` - 添加業務邏輯方法
4. `LinkController.java` - 添加控制器接口
### 數據庫操作
- `findByStatusListAndAgentId`: 查詢指定狀態和用戶的鏈接列表
- `countByStatusListAndAgentId`: 統計指定狀態和用戶的鏈接數量
- `batchDeleteByStatusListAndAgentId`: 批量刪除指定狀態和用戶的鏈接
## 安全考慮
### 權限控制
- 用戶只能刪除自己創建的鏈接
- 通過JWT token進行用戶身份驗證
- 通過 `agentId` 過濾確保數據隔離
### 操作確認
- 必須設置 `confirmDelete = true` 才能執行刪除操作
- 避免誤操作導致的數據丟失
### 參數驗證
- 驗證狀態值的有效性
- 限制單次操作的狀態數量最多10個
- 參數非空驗證
## 使用示例
### 1. 刪除過期鏈接
```http
POST /api/link/batch-delete-by-status
Authorization: Bearer {token}
Content-Type: application/json
{
"statusList": ["EXPIRED"],
"confirmDelete": true
}
```
### 2. 清理已退款和已過期的鏈接
```http
POST /api/link/batch-delete-by-status
Authorization: Bearer {token}
Content-Type: application/json
{
"statusList": ["EXPIRED", "REFUNDED"],
"confirmDelete": true
}
```
## 錯誤處理
### 常見錯誤
- `用戶未認證` - JWT token無效或過期
- `必須確認刪除操作` - confirmDelete不為true
- `要删除的状态列表不能为空` - statusList為空
- `无效的状态值` - 狀態值不在允許範圍內
- `单次最多只能指定10个状态` - 狀態數量超過限制
### 響應狀態碼
- `200` - 操作成功
- `400` - 請求參數錯誤
- `401` - 用戶未認證
- `500` - 服務器內部錯誤
## 日誌記錄
系統會記錄以下關鍵信息:
- 刪除操作的發起用戶
- 指定的狀態列表
- 實際刪除的鏈接數量
- 操作結果(成功/失敗)
- 失敗原因(如有)
這些日誌有助於問題排查和操作審計。

View File

@@ -1,5 +1,6 @@
package com.gameplatform.server.controller.link; package com.gameplatform.server.controller.link;
import com.gameplatform.server.model.dto.link.BatchDeleteByStatusRequest;
import com.gameplatform.server.model.dto.link.BatchDeleteRequest; import com.gameplatform.server.model.dto.link.BatchDeleteRequest;
import com.gameplatform.server.model.dto.link.BatchDeleteResponse; import com.gameplatform.server.model.dto.link.BatchDeleteResponse;
import com.gameplatform.server.model.dto.link.LinkGenerateRequest; import com.gameplatform.server.model.dto.link.LinkGenerateRequest;
@@ -296,6 +297,58 @@ public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
}); });
} }
@PostMapping("/batch-delete-by-status")
@Operation(summary = "按状态批量删除链接", description = "根据指定的状态批量删除链接用户只能删除自己创建的链接支持删除的状态NEW、USING、LOGGED_IN、COMPLETED、REFUNDED、EXPIRED")
public Mono<BatchDeleteResponse> batchDeleteLinksByStatus(@Valid @RequestBody BatchDeleteByStatusRequest request, Authentication authentication) {
log.info("=== 开始按状态批量删除链接 ===");
log.info("要删除的状态列表: {}", request.getStatusList());
log.info("是否确认删除: {}", request.getConfirmDelete());
if (authentication == null) {
log.error("=== 认证失败Authentication为空 ===");
return Mono.error(new IllegalArgumentException("用户未认证Authentication为空"));
}
// 检查是否确认删除
if (!Boolean.TRUE.equals(request.getConfirmDelete())) {
log.error("=== 未确认删除操作 ===");
return Mono.error(new IllegalArgumentException("必须确认删除操作请设置confirmDelete为true"));
}
// 获取用户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.batchDeleteLinksByStatus(request.getStatusList(), agentId)
.doOnSuccess(response -> {
log.info("按状态批量删除链接完成: 总数={}, 成功={}, 失败={}, agentId={}",
response.getTotalCount(), response.getSuccessCount(),
response.getFailedCount(), agentId);
if (!response.isAllSuccess()) {
log.warn("部分链接删除失败: 失败的链接={}, 失败原因={}",
response.getFailedCodeNos(), response.getFailedReasons());
}
})
.doOnError(error -> {
log.error("按状态批量删除链接时发生错误: agentId={}, statusList={}, error={}",
agentId, request.getStatusList(), error.getMessage(), error);
});
}
@PostMapping("/batch-delete") @PostMapping("/batch-delete")
@Operation(summary = "批量删除链接", description = "批量删除指定的链接用户只能删除自己创建的链接最多一次删除100个") @Operation(summary = "批量删除链接", description = "批量删除指定的链接用户只能删除自己创建的链接最多一次删除100个")
public Mono<BatchDeleteResponse> batchDeleteLinks(@RequestBody BatchDeleteRequest request, Authentication authentication) { public Mono<BatchDeleteResponse> batchDeleteLinks(@RequestBody BatchDeleteRequest request, Authentication authentication) {

View File

@@ -95,4 +95,19 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
* 根据设备ID和状态查询链接任务 * 根据设备ID和状态查询链接任务
*/ */
List<LinkTask> findByMachineIdAndStatus(@Param("machineId") String machineId, @Param("status") String status); List<LinkTask> findByMachineIdAndStatus(@Param("machineId") String machineId, @Param("status") String status);
/**
* 根据状态列表和代理ID查询链接任务用于验证权限和获取要删除的链接
*/
List<LinkTask> findByStatusListAndAgentId(@Param("statusList") List<String> statusList, @Param("agentId") Long agentId);
/**
* 根据状态列表和代理ID统计链接任务数量
*/
long countByStatusListAndAgentId(@Param("statusList") List<String> statusList, @Param("agentId") Long agentId);
/**
* 根据状态列表和代理ID批量删除链接任务
*/
int batchDeleteByStatusListAndAgentId(@Param("statusList") List<String> statusList, @Param("agentId") Long agentId);
} }

View File

@@ -0,0 +1,48 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.util.List;
/**
* 按狀態批量刪除鏈接請求DTO
*
* @author GamePlatform
*/
@Schema(description = "按狀態批量刪除鏈接請求")
public class BatchDeleteByStatusRequest {
@NotEmpty(message = "要刪除的狀態列表不能為空")
@Size(max = 10, message = "單次最多只能指定10個狀態")
@Schema(description = "要刪除的鏈接狀態列表可選值NEW、USING、LOGGED_IN、COMPLETED、REFUNDED、EXPIRED",
example = "[\"EXPIRED\", \"REFUNDED\"]", required = true)
private List<String> statusList;
@Schema(description = "是否確認刪除必須設置為true才能執行刪除操作", example = "true", required = true)
private Boolean confirmDelete = false;
public BatchDeleteByStatusRequest() {}
public BatchDeleteByStatusRequest(List<String> statusList, Boolean confirmDelete) {
this.statusList = statusList;
this.confirmDelete = confirmDelete;
}
public List<String> getStatusList() {
return statusList;
}
public void setStatusList(List<String> statusList) {
this.statusList = statusList;
}
public Boolean getConfirmDelete() {
return confirmDelete;
}
public void setConfirmDelete(Boolean confirmDelete) {
this.confirmDelete = confirmDelete;
}
}

View File

@@ -266,6 +266,93 @@ public class LinkStatusService {
}).subscribeOn(Schedulers.boundedElastic()); }).subscribeOn(Schedulers.boundedElastic());
} }
/**
* 按状态批量删除链接(确保用户只能删除自己的链接)
*/
@Transactional(rollbackFor = Exception.class)
public Mono<BatchDeleteResponse> batchDeleteLinksByStatus(List<String> statusList, Long agentId) {
return Mono.fromCallable(() -> {
log.info("开始按状态批量删除链接: statusList={}, agentId={}", statusList, agentId);
if (statusList == null || statusList.isEmpty()) {
throw new IllegalArgumentException("要删除的状态列表不能为空");
}
if (statusList.size() > 10) {
throw new IllegalArgumentException("单次最多只能指定10个状态");
}
// 验证状态值是否有效
List<String> validStatuses = List.of("NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED");
for (String status : statusList) {
if (!validStatuses.contains(status)) {
throw new IllegalArgumentException("无效的状态值: " + status);
}
}
// 统计要删除的链接数量
long totalCount = linkTaskMapper.countByStatusListAndAgentId(statusList, agentId);
log.info("用户拥有的指定状态链接数量: {}", totalCount);
if (totalCount == 0) {
// 没有符合条件的链接
BatchDeleteResponse response = new BatchDeleteResponse(0, 0, 0,
List.of(), List.of(), List.of());
log.info("没有找到符合条件的链接");
return response;
}
// 查询要删除的链接详情(用于记录日志)
List<LinkTask> linksToDelete = linkTaskMapper.findByStatusListAndAgentId(statusList, agentId);
List<String> codeNosToDelete = linksToDelete.stream()
.map(LinkTask::getCodeNo)
.collect(Collectors.toList());
log.info("即将删除的链接: {}", codeNosToDelete);
// 执行批量删除
int deleteCount = linkTaskMapper.batchDeleteByStatusListAndAgentId(statusList, agentId);
log.info("批量删除执行结果: 预期删除数量={}, 实际删除数量={}", totalCount, deleteCount);
// 构建响应
List<String> successCodeNos = new ArrayList<>();
List<String> failedCodeNos = new ArrayList<>();
List<String> failedReasons = new ArrayList<>();
if (deleteCount == totalCount) {
// 全部删除成功
successCodeNos.addAll(codeNosToDelete);
} else if (deleteCount > 0) {
// 部分删除成功(这种情况比较少见,可能是并发操作导致)
successCodeNos.addAll(codeNosToDelete.subList(0, deleteCount));
failedCodeNos.addAll(codeNosToDelete.subList(deleteCount, codeNosToDelete.size()));
for (int i = deleteCount; i < codeNosToDelete.size(); i++) {
failedReasons.add("删除操作部分失败");
}
} else {
// 全部删除失败
failedCodeNos.addAll(codeNosToDelete);
for (int i = 0; i < codeNosToDelete.size(); i++) {
failedReasons.add("删除操作失败");
}
}
BatchDeleteResponse response = new BatchDeleteResponse(
successCodeNos.size(),
failedCodeNos.size(),
codeNosToDelete.size(),
successCodeNos,
failedCodeNos,
failedReasons
);
log.info("按状态批量删除完成: 总数={}, 成功={}, 失败={}",
response.getTotalCount(), response.getSuccessCount(), response.getFailedCount());
return response;
}).subscribeOn(Schedulers.boundedElastic());
}
/** /**
* 批量删除链接(确保用户只能删除自己的链接) * 批量删除链接(确保用户只能删除自己的链接)
*/ */

View File

@@ -218,4 +218,34 @@
FROM link_task FROM link_task
WHERE machine_id = #{machineId} AND status = #{status} WHERE machine_id = #{machineId} AND status = #{status}
</select> </select>
<select id="findByStatusListAndAgentId" resultMap="LinkTaskMap">
SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at, completed_points, completion_images
FROM link_task
WHERE agent_id = #{agentId}
AND status IN
<foreach collection="statusList" item="status" open="(" close=")" separator=",">
#{status}
</foreach>
ORDER BY created_at DESC
</select>
<select id="countByStatusListAndAgentId" resultType="long">
SELECT COUNT(1)
FROM link_task
WHERE agent_id = #{agentId}
AND status IN
<foreach collection="statusList" item="status" open="(" close=")" separator=",">
#{status}
</foreach>
</select>
<delete id="batchDeleteByStatusListAndAgentId">
DELETE FROM link_task
WHERE agent_id = #{agentId}
AND status IN
<foreach collection="statusList" item="status" open="(" close=")" separator=",">
#{status}
</foreach>
</delete>
</mapper> </mapper>

View File

@@ -0,0 +1,82 @@
### 按狀態批量刪除鏈接接口測試
# 需要先獲取JWT token
POST http://localhost:8080/api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin"
}
### 使用獲取到的token進行按狀態批量刪除
# 1. 刪除已過期的鏈接
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": ["EXPIRED"],
"confirmDelete": true
}
### 2. 刪除已退款的鏈接
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": ["REFUNDED"],
"confirmDelete": true
}
### 3. 同時刪除多種狀態的鏈接(已過期和已退款)
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": ["EXPIRED", "REFUNDED"],
"confirmDelete": true
}
### 4. 測試錯誤情況:未確認刪除操作
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": ["EXPIRED"],
"confirmDelete": false
}
### 5. 測試錯誤情況:無效的狀態值
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": ["INVALID_STATUS"],
"confirmDelete": true
}
### 6. 測試錯誤情況:空狀態列表
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": [],
"confirmDelete": true
}
### 7. 刪除新建狀態的鏈接(謹慎使用)
POST http://localhost:8080/api/link/batch-delete-by-status
Content-Type: application/json
Authorization: Bearer {{token}}
{
"statusList": ["NEW"],
"confirmDelete": true
}