提交bug修复 后台权限管理
This commit is contained in:
@@ -32,6 +32,6 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ApiResponse<Void> handleException(Exception ex) {
|
||||
log.error("Unhandled exception", ex);
|
||||
return ApiResponse.fail("服务异常:" + ex.getMessage());
|
||||
return ApiResponse.fail("服务异常: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class DeleteBrandRunService {
|
||||
ZiniaoShopMatchResultVo matchResult = normalizedShopName.isBlank()
|
||||
? ziniaoShopSwitchService.emptyMatchResult()
|
||||
: storeMatchByShopName.computeIfAbsent(normalizedShopName,
|
||||
ignored -> ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName()));
|
||||
ignored -> ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName(), false));
|
||||
item.setMatchStatus(matchResult.getMatchStatus());
|
||||
item.setMatchMessage(matchResult.getMatchMessage());
|
||||
item.setMatched(matchResult.isMatched());
|
||||
@@ -714,7 +714,7 @@ public class DeleteBrandRunService {
|
||||
unmatchedCount++;
|
||||
continue;
|
||||
}
|
||||
ZiniaoShopMatchResultVo refreshed = ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName());
|
||||
ZiniaoShopMatchResultVo refreshed = ziniaoShopSwitchService.findIndexedStoreByName(item.getShopName(), false);
|
||||
if (refreshed != null) {
|
||||
String oldStatus = item.getMatchStatus();
|
||||
boolean oldMatched = item.isMatched();
|
||||
@@ -990,6 +990,26 @@ public class DeleteBrandRunService {
|
||||
Map<String, List<DeleteBrandResultFileDto>> mergedByFile = loadMergedChunks(taskId);
|
||||
int finishedFiles = countCompletedFiles(mergedByFile);
|
||||
|
||||
if (finishedFiles < expectedFiles) {
|
||||
Map<String, String> progress = new LinkedHashMap<>();
|
||||
progress.put("finished_files", String.valueOf(finishedFiles));
|
||||
// 定时补偿只负责“再试一次 finalize”,不能把缺片任务重新续命,
|
||||
// 否则 stale-check 永远看不到超时任务。
|
||||
if (!fromCompensation) {
|
||||
progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
|
||||
}
|
||||
deleteBrandTaskCacheService.saveProgress(taskId, progress);
|
||||
|
||||
task.setSuccessFileCount(finishedFiles);
|
||||
if (!fromCompensation) {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
|
||||
log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> progress = new LinkedHashMap<>();
|
||||
progress.put("finished_files", String.valueOf(finishedFiles));
|
||||
progress.put("updated_at", String.valueOf(System.currentTimeMillis()));
|
||||
@@ -1001,11 +1021,6 @@ public class DeleteBrandRunService {
|
||||
task.setSuccessFileCount(finishedFiles);
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
|
||||
if (finishedFiles < expectedFiles) {
|
||||
log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[DeleteBrand] tryFinalizeTask calling finalizeTask -> finishedFiles: {}, expectedFiles: {}", finishedFiles, expectedFiles);
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 鍒犻櫎鍝佺墝浠诲姟鐨勮秴鏃跺洖鏀朵笌 finalize 琛ュ伩锛涘苟涓庡垹闄ゅ搧鐗屽叡鐢ㄥ悓涓€濂楀畾鏃惰皟搴︼紝瀵瑰晢鍝侀闄╋紙PRODUCT_RISK_RESOLVE锛夐暱鏃堕棿鏃犲洖浼犲仛澶辫触鏀跺熬銆?
|
||||
* 删除品牌任务的超时回收与 finalize 补偿。
|
||||
* 同时复用这套定时调度,对商品风险和匹配店铺任务做超时兜底收尾。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -66,7 +67,7 @@ public class DeleteBrandStaleTaskService {
|
||||
private void failStaleDeleteBrandTasks() {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
|
||||
|
||||
// 鍏堟寜 DB updatedAt 绮楃瓫锛岄伩鍏嶅叏琛ㄦ壂鎻?
|
||||
// 先按 DB updatedAt 粗筛,避免全表扫描。
|
||||
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
|
||||
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||
@@ -96,19 +97,17 @@ public class DeleteBrandStaleTaskService {
|
||||
}
|
||||
|
||||
LocalDateTime lastActivityTime;
|
||||
// 鍙湁褰?has_progress 涓?true 涓旇秴杩?15 鍒嗛挓娌″績璺筹紝鎵嶈涓烘槸寮傚父鍗℃
|
||||
// 只有 has_progress=true 且心跳超时,才判定为异常卡住。
|
||||
if (lastHeartbeatAt > 0 && isActive) {
|
||||
// 鏈夊績璺筹紝鐢ㄦ渶鍚庡績璺虫椂闂村姣?15 鍒嗛挓 (threshold 鏄?now - 15)
|
||||
lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
|
||||
if (lastActivityTime.isAfter(threshold)) {
|
||||
continue; // 娌¤秴鏃?
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 娌℃湁蹇冭烦锛屾垨鑰呭綋鍓嶅鍦?has_progress: false 锛堢瓑寰呯敤鎴风偣鍑讳笅涓€涓换鍔℃帹閫佺殑闈欓粯鏈燂級
|
||||
// 闈欓粯鏈熷拰鎺掗槦鏈熺殑瀹介檺鏈熶竴鏍凤紝璁句负 12 涓皬鏃?
|
||||
// 没有心跳,或仍处于静默等待阶段时,使用更长的初始宽限期。
|
||||
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
|
||||
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
|
||||
continue; // 绛夊緟涓嬩釜闃熷垪鎺ㄩ€佷腑锛屼笉瑕佹潃瀹冿紒
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +116,7 @@ public class DeleteBrandStaleTaskService {
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
|
||||
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||
.set(FileTaskEntity::getStatus, "FAILED")
|
||||
.set(FileTaskEntity::getErrorMessage, "缁撴灉鍥炰紶闀挎椂闂存棤鍝嶅簲锛屼换鍔″凡鑷姩澶辫触")
|
||||
.set(FileTaskEntity::getErrorMessage, "结果回传长时间无响应,任务已自动失败")
|
||||
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
|
||||
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
||||
|
||||
@@ -170,7 +169,7 @@ public class DeleteBrandStaleTaskService {
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_PRODUCT_RISK)
|
||||
.eq(FileTaskEntity::getStatus, "RUNNING")
|
||||
.set(FileTaskEntity::getStatus, "FAILED")
|
||||
.set(FileTaskEntity::getErrorMessage, "闀挎椂闂存湭鏀跺埌 Python 缁撴灉锛屼换鍔″凡鑷姩澶辫触")
|
||||
.set(FileTaskEntity::getErrorMessage, "长时间未收到 Python 结果回传,任务已自动失败")
|
||||
.set(FileTaskEntity::getUpdatedAt, LocalDateTime.now())
|
||||
.set(FileTaskEntity::getFinishedAt, LocalDateTime.now()));
|
||||
if (updated > 0) {
|
||||
@@ -252,7 +251,7 @@ public class DeleteBrandStaleTaskService {
|
||||
try {
|
||||
deleteBrandRunService.tryFinalizeTask(task.getId(), true);
|
||||
} catch (Exception ignored) {
|
||||
// 瀹氭椂琛ュ伩涓嶅奖鍝嶄富娴佺▼锛涗笅娆¤皟搴︽垨浜哄伐閲嶈瘯鏃跺啀澶勭悊
|
||||
// 定时补偿不影响主流程;下次调度或人工重试时再处理。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,4 +270,3 @@ public class DeleteBrandStaleTaskService {
|
||||
private int skippedTaskCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.nanri.aiimage.modules.permission.controller;
|
||||
|
||||
import com.nanri.aiimage.common.api.ApiResponse;
|
||||
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuCreateRequest;
|
||||
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuUpdateRequest;
|
||||
import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest;
|
||||
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
|
||||
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
|
||||
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/admin")
|
||||
@Tag(name = "菜单权限管理", description = "统一维护软件端与后台管理端的菜单权限配置")
|
||||
public class PermissionMenuController {
|
||||
|
||||
private final PermissionMenuService permissionMenuService;
|
||||
|
||||
@GetMapping("/permission-menus")
|
||||
@Operation(summary = "查询菜单权限列表")
|
||||
public ApiResponse<List<PermissionMenuItemVo>> listMenus(
|
||||
@Parameter(description = "菜单类型: app/admin") @RequestParam(required = false) String menuType) {
|
||||
return ApiResponse.success(permissionMenuService.list(menuType));
|
||||
}
|
||||
|
||||
@PostMapping("/permission-menus")
|
||||
@Operation(summary = "新增菜单权限项")
|
||||
public ApiResponse<PermissionMenuItemVo> createMenu(@Valid @RequestBody PermissionMenuCreateRequest request) {
|
||||
return ApiResponse.success("创建成功", permissionMenuService.create(request));
|
||||
}
|
||||
|
||||
@PutMapping("/permission-menus/{id}")
|
||||
@Operation(summary = "编辑菜单权限项")
|
||||
public ApiResponse<PermissionMenuItemVo> updateMenu(@PathVariable Long id,
|
||||
@Valid @RequestBody PermissionMenuUpdateRequest request) {
|
||||
return ApiResponse.success("更新成功", permissionMenuService.update(id, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/permission-menus/{id}")
|
||||
@Operation(summary = "删除菜单权限项")
|
||||
public ApiResponse<Void> deleteMenu(@PathVariable Long id) {
|
||||
permissionMenuService.delete(id);
|
||||
return ApiResponse.success("删除成功", null);
|
||||
}
|
||||
|
||||
@GetMapping("/permission-users/{userId}/columns")
|
||||
@Operation(summary = "查询用户菜单权限 ID 列表")
|
||||
public ApiResponse<UserColumnIdsVo> getUserColumnIds(@PathVariable Long userId,
|
||||
@RequestParam(required = false) String menuType) {
|
||||
return ApiResponse.success(permissionMenuService.getUserColumnIds(userId, menuType));
|
||||
}
|
||||
|
||||
@PutMapping("/permission-users/{userId}/columns")
|
||||
@Operation(summary = "更新用户菜单权限")
|
||||
public ApiResponse<Void> updateUserColumnIds(@PathVariable Long userId,
|
||||
@RequestBody(required = false) UserColumnPermissionUpdateRequest request) {
|
||||
permissionMenuService.updateUserColumnPermissions(userId, request);
|
||||
return ApiResponse.success("保存成功", null);
|
||||
}
|
||||
|
||||
@GetMapping("/permission-users/{userId}/column-permissions")
|
||||
@Operation(summary = "查询用户菜单权限详情")
|
||||
public ApiResponse<List<PermissionMenuItemVo>> getUserColumnPermissions(@PathVariable Long userId,
|
||||
@RequestParam(required = false) String menuType) {
|
||||
return ApiResponse.success(permissionMenuService.getUserColumnPermissions(userId, menuType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.nanri.aiimage.modules.permission.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AdminUserMapper extends BaseMapper<AdminUserEntity> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.nanri.aiimage.modules.permission.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.PermissionMenuEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PermissionMenuMapper extends BaseMapper<PermissionMenuEntity> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.nanri.aiimage.modules.permission.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.UserColumnPermissionEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface UserColumnPermissionMapper extends BaseMapper<UserColumnPermissionEntity> {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.nanri.aiimage.modules.permission.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PermissionMenuCreateRequest {
|
||||
|
||||
@NotBlank(message = "菜单名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "菜单标识不能为空")
|
||||
private String columnKey;
|
||||
|
||||
@NotBlank(message = "菜单类型不能为空")
|
||||
private String menuType;
|
||||
|
||||
@NotBlank(message = "菜单路由不能为空")
|
||||
private String routePath;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.nanri.aiimage.modules.permission.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PermissionMenuUpdateRequest {
|
||||
|
||||
@NotBlank(message = "菜单名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "菜单标识不能为空")
|
||||
private String columnKey;
|
||||
|
||||
@NotBlank(message = "菜单类型不能为空")
|
||||
private String menuType;
|
||||
|
||||
@NotBlank(message = "菜单路由不能为空")
|
||||
private String routePath;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.nanri.aiimage.modules.permission.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserColumnPermissionUpdateRequest {
|
||||
|
||||
private List<Long> columnIds;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.nanri.aiimage.modules.permission.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("users")
|
||||
public class AdminUserEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String role;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.nanri.aiimage.modules.permission.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("columns")
|
||||
public class PermissionMenuEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String columnKey;
|
||||
private String menuType;
|
||||
private String routePath;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.nanri.aiimage.modules.permission.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("user_column_permission")
|
||||
public class UserColumnPermissionEntity {
|
||||
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
@TableField("column_id")
|
||||
private Long columnId;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.nanri.aiimage.modules.permission.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class PermissionMenuItemVo {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String columnKey;
|
||||
private String menuType;
|
||||
private String routePath;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.nanri.aiimage.modules.permission.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class UserColumnIdsVo {
|
||||
|
||||
private List<Long> columnIds;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.nanri.aiimage.modules.permission.service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PermissionMenuSchemaInitializer {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
executeQuietly("""
|
||||
CREATE TABLE IF NOT EXISTS columns (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL COMMENT '栏目名',
|
||||
column_key VARCHAR(64) NOT NULL COMMENT '栏目标识',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_column_key (column_key)
|
||||
)
|
||||
""");
|
||||
executeQuietly("ALTER TABLE columns ADD COLUMN menu_type VARCHAR(20) NOT NULL DEFAULT 'app' COMMENT '菜单类型: app/admin' AFTER column_key");
|
||||
executeQuietly("ALTER TABLE columns ADD COLUMN route_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '菜单路由或页面标识' AFTER menu_type");
|
||||
executeQuietly("UPDATE columns SET menu_type = 'app' WHERE menu_type IS NULL OR menu_type = ''");
|
||||
executeQuietly("""
|
||||
INSERT INTO columns (name, column_key, menu_type, route_path)
|
||||
SELECT '店铺管理', 'admin_shop_manage', 'admin', 'shop-manage'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM columns WHERE column_key = 'admin_shop_manage'
|
||||
)
|
||||
""");
|
||||
executeQuietly("""
|
||||
CREATE TABLE IF NOT EXISTS user_column_permission (
|
||||
user_id INT NOT NULL,
|
||||
column_id INT NOT NULL,
|
||||
PRIMARY KEY (user_id, column_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
|
||||
)
|
||||
""");
|
||||
}
|
||||
|
||||
private void executeQuietly(String sql) {
|
||||
try {
|
||||
jdbcTemplate.execute(sql);
|
||||
} catch (Exception ex) {
|
||||
log.debug("[permission-menu] schema init skipped: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.nanri.aiimage.modules.permission.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
|
||||
import com.nanri.aiimage.modules.permission.mapper.PermissionMenuMapper;
|
||||
import com.nanri.aiimage.modules.permission.mapper.UserColumnPermissionMapper;
|
||||
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuCreateRequest;
|
||||
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuUpdateRequest;
|
||||
import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.PermissionMenuEntity;
|
||||
import com.nanri.aiimage.modules.permission.model.entity.UserColumnPermissionEntity;
|
||||
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
|
||||
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionMenuService {
|
||||
|
||||
public static final String MENU_TYPE_APP = "app";
|
||||
public static final String MENU_TYPE_ADMIN = "admin";
|
||||
|
||||
private final PermissionMenuMapper permissionMenuMapper;
|
||||
private final UserColumnPermissionMapper userColumnPermissionMapper;
|
||||
private final AdminUserMapper adminUserMapper;
|
||||
|
||||
public List<PermissionMenuItemVo> list(String menuType) {
|
||||
LambdaQueryWrapper<PermissionMenuEntity> query = new LambdaQueryWrapper<PermissionMenuEntity>()
|
||||
.eq(isValidMenuType(menuType), PermissionMenuEntity::getMenuType, normalizeMenuType(menuType))
|
||||
.orderByAsc(PermissionMenuEntity::getId);
|
||||
return permissionMenuMapper.selectList(query).stream()
|
||||
.map(this::toItemVo)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PermissionMenuItemVo create(PermissionMenuCreateRequest request) {
|
||||
String name = normalizeRequired(request.getName(), "菜单名称不能为空");
|
||||
String columnKey = normalizeRequired(request.getColumnKey(), "菜单标识不能为空");
|
||||
String menuType = normalizeMenuTypeRequired(request.getMenuType());
|
||||
String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空");
|
||||
ensureUniqueColumnKey(columnKey, null);
|
||||
|
||||
PermissionMenuEntity entity = new PermissionMenuEntity();
|
||||
entity.setName(name);
|
||||
entity.setColumnKey(columnKey);
|
||||
entity.setMenuType(menuType);
|
||||
entity.setRoutePath(routePath);
|
||||
permissionMenuMapper.insert(entity);
|
||||
return toItemVo(getMenuById(entity.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PermissionMenuItemVo update(Long id, PermissionMenuUpdateRequest request) {
|
||||
PermissionMenuEntity entity = getMenuById(id);
|
||||
String name = normalizeRequired(request.getName(), "菜单名称不能为空");
|
||||
String columnKey = normalizeRequired(request.getColumnKey(), "菜单标识不能为空");
|
||||
String menuType = normalizeMenuTypeRequired(request.getMenuType());
|
||||
String routePath = normalizeRequired(request.getRoutePath(), "菜单路由不能为空");
|
||||
ensureUniqueColumnKey(columnKey, id);
|
||||
|
||||
entity.setName(name);
|
||||
entity.setColumnKey(columnKey);
|
||||
entity.setMenuType(menuType);
|
||||
entity.setRoutePath(routePath);
|
||||
permissionMenuMapper.updateById(entity);
|
||||
return toItemVo(getMenuById(id));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
PermissionMenuEntity entity = getMenuById(id);
|
||||
userColumnPermissionMapper.delete(new LambdaUpdateWrapper<UserColumnPermissionEntity>()
|
||||
.eq(UserColumnPermissionEntity::getColumnId, entity.getId()));
|
||||
permissionMenuMapper.deleteById(entity.getId());
|
||||
}
|
||||
|
||||
public UserColumnIdsVo getUserColumnIds(Long userId, String menuType) {
|
||||
AdminUserEntity user = getUserById(userId);
|
||||
List<Long> columnIds;
|
||||
if (isSuperAdmin(user)) {
|
||||
columnIds = list(menuType).stream().map(PermissionMenuItemVo::getId).toList();
|
||||
} else {
|
||||
List<Long> assignedIds = userColumnPermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<UserColumnPermissionEntity>()
|
||||
.eq(UserColumnPermissionEntity::getUserId, userId))
|
||||
.stream()
|
||||
.map(UserColumnPermissionEntity::getColumnId)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (assignedIds.isEmpty()) {
|
||||
columnIds = List.of();
|
||||
} else if (isValidMenuType(menuType)) {
|
||||
Set<Long> allowedIds = list(menuType).stream()
|
||||
.map(PermissionMenuItemVo::getId)
|
||||
.collect(Collectors.toSet());
|
||||
columnIds = assignedIds.stream().filter(allowedIds::contains).toList();
|
||||
} else {
|
||||
columnIds = assignedIds;
|
||||
}
|
||||
}
|
||||
UserColumnIdsVo vo = new UserColumnIdsVo();
|
||||
vo.setColumnIds(columnIds);
|
||||
return vo;
|
||||
}
|
||||
|
||||
public List<PermissionMenuItemVo> getUserColumnPermissions(Long userId, String menuType) {
|
||||
AdminUserEntity user = getUserById(userId);
|
||||
if (isSuperAdmin(user)) {
|
||||
return list(menuType);
|
||||
}
|
||||
List<Long> assignedIds = userColumnPermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<UserColumnPermissionEntity>()
|
||||
.eq(UserColumnPermissionEntity::getUserId, userId))
|
||||
.stream()
|
||||
.map(UserColumnPermissionEntity::getColumnId)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (assignedIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<PermissionMenuEntity> menus = permissionMenuMapper.selectBatchIds(assignedIds);
|
||||
Map<Long, PermissionMenuEntity> menuMap = menus.stream()
|
||||
.collect(Collectors.toMap(PermissionMenuEntity::getId, Function.identity()));
|
||||
String safeMenuType = normalizeMenuType(menuType);
|
||||
return assignedIds.stream()
|
||||
.map(menuMap::get)
|
||||
.filter(menu -> menu != null)
|
||||
.filter(menu -> safeMenuType == null || safeMenuType.equals(menu.getMenuType()))
|
||||
.map(this::toItemVo)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateUserColumnPermissions(Long userId, UserColumnPermissionUpdateRequest request) {
|
||||
getUserById(userId);
|
||||
List<Long> requestedIds = request == null ? List.of() : normalizeColumnIds(request.getColumnIds());
|
||||
if (!requestedIds.isEmpty()) {
|
||||
Long validCount = permissionMenuMapper.selectCount(new LambdaQueryWrapper<PermissionMenuEntity>()
|
||||
.in(PermissionMenuEntity::getId, requestedIds));
|
||||
if (validCount == null || validCount != requestedIds.size()) {
|
||||
throw new BusinessException("存在无效的菜单权限项");
|
||||
}
|
||||
}
|
||||
|
||||
userColumnPermissionMapper.delete(new LambdaUpdateWrapper<UserColumnPermissionEntity>()
|
||||
.eq(UserColumnPermissionEntity::getUserId, userId));
|
||||
for (Long columnId : requestedIds) {
|
||||
UserColumnPermissionEntity entity = new UserColumnPermissionEntity();
|
||||
entity.setUserId(userId);
|
||||
entity.setColumnId(columnId);
|
||||
userColumnPermissionMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private PermissionMenuEntity getMenuById(Long id) {
|
||||
PermissionMenuEntity entity = permissionMenuMapper.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new BusinessException("菜单权限项不存在");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private AdminUserEntity getUserById(Long userId) {
|
||||
AdminUserEntity entity = adminUserMapper.selectById(userId);
|
||||
if (entity == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(AdminUserEntity user) {
|
||||
return user != null && "super_admin".equalsIgnoreCase(normalizeRequired(user.getRole(), ""));
|
||||
}
|
||||
|
||||
private void ensureUniqueColumnKey(String columnKey, Long excludeId) {
|
||||
PermissionMenuEntity exists = permissionMenuMapper.selectOne(new LambdaQueryWrapper<PermissionMenuEntity>()
|
||||
.eq(PermissionMenuEntity::getColumnKey, columnKey)
|
||||
.last("LIMIT 1"));
|
||||
if (exists != null && (excludeId == null || !excludeId.equals(exists.getId()))) {
|
||||
throw new BusinessException("菜单标识已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRequired(String value, String message) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
if (normalized.isEmpty() && !message.isEmpty()) {
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private boolean isValidMenuType(String menuType) {
|
||||
return normalizeMenuType(menuType) != null;
|
||||
}
|
||||
|
||||
private String normalizeMenuTypeRequired(String menuType) {
|
||||
String normalized = normalizeMenuType(menuType);
|
||||
if (normalized == null) {
|
||||
throw new BusinessException("菜单类型仅支持 app 或 admin");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String normalizeMenuType(String menuType) {
|
||||
String normalized = menuType == null ? "" : menuType.trim().toLowerCase(Locale.ROOT);
|
||||
if (normalized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (MENU_TYPE_APP.equals(normalized) || MENU_TYPE_ADMIN.equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Long> normalizeColumnIds(List<Long> columnIds) {
|
||||
if (columnIds == null || columnIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Set<Long> uniqueIds = new LinkedHashSet<>();
|
||||
for (Long columnId : columnIds) {
|
||||
if (columnId != null && columnId > 0) {
|
||||
uniqueIds.add(columnId);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(uniqueIds);
|
||||
}
|
||||
|
||||
private PermissionMenuItemVo toItemVo(PermissionMenuEntity entity) {
|
||||
PermissionMenuItemVo vo = new PermissionMenuItemVo();
|
||||
vo.setId(entity.getId());
|
||||
vo.setName(entity.getName());
|
||||
vo.setColumnKey(entity.getColumnKey());
|
||||
vo.setMenuType(entity.getMenuType());
|
||||
vo.setRoutePath(entity.getRoutePath());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -36,8 +38,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
@@ -50,214 +50,131 @@ import java.util.List;
|
||||
@RequestMapping("/api/product-risk-resolve")
|
||||
@Tag(
|
||||
name = "商品风险解决",
|
||||
description = "亚马逊商品风险处理:备选店铺、紫鸟匹配、创建任务、Python 队列与回传、生成 zip。"
|
||||
+ "查询类接口需带 Query 参数 user_id(与登录用户一致)。")
|
||||
description = "亚马逊商品风险处理:备选店铺、紫鸟匹配、创建任务、Python 队列与回传、生成 zip。查询类接口需要携带 Query 参数 user_id。")
|
||||
public class ProductRiskResolveController {
|
||||
|
||||
private final ProductRiskResolveService productRiskResolveService;
|
||||
private final ProductRiskTaskService productRiskTaskService;
|
||||
|
||||
@GetMapping("/candidates")
|
||||
@Operation(
|
||||
summary = "查询备选店铺列表",
|
||||
description = "返回当前用户在「商品风险」模块中已保存的备选店铺,按创建时间倒序。用于前端勾选后再调用匹配接口。")
|
||||
@Operation(summary = "查询备选店铺列表", description = "返回当前用户在商品风险模块中已保存的备选店铺。")
|
||||
public ApiResponse<List<ProductRiskCandidateVo>> listCandidates(
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID,仅返回该用户下的备选店铺",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(productRiskResolveService.listCandidates(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/candidates")
|
||||
@Operation(
|
||||
summary = "新增备选店铺",
|
||||
description = "将店铺名写入用户备选表;店铺名会规范化去重。仅用于前端勾选与匹配流程,与任务/结果表无强绑定。")
|
||||
@Operation(summary = "新增备选店铺", description = "将店铺名写入用户备选表,用于前端勾选和匹配流程。")
|
||||
public ApiResponse<ProductRiskCandidateVo> addCandidate(@Valid @RequestBody ProductRiskCandidateAddRequest request) {
|
||||
return ApiResponse.success(productRiskResolveService.addCandidate(request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/candidates/{id}")
|
||||
@Operation(
|
||||
summary = "删除备选店铺",
|
||||
description = "按主键删除一条备选记录;`id` 必须属于当前 `user_id`,否则报错。")
|
||||
@Operation(summary = "删除备选店铺", description = "删除一条备选记录,id 必须属于当前 user_id。")
|
||||
public ApiResponse<Void> deleteCandidate(
|
||||
@Parameter(description = "备选店铺记录主键(biz_product_risk_shop_candidate.id)", example = "10")
|
||||
@PathVariable Long id,
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID,用于校验数据归属",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(description = "备选店铺记录主键", example = "10") @PathVariable Long id,
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
productRiskResolveService.deleteCandidate(userId, id);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/country-preference")
|
||||
@Operation(
|
||||
summary = "查询用户五国处理顺序",
|
||||
description = "返回当前用户已保存的国家代码列表(顺序即处理顺序);未保存时返回默认:DE、UK、FR、IT、ES(全选)。")
|
||||
@Operation(summary = "查询国家处理顺序", description = "返回当前用户保存的国家代码列表,顺序即处理顺序。")
|
||||
public ApiResponse<ProductRiskCountryPreferenceVo> getCountryPreference(
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(productRiskResolveService.getCountryPreference(userId));
|
||||
}
|
||||
|
||||
@PutMapping("/country-preference")
|
||||
@Operation(
|
||||
summary = "保存用户五国处理顺序",
|
||||
description = "写入 biz_product_risk_country_pref;country_codes 为 ProductRiskCountryCode 枚举名,至少 1 个、无重复。")
|
||||
@Operation(summary = "保存国家处理顺序", description = "保存当前用户的国家勾选与处理顺序。")
|
||||
public ApiResponse<ProductRiskCountryPreferenceVo> saveCountryPreference(
|
||||
@Valid @RequestBody ProductRiskCountryPreferenceSaveRequest request) {
|
||||
return ApiResponse.success(productRiskResolveService.saveCountryPreference(request));
|
||||
}
|
||||
|
||||
@PostMapping("/match-shops")
|
||||
@Operation(
|
||||
summary = "批量匹配店铺(紫鸟索引)",
|
||||
description = "按店名查紫鸟索引,返回 shopId、platform、companyName、matchStatus 等;items 可作队列 data.items。"
|
||||
+ "未命中时 matched=false,不可创建任务。")
|
||||
@Operation(summary = "批量匹配店铺", description = "按店名查询紫鸟索引,返回 shopId、platform、companyName、matchStatus 等。")
|
||||
public ApiResponse<ProductRiskMatchShopsVo> matchShops(@Valid @RequestBody ProductRiskMatchShopsRequest request) {
|
||||
return ApiResponse.success(productRiskResolveService.matchShops(request));
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
@Operation(
|
||||
summary = "统计看板",
|
||||
description = "汇总当前用户:备选店铺数量;商品风险模块下已结束任务数(SUCCESS/FAILED);其中成功任务数、失败任务数。")
|
||||
@Operation(summary = "统计看板", description = "返回候选店铺数量、已结束任务数、成功任务数和失败任务数。")
|
||||
public ApiResponse<ProductRiskDashboardVo> dashboard(
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(productRiskTaskService.dashboard(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/history")
|
||||
@Operation(
|
||||
summary = "处理记录列表(含进行中与已结束)",
|
||||
description = "返回当前用户商品风险模块下各店结果行(biz_file_result),按创建时间倒序最多 100 条;"
|
||||
+ "含 resultId、taskId、店名、任务状态、success、错误、输出文件名等。")
|
||||
@Operation(summary = "处理记录列表", description = "返回当前用户商品风险模块下各店结果行,含运行中与历史记录。")
|
||||
public ApiResponse<ProductRiskHistoryVo> history(
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(productRiskTaskService.listHistory(userId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/history/{resultId}")
|
||||
@Operation(
|
||||
summary = "删除单条店铺结果记录",
|
||||
description = "删除一条 biz_file_result(须为本模块且归属 user_id);随后重算父任务,无子结果则删任务。")
|
||||
@Operation(summary = "删除单条店铺结果记录", description = "删除一条 biz_file_result,并在后端同步重算父任务状态。")
|
||||
public ApiResponse<Void> deleteHistory(
|
||||
@Parameter(description = "结果行主键 biz_file_result.id", example = "1001")
|
||||
@PathVariable Long resultId,
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID,用于校验该结果归属",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(description = "结果行主键 biz_file_result.id", example = "1001") @PathVariable Long resultId,
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
productRiskTaskService.deleteHistory(resultId, userId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@PostMapping("/tasks")
|
||||
@Operation(
|
||||
summary = "创建商品风险处理任务",
|
||||
description = "推 Python 队列前调用:落库 RUNNING 任务及各店 biz_file_result;响应 taskId 写入队列 data.taskId,"
|
||||
+ "items 含各店 resultId。请求 items 须全为 matched=true,店名会去重。")
|
||||
@Operation(summary = "创建商品风险处理任务", description = "落库 RUNNING 任务及各店占位结果,返回 taskId 与 items。")
|
||||
public ApiResponse<ProductRiskCreateTaskVo> createTask(@Valid @RequestBody ProductRiskCreateTaskRequest request) {
|
||||
return ApiResponse.success(productRiskTaskService.createTask(request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/tasks/{taskId}")
|
||||
@Operation(
|
||||
summary = "删除整条任务",
|
||||
description = "删除该任务及下属全部结果行(RUNNING/SUCCESS/FAILED 均可);user_id 须与任务归属一致。")
|
||||
@Operation(summary = "删除整条任务", description = "删除该任务及其下属全部结果行,RUNNING/SUCCESS/FAILED 均可。")
|
||||
public ApiResponse<Void> deleteTask(
|
||||
@Parameter(description = "任务主键 biz_file_task.id", example = "200")
|
||||
@PathVariable Long taskId,
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID,须与任务归属一致",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(description = "任务主键 biz_file_task.id", example = "200") @PathVariable Long taskId,
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId) {
|
||||
productRiskTaskService.deleteTask(taskId, userId);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@DeleteMapping("/pending-shop-result")
|
||||
@Operation(
|
||||
summary = "按店铺名删除运行中任务下的一条结果",
|
||||
description = "仅在 RUNNING 任务下按规范化店名删一条 biz_file_result 并重算任务;无匹配时 removed=false。"
|
||||
+ "用于前端从匹配列表移除店铺时同步后端占位。")
|
||||
@Operation(summary = "按店铺名删除运行中结果", description = "仅在 RUNNING 任务下按规范化店名删除一条 biz_file_result,并重算任务。")
|
||||
public ApiResponse<ProductRiskPendingDeleteVo> deletePendingShopResult(
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId,
|
||||
@Parameter(
|
||||
name = "shop_name",
|
||||
description = "店铺名称(与匹配/创建任务时一致;服务端会按紫鸟规则规范化后再与库中 source_filename 比对)",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "某某店铺")
|
||||
@Parameter(name = "shop_name", description = "店铺名称", required = true, in = ParameterIn.QUERY, example = "某某店铺")
|
||||
@RequestParam("shop_name") String shopName) {
|
||||
return ApiResponse.success(productRiskTaskService.deletePendingShopResult(userId, shopName));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/batch")
|
||||
@Operation(
|
||||
summary = "批量查询任务详情(轮询)",
|
||||
description = "按 taskIds 返回任务概要及各店结果;无效 id 记入 missingTaskIds。供前端轮询 RUNNING 至结束。")
|
||||
@Operation(summary = "批量查询任务详情", description = "按 taskIds 返回任务概览与各店结果,供前端轮询使用。")
|
||||
public ApiResponse<ProductRiskTaskBatchVo> tasksBatch(@Valid @RequestBody ProductRiskTaskBatchRequest request) {
|
||||
return ApiResponse.success(productRiskTaskService.getTaskDetailsBatch(request.getTaskIds()));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/result")
|
||||
@Operation(
|
||||
summary = "Python 回传处理结果(可多次、可分批)",
|
||||
description = "Python 调用;同一 taskId 可多次,未出现在 shops 中的店保持待处理。"
|
||||
+ "有 error 则标店失败;否则按 countries(键为五国枚举 DE/FR/ES/IT/UK)生成 xlsx/zip 上传 OSS,"
|
||||
+ "Excel 内仍为中文工作表名。全部店终态后任务 SUCCESS/FAILED。")
|
||||
summary = "Python 回传处理结果",
|
||||
description = "同一 taskId 可多次回传。成功时按 countries 生成 xlsx/zip 并上传 OSS,全部店铺终态后任务进入 SUCCESS/FAILED。")
|
||||
public ApiResponse<Void> submitResult(
|
||||
@Parameter(description = "任务主键,须为 RUNNING 状态", example = "200")
|
||||
@PathVariable Long taskId,
|
||||
@Parameter(description = "任务主键,需处于 RUNNING 状态", example = "200") @PathVariable Long taskId,
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "按店铺回传:成功时 `countries` 的 key 为枚举 ProductRiskCountryCode(JSON 与枚举名一致:DE、FR、ES、IT、UK);"
|
||||
+ "失败时填 `error`,可不传 `countries`",
|
||||
description = "按店铺回传结果;失败时填写 error,成功时填写 countries。",
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ProductRiskSubmitResultRequest.class),
|
||||
examples = {
|
||||
@ExampleObject(
|
||||
name = "成功(单店,五国含样例行)",
|
||||
summary = "DE、UK 各一行,其余国可为空数组",
|
||||
name = "成功示例",
|
||||
summary = "DE、UK 各一行,其余国家可为空数组",
|
||||
value = """
|
||||
{
|
||||
"shops": [
|
||||
@@ -269,7 +186,7 @@ public class ProductRiskResolveController {
|
||||
"shopName": "模拟旗舰店A",
|
||||
"productAsinSku": "B0DEMO1234",
|
||||
"status": "风险待解除",
|
||||
"done": "否",
|
||||
"done": true,
|
||||
"removeAsin": "B0OLD5678",
|
||||
"removeStatus": "排队中"
|
||||
}
|
||||
@@ -282,7 +199,7 @@ public class ProductRiskResolveController {
|
||||
"shopName": "模拟旗舰店A",
|
||||
"productAsinSku": "B0UKDEMO99",
|
||||
"status": "正常",
|
||||
"done": "是",
|
||||
"done": true,
|
||||
"removeAsin": "",
|
||||
"removeStatus": ""
|
||||
}
|
||||
@@ -293,7 +210,7 @@ public class ProductRiskResolveController {
|
||||
}
|
||||
"""),
|
||||
@ExampleObject(
|
||||
name = "失败(单店仅 error)",
|
||||
name = "失败示例",
|
||||
summary = "该店标记失败,不生成 zip",
|
||||
value = """
|
||||
{
|
||||
@@ -312,18 +229,10 @@ public class ProductRiskResolveController {
|
||||
}
|
||||
|
||||
@GetMapping("/results/{resultId}/download")
|
||||
@Operation(
|
||||
summary = "流式下载单个店铺的 zip 结果",
|
||||
description = "从 OSS 拉取 zip 以附件写入响应体(非 JSON);须归属 user_id;无文件则 404。")
|
||||
@Operation(summary = "下载单个店铺的 zip 结果", description = "从 OSS 拉取 zip 并以附件流返回;需校验 user_id 归属。")
|
||||
public void downloadResult(
|
||||
@Parameter(description = "店铺结果行主键 biz_file_result.id", example = "1001")
|
||||
@PathVariable Long resultId,
|
||||
@Parameter(
|
||||
name = "user_id",
|
||||
description = "当前用户 ID",
|
||||
required = true,
|
||||
in = ParameterIn.QUERY,
|
||||
example = "1")
|
||||
@Parameter(description = "结果行主键 biz_file_result.id", example = "1001") @PathVariable Long resultId,
|
||||
@Parameter(name = "user_id", description = "当前用户 ID", required = true, in = ParameterIn.QUERY, example = "1")
|
||||
@RequestParam("user_id") Long userId,
|
||||
jakarta.servlet.http.HttpServletResponse response) {
|
||||
String url = productRiskTaskService.resolveResultDownloadUrl(resultId, userId);
|
||||
|
||||
@@ -201,11 +201,13 @@ public class ProductRiskTaskService {
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
return;
|
||||
}
|
||||
String oldStatus = task.getStatus();
|
||||
List<FileResultEntity> latest = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||
.eq(FileResultEntity::getTaskId, taskId)
|
||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(FileResultEntity::getId));
|
||||
if (latest.isEmpty()) {
|
||||
log.warn("[product-risk] reconcile delete empty taskId={} oldStatus={} -> deleting task", taskId, oldStatus);
|
||||
fileTaskMapper.deleteById(taskId);
|
||||
return;
|
||||
}
|
||||
@@ -255,6 +257,8 @@ public class ProductRiskTaskService {
|
||||
log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
log.warn("[product-risk] reconcile status changed taskId={} oldStatus={} newStatus={} ok={} fail={} allDone={} errorMessage={}",
|
||||
taskId, oldStatus, task.getStatus(), ok, fail, allDone, task.getErrorMessage());
|
||||
|
||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||
productRiskTaskCacheService.deleteTaskCache(taskId);
|
||||
@@ -384,8 +388,11 @@ public class ProductRiskTaskService {
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||
log.warn("[product-risk] submitResult rejected taskId={} status={} payloadShopCount={}",
|
||||
taskId, task.getStatus(), request.getShops() == null ? 0 : request.getShops().size());
|
||||
throw new BusinessException("任务已结束,拒绝重复提交");
|
||||
}
|
||||
String oldStatus = task.getStatus();
|
||||
|
||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||
.eq(FileResultEntity::getTaskId, taskId)
|
||||
@@ -525,6 +532,9 @@ public class ProductRiskTaskService {
|
||||
log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
log.warn("[product-risk] submitResult status evaluated taskId={} oldStatus={} newStatus={} ok={} fail={} allDone={} matchedShopCount={} skippedUnmatchedCount={} waitingCount={} assembledCount={} fallbackMatchedCount={} batchErrors={}",
|
||||
taskId, oldStatus, task.getStatus(), ok, fail, allDone, matchedShopCount, skippedUnmatchedCount,
|
||||
waitingCount, assembledCount, fallbackMatchedCount, batchErrors);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -537,8 +547,11 @@ public class ProductRiskTaskService {
|
||||
return false;
|
||||
}
|
||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||
log.warn("[product-risk] stale finalize skipped taskId={} status={} fromCompensation={}",
|
||||
taskId, task.getStatus(), fromCompensation);
|
||||
return true;
|
||||
}
|
||||
String oldStatus = task.getStatus();
|
||||
|
||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||
.eq(FileResultEntity::getTaskId, taskId)
|
||||
@@ -620,6 +633,8 @@ public class ProductRiskTaskService {
|
||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(FileResultEntity::getId));
|
||||
updateTaskStatusFromLatestRows(task, latest, batchErrors);
|
||||
log.warn("[product-risk] stale finalize status evaluated taskId={} oldStatus={} newStatus={} changed={} cachedPayloadCount={} fromCompensation={} batchErrors={}",
|
||||
taskId, oldStatus, task.getStatus(), changed, cachedPayloadByShop.size(), fromCompensation, batchErrors);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -677,6 +692,7 @@ public class ProductRiskTaskService {
|
||||
private void updateTaskStatusFromLatestRows(FileTaskEntity task,
|
||||
List<FileResultEntity> latest,
|
||||
List<String> batchErrors) {
|
||||
String oldStatus = task.getStatus();
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
List<String> allErrors = new ArrayList<>();
|
||||
@@ -721,6 +737,8 @@ public class ProductRiskTaskService {
|
||||
log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
log.warn("[product-risk] status updated from latest rows taskId={} oldStatus={} newStatus={} ok={} fail={} allDone={} batchErrors={} errorMessage={}",
|
||||
task.getId(), oldStatus, task.getStatus(), ok, fail, allDone, batchErrors, task.getErrorMessage());
|
||||
}
|
||||
|
||||
private ProductRiskTaskDetailVo buildTaskDetail(FileTaskEntity task) {
|
||||
|
||||
@@ -61,7 +61,7 @@ public class ShopMatchTaskService {
|
||||
|
||||
public ProductRiskDashboardVo dashboard(Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
ProductRiskDashboardVo vo = new ProductRiskDashboardVo();
|
||||
vo.setCandidateCount(candidateMapper.selectCount(new LambdaQueryWrapper<ShopMatchShopCandidateEntity>()
|
||||
@@ -86,7 +86,7 @@ public class ShopMatchTaskService {
|
||||
|
||||
public ProductRiskHistoryVo listHistory(Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
ProductRiskHistoryVo vo = new ProductRiskHistoryVo();
|
||||
List<FileResultEntity> entities = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||
@@ -118,11 +118,11 @@ public class ShopMatchTaskService {
|
||||
@Transactional
|
||||
public void deleteHistory(Long resultId, Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
Long taskId = entity.getTaskId();
|
||||
fileResultMapper.deleteById(resultId);
|
||||
@@ -134,11 +134,11 @@ public class ShopMatchTaskService {
|
||||
@Transactional
|
||||
public void deleteTask(Long taskId, Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
|
||||
.eq(FileResultEntity::getTaskId, taskId)
|
||||
@@ -186,10 +186,10 @@ public class ShopMatchTaskService {
|
||||
public String resolveResultDownloadUrl(Long resultId, Long userId) {
|
||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("记录不存在");
|
||||
}
|
||||
if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl());
|
||||
}
|
||||
@@ -197,7 +197,7 @@ public class ShopMatchTaskService {
|
||||
public String resolveResultDownloadFilename(Long resultId, Long userId) {
|
||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
return entity.getResultFilename() != null && !entity.getResultFilename().isBlank()
|
||||
? entity.getResultFilename()
|
||||
@@ -207,18 +207,18 @@ public class ShopMatchTaskService {
|
||||
@Transactional
|
||||
public ProductRiskCreateTaskVo createTask(ShopMatchCreateTaskRequest request) {
|
||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
if (request.getItems() == null || request.getItems().isEmpty()) {
|
||||
throw new BusinessException("items 涓嶈兘涓虹┖");
|
||||
throw new BusinessException("items 不能为空");
|
||||
}
|
||||
List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(request.getItems());
|
||||
if (uniqueItems.isEmpty()) {
|
||||
throw new BusinessException("items 涓嶈兘涓虹┖");
|
||||
throw new BusinessException("items 不能为空");
|
||||
}
|
||||
for (ProductRiskShopQueueItemVo item : uniqueItems) {
|
||||
if (item == null || !item.isMatched()) {
|
||||
throw new BusinessException("瀛樺湪鏈尮閰嶅簵閾猴紝鏃犳硶鍒涘缓浠诲姟");
|
||||
throw new BusinessException("存在未匹配店铺,无法创建任务");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ public class ShopMatchTaskService {
|
||||
for (ProductRiskShopQueueItemVo item : uniqueItems) {
|
||||
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
||||
if (normalized.isBlank()) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务数据已失效,请刷新后重试");
|
||||
}
|
||||
FileResultEntity result = new FileResultEntity();
|
||||
result.setTaskId(task.getId());
|
||||
@@ -271,7 +271,7 @@ public class ShopMatchTaskService {
|
||||
task.setRequestJson(objectMapper.writeValueAsString(persistedRequest));
|
||||
task.setResultJson(objectMapper.writeValueAsString(snapshot));
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("task 不存在");
|
||||
}
|
||||
fileTaskMapper.updateById(task);
|
||||
|
||||
@@ -284,34 +284,34 @@ public class ShopMatchTaskService {
|
||||
@Transactional
|
||||
public void activateTask(Long taskId, Long userId, Integer stageIndex) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
throw new BusinessException("taskId is invalid");
|
||||
throw new BusinessException("taskId 不合法");
|
||||
}
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
if (stageIndex == null || stageIndex < 0) {
|
||||
throw new BusinessException("stage_index is invalid");
|
||||
throw new BusinessException("stage_index 不合法");
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务状态不正确");
|
||||
}
|
||||
if (!"SCHEDULED".equals(task.getStatus())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务状态不正确");
|
||||
}
|
||||
if (task.getScheduledAt() != null && LocalDateTime.now().isBefore(task.getScheduledAt())) {
|
||||
throw new BusinessException("鏈埌瀹氭椂鎵ц鏃堕棿");
|
||||
if (task.getScheduledAt() != null && LocalDateTime.now().isBefore(task.getScheduledAt().minusSeconds(90))) {
|
||||
throw new BusinessException("未到定时执行时间");
|
||||
}
|
||||
ShopMatchCreateTaskRequest state = parseTaskRequest(task);
|
||||
int currentStageIndex = state.getCurrentStageIndex() == null ? 0 : state.getCurrentStageIndex();
|
||||
if (currentStageIndex != stageIndex) {
|
||||
throw new BusinessException("褰撳墠杞宸插彉鍖栵紝璇峰埛鏂板悗閲嶈瘯");
|
||||
throw new BusinessException("当前轮次已变化,请刷新后重试");
|
||||
}
|
||||
if (state.getScheduleTimes() == null || stageIndex >= state.getScheduleTimes().size()) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("轮次配置缺失");
|
||||
}
|
||||
state.setActiveStageIndex(stageIndex);
|
||||
task.setStatus("RUNNING");
|
||||
@@ -323,28 +323,28 @@ public class ShopMatchTaskService {
|
||||
@Transactional
|
||||
public void completeStage(Long taskId, Long userId, ShopMatchStageCompleteRequest request) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
throw new BusinessException("taskId is invalid");
|
||||
throw new BusinessException("taskId 不合法");
|
||||
}
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id is invalid");
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
if (request == null || request.getStageIndex() == null || request.getStageIndex() < 0) {
|
||||
throw new BusinessException("stage_index is invalid");
|
||||
throw new BusinessException("stage_index 不合法");
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
if (!"RUNNING".equals(task.getStatus())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务状态不正确");
|
||||
}
|
||||
ShopMatchCreateTaskRequest state = parseTaskRequest(task);
|
||||
if (state.getActiveStageIndex() == null || !state.getActiveStageIndex().equals(request.getStageIndex())) {
|
||||
throw new BusinessException("杞鐘舵€佷笉鍖归厤");
|
||||
throw new BusinessException("轮次状态不匹配");
|
||||
}
|
||||
int nextIndex = request.getStageIndex() + 1;
|
||||
if (state.getScheduleTimes() == null || nextIndex >= state.getScheduleTimes().size()) {
|
||||
throw new BusinessException("Current stage is already the final stage");
|
||||
throw new BusinessException("当前轮次已经是最后一轮");
|
||||
}
|
||||
state.setCurrentStageIndex(nextIndex);
|
||||
state.setActiveStageIndex(null);
|
||||
@@ -360,18 +360,18 @@ public class ShopMatchTaskService {
|
||||
@Transactional
|
||||
public void submitResult(Long taskId, ShopMatchSubmitResultRequest request) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
throw new BusinessException("taskId is invalid");
|
||||
throw new BusinessException("taskId 不合法");
|
||||
}
|
||||
if (request == null || request.getShops() == null || request.getShops().isEmpty()) {
|
||||
throw new BusinessException("shops 涓嶈兘涓虹┖");
|
||||
throw new BusinessException("shops 不能为空");
|
||||
}
|
||||
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("任务不存在");
|
||||
}
|
||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||
throw new BusinessException("浠诲姟宸茬粨鏉燂紝鎷掔粷閲嶅鎻愪氦");
|
||||
throw new BusinessException("任务已结束,拒绝重复提交");
|
||||
}
|
||||
|
||||
List<FileResultEntity> resultRows = fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||
@@ -408,7 +408,7 @@ public class ShopMatchTaskService {
|
||||
continue;
|
||||
}
|
||||
if (countPayloadRows(merged) <= 0) {
|
||||
markResultFailed(result, "No result rows received for assembly");
|
||||
markResultFailed(result, "未收到可用于组装的结果数据");
|
||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
continue;
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public class ShopMatchTaskService {
|
||||
assembleShopResult(result, shopKey, merged, workRoot);
|
||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
} catch (Exception ex) {
|
||||
markResultFailed(result, ex.getMessage() == null ? "assemble failed" : ex.getMessage());
|
||||
markResultFailed(result, ex.getMessage() == null ? "结果组装失败" : ex.getMessage());
|
||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
}
|
||||
}
|
||||
@@ -468,8 +468,8 @@ public class ShopMatchTaskService {
|
||||
ShopMatchShopPayloadDto cachedPayload = shopKey == null ? null : cachedPayloadByShop.get(shopKey);
|
||||
|
||||
if (cachedPayload == null) {
|
||||
markResultFailed(result, "Python interrupted before this shop finished uploading results");
|
||||
batchErrors.add(shopKey + ": Python interrupted before this shop finished uploading results");
|
||||
markResultFailed(result, "Python 在该店铺结果上传完成前中断");
|
||||
batchErrors.add(shopKey + ": Python 在该店铺结果上传完成前中断");
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
@@ -483,7 +483,7 @@ public class ShopMatchTaskService {
|
||||
}
|
||||
|
||||
if (countPayloadRows(cachedPayload) <= 0 && payloadHasAnyDoneTrue(cachedPayload)) {
|
||||
markResultFailed(result, "No result rows received for assembly");
|
||||
markResultFailed(result, "未收到可用于组装的结果数据");
|
||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
changed = true;
|
||||
log.warn("[shop-match] stale finalize finished without data taskId={} shop={} fromCompensation={}",
|
||||
@@ -492,8 +492,8 @@ public class ShopMatchTaskService {
|
||||
}
|
||||
|
||||
if (countPayloadRows(cachedPayload) <= 0) {
|
||||
markResultFailed(result, "Python interrupted before any assembleable rows were uploaded");
|
||||
batchErrors.add(shopKey + ": Python interrupted before any assembleable rows were uploaded");
|
||||
markResultFailed(result, "Python 在上传可组装结果前中断");
|
||||
batchErrors.add(shopKey + ": Python 在上传可组装结果前中断");
|
||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
changed = true;
|
||||
continue;
|
||||
@@ -506,7 +506,7 @@ public class ShopMatchTaskService {
|
||||
log.warn("[shop-match] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={}",
|
||||
taskId, shopKey, fromCompensation, isShopPayloadCompleted(cachedPayload));
|
||||
} catch (Exception ex) {
|
||||
String message = ex.getMessage() == null ? "assemble failed" : ex.getMessage();
|
||||
String message = ex.getMessage() == null ? "结果组装失败" : ex.getMessage();
|
||||
markResultFailed(result, message);
|
||||
batchErrors.add(shopKey + ": " + message);
|
||||
shopMatchTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
@@ -595,11 +595,11 @@ public class ShopMatchTaskService {
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
} else if (ok > 0) {
|
||||
task.setStatus("SUCCESS");
|
||||
task.setErrorMessage(String.join("; ", errors.isEmpty() ? batchErrors : errors));
|
||||
task.setErrorMessage(String.join(";", errors.isEmpty() ? batchErrors : errors));
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
} else {
|
||||
task.setStatus("FAILED");
|
||||
task.setErrorMessage(errors.isEmpty() ? "all shops failed" : String.join("; ", errors));
|
||||
task.setErrorMessage(errors.isEmpty() ? "全部店铺处理失败" : String.join(";", errors));
|
||||
task.setFinishedAt(LocalDateTime.now());
|
||||
}
|
||||
try {
|
||||
@@ -633,7 +633,7 @@ public class ShopMatchTaskService {
|
||||
.orderByAsc(FileResultEntity::getId));
|
||||
List<ProductRiskResultItemVo> items = new ArrayList<>();
|
||||
for (FileResultEntity row : rows) {
|
||||
throw new BusinessException("Task request payload is missing");
|
||||
items.add(toHistoryItemVo(row, taskStatus));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -778,7 +778,7 @@ public class ShopMatchTaskService {
|
||||
private ShopMatchCreateTaskRequest parseTaskRequest(FileTaskEntity task) {
|
||||
ShopMatchCreateTaskRequest request = parseTaskRequestSilently(task);
|
||||
if (request == null) {
|
||||
throw new BusinessException("浠诲姟閰嶇疆缂哄け");
|
||||
throw new BusinessException("任务配置缺失");
|
||||
}
|
||||
if (request.getItems() == null) {
|
||||
request.setItems(new ArrayList<>());
|
||||
@@ -807,7 +807,7 @@ public class ShopMatchTaskService {
|
||||
try {
|
||||
task.setRequestJson(objectMapper.writeValueAsString(request));
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("Invalid request");
|
||||
throw new BusinessException("序列化任务数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,7 +831,7 @@ public class ShopMatchTaskService {
|
||||
|
||||
private List<String> normalizeCountryCodes(List<String> raw) {
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
throw new BusinessException("country_codes 涓嶈兘涓虹┖");
|
||||
throw new BusinessException("country_codes 不能为空");
|
||||
}
|
||||
LinkedHashSet<String> out = new LinkedHashSet<>();
|
||||
for (String code : raw) {
|
||||
@@ -842,12 +842,12 @@ public class ShopMatchTaskService {
|
||||
try {
|
||||
ProductRiskCountryCode.valueOf(normalized);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new BusinessException("闈炴硶鍥藉浠g爜: " + code);
|
||||
throw new BusinessException("非法国家代码: " + code);
|
||||
}
|
||||
out.add(normalized);
|
||||
}
|
||||
if (out.isEmpty()) {
|
||||
throw new BusinessException("country_codes 涓嶈兘涓虹┖");
|
||||
throw new BusinessException("country_codes 不能为空");
|
||||
}
|
||||
return new ArrayList<>(out);
|
||||
}
|
||||
@@ -864,16 +864,16 @@ public class ShopMatchTaskService {
|
||||
continue;
|
||||
}
|
||||
if (!time.isAfter(now)) {
|
||||
throw new BusinessException("schedule_times 蹇呴』鏅氫簬褰撳墠鏃堕棿");
|
||||
throw new BusinessException("schedule_times 必须晚于当前时间");
|
||||
}
|
||||
if (previous != null && !time.isAfter(previous)) {
|
||||
throw new BusinessException("schedule_times 蹇呴』鎸夋椂闂撮€掑");
|
||||
throw new BusinessException("schedule_times 必须按时间递增");
|
||||
}
|
||||
out.add(time);
|
||||
previous = time;
|
||||
}
|
||||
if (out.isEmpty()) {
|
||||
throw new BusinessException("schedule_times 涓嶈兘涓虹┖");
|
||||
throw new BusinessException("schedule_times 不能为空");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE `columns`
|
||||
ADD COLUMN `menu_type` VARCHAR(20) NOT NULL DEFAULT 'app' COMMENT '菜单类型: app/admin' AFTER `column_key`,
|
||||
ADD COLUMN `route_path` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '菜单路由或页面标识' AFTER `menu_type`;
|
||||
|
||||
INSERT INTO `columns` (`name`, `column_key`, `menu_type`, `route_path`)
|
||||
SELECT '店铺管理', 'admin_shop_manage', 'admin', 'shop-manage'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `columns` WHERE `column_key` = 'admin_shop_manage'
|
||||
);
|
||||
Reference in New Issue
Block a user