改造后台权限和APP权限
This commit is contained in:
BIN
backend-java/aiimage-backend.log.2026-04-17.0.gz
Normal file
BIN
backend-java/aiimage-backend.log.2026-04-17.0.gz
Normal file
Binary file not shown.
@@ -15,6 +15,12 @@ public class PermissionMenuSchemaInitializer {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
private static final List<DefaultAppMenu> DEFAULT_APP_MENUS = List.of(
|
||||
new DefaultAppMenu("前端工具", "brand_front_tools", "brand-front-tools", 110),
|
||||
new DefaultAppMenu("运营工具", "brand_operation_tools", "brand-operation-tools", 120),
|
||||
new DefaultAppMenu("后勤工具", "brand_logistics_tools", "brand-logistics-tools", 130)
|
||||
);
|
||||
|
||||
private static final List<DefaultAdminMenu> DEFAULT_ADMIN_MENUS = List.of(
|
||||
new DefaultAdminMenu("用户管理", "admin_users", "users", 10),
|
||||
new DefaultAdminMenu("栏目权限配置", "admin_columns", "columns", 20),
|
||||
@@ -44,6 +50,7 @@ public class PermissionMenuSchemaInitializer {
|
||||
executeQuietly("UPDATE columns SET sort_order = id WHERE sort_order IS NULL OR sort_order = 0");
|
||||
executeQuietly("ALTER TABLE columns ADD UNIQUE KEY uk_menu_type_route_path (menu_type, route_path)");
|
||||
ensureDefaultAdminMenus();
|
||||
ensureDefaultAppMenus();
|
||||
executeQuietly("""
|
||||
CREATE TABLE IF NOT EXISTS user_column_permission (
|
||||
user_id INT NOT NULL,
|
||||
@@ -72,6 +79,23 @@ public class PermissionMenuSchemaInitializer {
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureDefaultAppMenus() {
|
||||
for (DefaultAppMenu menu : DEFAULT_APP_MENUS) {
|
||||
executeQuietly("""
|
||||
INSERT INTO columns (name, column_key, menu_type, route_path, sort_order)
|
||||
SELECT '%s', '%s', 'app', '%s', %d
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM columns WHERE column_key = '%s'
|
||||
)
|
||||
""".formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.sortOrder(), menu.columnKey()));
|
||||
executeQuietly("""
|
||||
UPDATE columns
|
||||
SET sort_order = %d, route_path = '%s', menu_type = 'app'
|
||||
WHERE column_key = '%s'
|
||||
""".formatted(menu.sortOrder(), menu.routePath(), menu.columnKey()));
|
||||
}
|
||||
}
|
||||
|
||||
private void executeQuietly(String sql) {
|
||||
try {
|
||||
jdbcTemplate.execute(sql);
|
||||
@@ -82,4 +106,7 @@ public class PermissionMenuSchemaInitializer {
|
||||
|
||||
private record DefaultAdminMenu(String name, String columnKey, String routePath, int sortOrder) {
|
||||
}
|
||||
|
||||
private record DefaultAppMenu(String name, String columnKey, String routePath, int sortOrder) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.nanri.aiimage.common.api.ApiResponse;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCandidateAddRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCountryPreferenceSaveRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCreateTaskRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackLoopRunChildFinishedRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackLoopRunCreateRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackMatchShopsRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackSubmitResultRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackTaskBatchRequest;
|
||||
@@ -12,9 +14,12 @@ import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCountryPreference
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCreateTaskVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackDashboardVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackHistoryVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackLoopRunDispatchVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackLoopRunVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackPendingDeleteVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackTaskBatchVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackLoopRunService;
|
||||
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackService;
|
||||
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -53,6 +58,7 @@ public class PriceTrackController {
|
||||
|
||||
private final PriceTrackService priceTrackService;
|
||||
private final PriceTrackTaskService priceTrackTaskService;
|
||||
private final PriceTrackLoopRunService priceTrackLoopRunService;
|
||||
|
||||
@GetMapping("/candidates")
|
||||
@Operation(summary = "查询备选店铺列表", description = "返回当前用户在跟价模块中保存的备选店铺。")
|
||||
@@ -131,6 +137,45 @@ public class PriceTrackController {
|
||||
return ApiResponse.success(priceTrackTaskService.createTask(request));
|
||||
}
|
||||
|
||||
@PostMapping("/loop-runs")
|
||||
@Operation(summary = "创建跟价循环任务", description = "创建整批店铺的多轮循环主任务。")
|
||||
public ApiResponse<PriceTrackLoopRunVo> createLoopRun(@Valid @RequestBody PriceTrackLoopRunCreateRequest request) {
|
||||
return ApiResponse.success(priceTrackLoopRunService.createLoopRun(request));
|
||||
}
|
||||
|
||||
@GetMapping("/loop-runs/{loopRunId}")
|
||||
@Operation(summary = "查询跟价循环任务", description = "返回循环任务当前状态与进度。")
|
||||
public ApiResponse<PriceTrackLoopRunVo> getLoopRun(
|
||||
@PathVariable Long loopRunId,
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(priceTrackLoopRunService.getLoopRun(loopRunId, userId));
|
||||
}
|
||||
|
||||
@PostMapping("/loop-runs/{loopRunId}/dispatch-next")
|
||||
@Operation(summary = "获取循环任务下一条派发信息", description = "当前无活跃子任务时调用,返回下一条应该创建并入队的子任务请求。")
|
||||
public ApiResponse<PriceTrackLoopRunDispatchVo> dispatchNext(
|
||||
@PathVariable Long loopRunId,
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(priceTrackLoopRunService.dispatchNext(loopRunId, userId));
|
||||
}
|
||||
|
||||
@PostMapping("/loop-runs/{loopRunId}/child-finished")
|
||||
@Operation(summary = "通知循环任务子任务已结束", description = "子任务进入终态后调用,用于推进到下一店或下一轮。")
|
||||
public ApiResponse<PriceTrackLoopRunVo> childFinished(
|
||||
@PathVariable Long loopRunId,
|
||||
@RequestParam("user_id") Long userId,
|
||||
@Valid @RequestBody PriceTrackLoopRunChildFinishedRequest request) {
|
||||
return ApiResponse.success(priceTrackLoopRunService.childFinished(loopRunId, userId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/loop-runs/{loopRunId}/stop")
|
||||
@Operation(summary = "停止循环任务", description = "请求停止循环;若无活跃子任务则立即停止,否则等待当前子任务完成后收尾。")
|
||||
public ApiResponse<PriceTrackLoopRunVo> stopLoopRun(
|
||||
@PathVariable Long loopRunId,
|
||||
@RequestParam("user_id") Long userId) {
|
||||
return ApiResponse.success(priceTrackLoopRunService.requestStop(loopRunId, userId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/tasks/{taskId}")
|
||||
@Operation(summary = "删除整条任务", description = "删除该任务以及其下全部结果行,RUNNING、SUCCESS、FAILED 均可删除。")
|
||||
public ApiResponse<Void> deleteTask(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.entity.PriceTrackLoopRunEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PriceTrackLoopRunMapper extends BaseMapper<PriceTrackLoopRunEntity> {
|
||||
}
|
||||
@@ -33,4 +33,12 @@ public class PriceTrackCreateTaskRequest {
|
||||
@NotEmpty(message = "国家列表不能为空")
|
||||
@Schema(description = "国家代码列表,顺序即跟价处理顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"DE\",\"UK\",\"FR\"]")
|
||||
private List<String> countryCodes;
|
||||
@Schema(description = "optional loop run id", example = "1")
|
||||
private Long loopRunId;
|
||||
|
||||
@Schema(description = "optional round index, 1-based", example = "2")
|
||||
private Integer roundIndex;
|
||||
|
||||
@Schema(description = "optional shop index, 0-based", example = "0")
|
||||
private Integer shopIndex;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "循环任务子任务完成通知")
|
||||
public class PriceTrackLoopRunChildFinishedRequest {
|
||||
|
||||
@NotNull(message = "childTaskId不能为空")
|
||||
private Long childTaskId;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.model.dto;
|
||||
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建跟价循环任务请求")
|
||||
public class PriceTrackLoopRunCreateRequest {
|
||||
|
||||
@NotNull(message = "user_id不能为空")
|
||||
private Long userId;
|
||||
|
||||
private boolean statusMode;
|
||||
|
||||
private boolean asinMode;
|
||||
|
||||
@NotEmpty(message = "items不能为空")
|
||||
@Valid
|
||||
private List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> items = new ArrayList<>();
|
||||
|
||||
private List<String> asinFiles = new ArrayList<>();
|
||||
|
||||
@NotEmpty(message = "countryCodes不能为空")
|
||||
private List<String> countryCodes = new ArrayList<>();
|
||||
|
||||
@NotBlank(message = "executionMode不能为空")
|
||||
@Schema(description = "FINITE or INFINITE", example = "FINITE")
|
||||
private String executionMode;
|
||||
|
||||
@Schema(description = "固定轮次时必填", example = "3")
|
||||
private Integer targetRounds;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.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("biz_price_track_loop_run")
|
||||
public class PriceTrackLoopRunEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String status;
|
||||
private String executionMode;
|
||||
private Integer targetRounds;
|
||||
private Integer currentRound;
|
||||
private Integer currentShopIndex;
|
||||
private Boolean statusMode;
|
||||
private Boolean asinMode;
|
||||
private String asinFilesJson;
|
||||
private String countryCodesJson;
|
||||
private String shopsJson;
|
||||
private Long activeTaskId;
|
||||
private Boolean stopRequested;
|
||||
private String errorMessage;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime finishedAt;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class PriceTrackCreateTaskRequestVo {
|
||||
private Long userId;
|
||||
private boolean statusMode;
|
||||
private boolean asinMode;
|
||||
private List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> items = new ArrayList<>();
|
||||
private List<String> asinFiles = new ArrayList<>();
|
||||
private List<String> countryCodes = new ArrayList<>();
|
||||
private Long loopRunId;
|
||||
private Integer roundIndex;
|
||||
private Integer shopIndex;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "循环任务下一条派发信息")
|
||||
public class PriceTrackLoopRunDispatchVo {
|
||||
|
||||
private PriceTrackLoopRunVo loopRun;
|
||||
|
||||
private PriceTrackCreateTaskRequestVo childTaskRequest;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "跟价循环任务")
|
||||
public class PriceTrackLoopRunVo {
|
||||
|
||||
private Long id;
|
||||
private String status;
|
||||
private String executionMode;
|
||||
private Integer targetRounds;
|
||||
private Integer currentRound;
|
||||
private Integer currentShopIndex;
|
||||
private Integer totalShopCount;
|
||||
private Long activeTaskId;
|
||||
private String activeTaskStatus;
|
||||
private Boolean stopRequested;
|
||||
private String errorMessage;
|
||||
private Boolean statusMode;
|
||||
private Boolean asinMode;
|
||||
private List<String> asinFiles = new ArrayList<>();
|
||||
private List<String> countryCodes = new ArrayList<>();
|
||||
}
|
||||
@@ -49,4 +49,10 @@ public class PriceTrackResultItemVo {
|
||||
|
||||
@Schema(description = "预签名下载 URL,列表可能带回,也可通过下载接口获取")
|
||||
private String downloadUrl;
|
||||
|
||||
@Schema(description = "所属循环任务 ID", example = "1")
|
||||
private Long loopRunId;
|
||||
|
||||
@Schema(description = "所属轮次,1-based", example = "2")
|
||||
private Integer roundIndex;
|
||||
}
|
||||
|
||||
@@ -26,4 +26,10 @@ public class PriceTrackTaskItemVo {
|
||||
|
||||
@Schema(description = "完成时间;未结束时为空")
|
||||
private String finishedAt;
|
||||
|
||||
@Schema(description = "所属循环任务 ID", example = "1")
|
||||
private Long loopRunId;
|
||||
|
||||
@Schema(description = "所属轮次,1-based", example = "2")
|
||||
private Integer roundIndex;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
package com.nanri.aiimage.modules.pricetrack.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.modules.pricetrack.mapper.PriceTrackLoopRunMapper;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackLoopRunChildFinishedRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackLoopRunCreateRequest;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.entity.PriceTrackLoopRunEntity;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCreateTaskRequestVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackLoopRunDispatchVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackLoopRunVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PriceTrackLoopRunService {
|
||||
|
||||
private static final String MODULE_TYPE = "PRICE_TRACK";
|
||||
private static final String STATUS_RUNNING = "RUNNING";
|
||||
private static final String STATUS_SUCCESS = "SUCCESS";
|
||||
private static final String STATUS_FAILED = "FAILED";
|
||||
private static final String STATUS_STOPPED = "STOPPED";
|
||||
private static final String EXECUTION_MODE_FINITE = "FINITE";
|
||||
private static final String EXECUTION_MODE_INFINITE = "INFINITE";
|
||||
|
||||
private final PriceTrackLoopRunMapper loopRunMapper;
|
||||
private final FileTaskMapper fileTaskMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ZiniaoShopSwitchService ziniaoShopSwitchService;
|
||||
|
||||
@Transactional
|
||||
public PriceTrackLoopRunVo createLoopRun(PriceTrackLoopRunCreateRequest request) {
|
||||
validateCreateRequest(request);
|
||||
List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> items = dedupeItems(request.getItems());
|
||||
if (items.isEmpty()) {
|
||||
throw new BusinessException("items不能为空");
|
||||
}
|
||||
PriceTrackLoopRunEntity entity = new PriceTrackLoopRunEntity();
|
||||
entity.setUserId(request.getUserId());
|
||||
entity.setStatus(STATUS_RUNNING);
|
||||
entity.setExecutionMode(normalizeExecutionMode(request.getExecutionMode()));
|
||||
entity.setTargetRounds("FINITE".equals(normalizeExecutionMode(request.getExecutionMode())) ? request.getTargetRounds() : null);
|
||||
entity.setCurrentRound(1);
|
||||
entity.setCurrentShopIndex(0);
|
||||
entity.setStatusMode(request.isStatusMode());
|
||||
entity.setAsinMode(request.isAsinMode());
|
||||
entity.setActiveTaskId(null);
|
||||
entity.setStopRequested(false);
|
||||
entity.setErrorMessage(null);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
try {
|
||||
entity.setAsinFilesJson(objectMapper.writeValueAsString(request.getAsinFiles() == null ? List.of() : request.getAsinFiles()));
|
||||
entity.setCountryCodesJson(objectMapper.writeValueAsString(request.getCountryCodes()));
|
||||
entity.setShopsJson(objectMapper.writeValueAsString(items));
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("循环任务序列化失败");
|
||||
}
|
||||
loopRunMapper.insert(entity);
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
public PriceTrackLoopRunVo getLoopRun(Long loopRunId, Long userId) {
|
||||
PriceTrackLoopRunEntity entity = getOwnedLoopRun(loopRunId, userId);
|
||||
reconcileWithTerminalChild(entity);
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PriceTrackLoopRunDispatchVo dispatchNext(Long loopRunId, Long userId) {
|
||||
PriceTrackLoopRunEntity entity = getOwnedLoopRun(loopRunId, userId);
|
||||
reconcileWithTerminalChild(entity);
|
||||
if (isTerminal(entity.getStatus())) {
|
||||
PriceTrackLoopRunDispatchVo vo = new PriceTrackLoopRunDispatchVo();
|
||||
vo.setLoopRun(toVo(entity));
|
||||
vo.setChildTaskRequest(null);
|
||||
return vo;
|
||||
}
|
||||
if (Boolean.TRUE.equals(entity.getStopRequested()) && entity.getActiveTaskId() == null) {
|
||||
markStopped(entity, null);
|
||||
PriceTrackLoopRunDispatchVo vo = new PriceTrackLoopRunDispatchVo();
|
||||
vo.setLoopRun(toVo(entity));
|
||||
vo.setChildTaskRequest(null);
|
||||
return vo;
|
||||
}
|
||||
if (entity.getActiveTaskId() != null) {
|
||||
throw new BusinessException("当前仍有执行中的子任务");
|
||||
}
|
||||
List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> items = parseShops(entity);
|
||||
if (items.isEmpty()) {
|
||||
throw new BusinessException("循环任务缺少店铺配置");
|
||||
}
|
||||
int shopIndex = Math.max(0, entity.getCurrentShopIndex() == null ? 0 : entity.getCurrentShopIndex());
|
||||
if (shopIndex >= items.size()) {
|
||||
advanceAfterSuccessfulRound(entity, items.size());
|
||||
if (isTerminal(entity.getStatus())) {
|
||||
PriceTrackLoopRunDispatchVo vo = new PriceTrackLoopRunDispatchVo();
|
||||
vo.setLoopRun(toVo(entity));
|
||||
vo.setChildTaskRequest(null);
|
||||
return vo;
|
||||
}
|
||||
shopIndex = Math.max(0, entity.getCurrentShopIndex() == null ? 0 : entity.getCurrentShopIndex());
|
||||
}
|
||||
PriceTrackCreateTaskRequestVo childRequest = new PriceTrackCreateTaskRequestVo();
|
||||
childRequest.setUserId(entity.getUserId());
|
||||
childRequest.setStatusMode(Boolean.TRUE.equals(entity.getStatusMode()));
|
||||
childRequest.setAsinMode(Boolean.TRUE.equals(entity.getAsinMode()));
|
||||
childRequest.setAsinFiles(parseStringList(entity.getAsinFilesJson()));
|
||||
childRequest.setCountryCodes(parseStringList(entity.getCountryCodesJson()));
|
||||
childRequest.setItems(List.of(items.get(shopIndex)));
|
||||
childRequest.setLoopRunId(entity.getId());
|
||||
childRequest.setRoundIndex(Math.max(1, entity.getCurrentRound() == null ? 1 : entity.getCurrentRound()));
|
||||
childRequest.setShopIndex(shopIndex);
|
||||
|
||||
PriceTrackLoopRunDispatchVo vo = new PriceTrackLoopRunDispatchVo();
|
||||
vo.setLoopRun(toVo(entity));
|
||||
vo.setChildTaskRequest(childRequest);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PriceTrackLoopRunVo childFinished(Long loopRunId, Long userId, PriceTrackLoopRunChildFinishedRequest request) {
|
||||
PriceTrackLoopRunEntity entity = getOwnedLoopRun(loopRunId, userId);
|
||||
handleChildFinished(entity, request.getChildTaskId(), true);
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PriceTrackLoopRunVo requestStop(Long loopRunId, Long userId) {
|
||||
PriceTrackLoopRunEntity entity = getOwnedLoopRun(loopRunId, userId);
|
||||
entity.setStopRequested(true);
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
if (entity.getActiveTaskId() == null || isChildTaskTerminal(entity.getActiveTaskId())) {
|
||||
markStopped(entity, null);
|
||||
} else {
|
||||
loopRunMapper.updateById(entity);
|
||||
}
|
||||
return toVo(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void bindChildTask(Long loopRunId, Long childTaskId, Integer roundIndex, Integer shopIndex) {
|
||||
if (loopRunId == null || childTaskId == null) {
|
||||
return;
|
||||
}
|
||||
PriceTrackLoopRunEntity entity = loopRunMapper.selectById(loopRunId);
|
||||
if (entity == null || isTerminal(entity.getStatus())) {
|
||||
return;
|
||||
}
|
||||
if (entity.getCurrentRound() == null || entity.getCurrentShopIndex() == null) {
|
||||
return;
|
||||
}
|
||||
if (!entity.getCurrentRound().equals(roundIndex) || !entity.getCurrentShopIndex().equals(shopIndex)) {
|
||||
return;
|
||||
}
|
||||
if (entity.getActiveTaskId() != null && !isChildTaskTerminal(entity.getActiveTaskId())) {
|
||||
return;
|
||||
}
|
||||
entity.setActiveTaskId(childTaskId);
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
loopRunMapper.updateById(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void syncLoopRunAfterChildTerminal(Long childTaskId) {
|
||||
if (childTaskId == null || childTaskId <= 0) {
|
||||
return;
|
||||
}
|
||||
List<PriceTrackLoopRunEntity> loops = loopRunMapper.selectList(new LambdaQueryWrapper<PriceTrackLoopRunEntity>()
|
||||
.eq(PriceTrackLoopRunEntity::getActiveTaskId, childTaskId)
|
||||
.last("limit 5"));
|
||||
for (PriceTrackLoopRunEntity entity : loops) {
|
||||
handleChildFinished(entity, childTaskId, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void syncLoopRunAfterChildRemoved(Long childTaskId) {
|
||||
if (childTaskId == null || childTaskId <= 0) {
|
||||
return;
|
||||
}
|
||||
List<PriceTrackLoopRunEntity> loops = loopRunMapper.selectList(new LambdaQueryWrapper<PriceTrackLoopRunEntity>()
|
||||
.eq(PriceTrackLoopRunEntity::getActiveTaskId, childTaskId)
|
||||
.last("limit 5"));
|
||||
for (PriceTrackLoopRunEntity entity : loops) {
|
||||
if (entity == null || isTerminal(entity.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
entity.setActiveTaskId(null);
|
||||
entity.setStatus(STATUS_STOPPED);
|
||||
entity.setStopRequested(true);
|
||||
entity.setErrorMessage("当前循环子任务已被删除,循环已停止");
|
||||
entity.setFinishedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
loopRunMapper.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChildFinished(PriceTrackLoopRunEntity entity, Long childTaskId, boolean validateOwnership) {
|
||||
if (entity == null || childTaskId == null || childTaskId <= 0 || isTerminal(entity.getStatus())) {
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(childTaskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("子任务不存在");
|
||||
}
|
||||
if (validateOwnership && !entity.getUserId().equals(task.getUserId())) {
|
||||
throw new BusinessException("子任务不属于当前用户");
|
||||
}
|
||||
if (!isTerminal(task.getStatus())) {
|
||||
throw new BusinessException("子任务尚未结束");
|
||||
}
|
||||
if (!belongsToLoop(task, entity.getId())) {
|
||||
throw new BusinessException("子任务不属于当前循环");
|
||||
}
|
||||
if (entity.getActiveTaskId() != null && !entity.getActiveTaskId().equals(childTaskId)) {
|
||||
return;
|
||||
}
|
||||
if (entity.getActiveTaskId() == null) {
|
||||
ChildTaskContext ctx = parseChildTaskContext(task);
|
||||
if (ctx == null
|
||||
|| !ctx.roundIndex().equals(entity.getCurrentRound())
|
||||
|| !ctx.shopIndex().equals(entity.getCurrentShopIndex())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
entity.setActiveTaskId(null);
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
if (STATUS_FAILED.equals(task.getStatus())) {
|
||||
entity.setStatus(STATUS_FAILED);
|
||||
entity.setErrorMessage(task.getErrorMessage() == null || task.getErrorMessage().isBlank()
|
||||
? "子任务执行失败"
|
||||
: task.getErrorMessage());
|
||||
entity.setFinishedAt(LocalDateTime.now());
|
||||
loopRunMapper.updateById(entity);
|
||||
return;
|
||||
}
|
||||
List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> items = parseShops(entity);
|
||||
if (items.isEmpty()) {
|
||||
entity.setStatus(STATUS_FAILED);
|
||||
entity.setErrorMessage("循环任务缺少店铺配置");
|
||||
entity.setFinishedAt(LocalDateTime.now());
|
||||
loopRunMapper.updateById(entity);
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(entity.getStopRequested())) {
|
||||
markStopped(entity, null);
|
||||
return;
|
||||
}
|
||||
int nextShopIndex = (entity.getCurrentShopIndex() == null ? 0 : entity.getCurrentShopIndex()) + 1;
|
||||
if (nextShopIndex < items.size()) {
|
||||
entity.setCurrentShopIndex(nextShopIndex);
|
||||
loopRunMapper.updateById(entity);
|
||||
return;
|
||||
}
|
||||
advanceAfterSuccessfulRound(entity, items.size());
|
||||
}
|
||||
|
||||
private void reconcileWithTerminalChild(PriceTrackLoopRunEntity entity) {
|
||||
if (entity == null || entity.getActiveTaskId() == null || isTerminal(entity.getStatus())) {
|
||||
return;
|
||||
}
|
||||
if (isChildTaskTerminal(entity.getActiveTaskId())) {
|
||||
handleChildFinished(entity, entity.getActiveTaskId(), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void advanceAfterSuccessfulRound(PriceTrackLoopRunEntity entity, int shopCount) {
|
||||
if (Boolean.TRUE.equals(entity.getStopRequested())) {
|
||||
markStopped(entity, null);
|
||||
return;
|
||||
}
|
||||
if (EXECUTION_MODE_FINITE.equals(entity.getExecutionMode())) {
|
||||
int targetRounds = entity.getTargetRounds() == null ? 1 : Math.max(1, entity.getTargetRounds());
|
||||
int currentRound = entity.getCurrentRound() == null ? 1 : Math.max(1, entity.getCurrentRound());
|
||||
if (currentRound >= targetRounds) {
|
||||
entity.setStatus(STATUS_SUCCESS);
|
||||
entity.setFinishedAt(LocalDateTime.now());
|
||||
} else {
|
||||
entity.setCurrentRound(currentRound + 1);
|
||||
entity.setCurrentShopIndex(0);
|
||||
}
|
||||
} else {
|
||||
int currentRound = entity.getCurrentRound() == null ? 1 : Math.max(1, entity.getCurrentRound());
|
||||
entity.setCurrentRound(currentRound + 1);
|
||||
entity.setCurrentShopIndex(0);
|
||||
}
|
||||
entity.setActiveTaskId(null);
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
loopRunMapper.updateById(entity);
|
||||
}
|
||||
|
||||
private void markStopped(PriceTrackLoopRunEntity entity, String message) {
|
||||
entity.setStatus(STATUS_STOPPED);
|
||||
entity.setActiveTaskId(null);
|
||||
entity.setErrorMessage(message);
|
||||
entity.setFinishedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
loopRunMapper.updateById(entity);
|
||||
}
|
||||
|
||||
private boolean isChildTaskTerminal(Long taskId) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
return task != null && isTerminal(task.getStatus());
|
||||
}
|
||||
|
||||
private boolean belongsToLoop(FileTaskEntity task, Long loopRunId) {
|
||||
if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> payload = objectMapper.readValue(task.getRequestJson(), new TypeReference<Map<String, Object>>() {});
|
||||
Object raw = payload.get("loopRunId");
|
||||
if (raw instanceof Number number) {
|
||||
return loopRunId.equals(number.longValue());
|
||||
}
|
||||
if (raw instanceof String text && !text.isBlank()) {
|
||||
return loopRunId.equals(Long.parseLong(text));
|
||||
}
|
||||
return false;
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-loop] parse child loopRunId failed taskId={} msg={}", task.getId(), ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private PriceTrackLoopRunEntity getOwnedLoopRun(Long loopRunId, Long userId) {
|
||||
if (loopRunId == null || loopRunId <= 0) {
|
||||
throw new BusinessException("loopRunId不合法");
|
||||
}
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new BusinessException("user_id不合法");
|
||||
}
|
||||
PriceTrackLoopRunEntity entity = loopRunMapper.selectById(loopRunId);
|
||||
if (entity == null || !userId.equals(entity.getUserId())) {
|
||||
throw new BusinessException("循环任务不存在");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void validateCreateRequest(PriceTrackLoopRunCreateRequest request) {
|
||||
if (request == null) {
|
||||
throw new BusinessException("请求不能为空");
|
||||
}
|
||||
if (request.isStatusMode() == request.isAsinMode()) {
|
||||
throw new BusinessException("跟价模式必须二选一");
|
||||
}
|
||||
if (request.isAsinMode() && (request.getAsinFiles() == null || request.getAsinFiles().isEmpty())) {
|
||||
throw new BusinessException("自定义ASIN模式必须上传ASIN文件");
|
||||
}
|
||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
||||
throw new BusinessException("user_id不合法");
|
||||
}
|
||||
String executionMode = normalizeExecutionMode(request.getExecutionMode());
|
||||
if (EXECUTION_MODE_FINITE.equals(executionMode) && (request.getTargetRounds() == null || request.getTargetRounds() <= 0)) {
|
||||
throw new BusinessException("固定轮次必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeExecutionMode(String raw) {
|
||||
String value = raw == null ? "" : raw.trim().toUpperCase();
|
||||
if (EXECUTION_MODE_FINITE.equals(value) || EXECUTION_MODE_INFINITE.equals(value)) {
|
||||
return value;
|
||||
}
|
||||
throw new BusinessException("executionMode不合法");
|
||||
}
|
||||
|
||||
private List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> dedupeItems(List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> rawItems) {
|
||||
Map<String, PriceTrackMatchShopsVo.PriceTrackShopQueueItem> out = new LinkedHashMap<>();
|
||||
for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : rawItems) {
|
||||
if (item == null || !Boolean.TRUE.equals(item.getMatched())) {
|
||||
continue;
|
||||
}
|
||||
String key = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
||||
if (!key.isBlank()) {
|
||||
out.putIfAbsent(key, item);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(out.values());
|
||||
}
|
||||
|
||||
private PriceTrackLoopRunVo toVo(PriceTrackLoopRunEntity entity) {
|
||||
PriceTrackLoopRunVo vo = new PriceTrackLoopRunVo();
|
||||
vo.setId(entity.getId());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setExecutionMode(entity.getExecutionMode());
|
||||
vo.setTargetRounds(entity.getTargetRounds());
|
||||
vo.setCurrentRound(entity.getCurrentRound());
|
||||
vo.setCurrentShopIndex(entity.getCurrentShopIndex());
|
||||
vo.setTotalShopCount(parseShops(entity).size());
|
||||
vo.setActiveTaskId(entity.getActiveTaskId());
|
||||
vo.setStopRequested(Boolean.TRUE.equals(entity.getStopRequested()));
|
||||
vo.setErrorMessage(entity.getErrorMessage());
|
||||
vo.setStatusMode(Boolean.TRUE.equals(entity.getStatusMode()));
|
||||
vo.setAsinMode(Boolean.TRUE.equals(entity.getAsinMode()));
|
||||
vo.setAsinFiles(parseStringList(entity.getAsinFilesJson()));
|
||||
vo.setCountryCodes(parseStringList(entity.getCountryCodesJson()));
|
||||
if (entity.getActiveTaskId() != null) {
|
||||
FileTaskEntity activeTask = fileTaskMapper.selectById(entity.getActiveTaskId());
|
||||
if (activeTask != null) {
|
||||
vo.setActiveTaskStatus(activeTask.getStatus());
|
||||
}
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem> parseShops(PriceTrackLoopRunEntity entity) {
|
||||
if (entity == null || entity.getShopsJson() == null || entity.getShopsJson().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(entity.getShopsJson(), new TypeReference<List<PriceTrackMatchShopsVo.PriceTrackShopQueueItem>>() {});
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-loop] parse shops failed loopRunId={} msg={}", entity.getId(), ex.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> parseStringList(String rawJson) {
|
||||
if (rawJson == null || rawJson.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(rawJson, new TypeReference<List<String>>() {});
|
||||
} catch (Exception ex) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private ChildTaskContext parseChildTaskContext(FileTaskEntity task) {
|
||||
if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> payload = objectMapper.readValue(task.getRequestJson(), new TypeReference<Map<String, Object>>() {});
|
||||
Integer roundIndex = toInteger(payload.get("roundIndex"));
|
||||
Integer shopIndex = toInteger(payload.get("shopIndex"));
|
||||
if (roundIndex == null || shopIndex == null) {
|
||||
return null;
|
||||
}
|
||||
return new ChildTaskContext(roundIndex, shopIndex);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTerminal(String status) {
|
||||
return STATUS_SUCCESS.equals(status) || STATUS_FAILED.equals(status) || STATUS_STOPPED.equals(status);
|
||||
}
|
||||
|
||||
private Integer toInteger(Object raw) {
|
||||
if (raw instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (raw instanceof String text && !text.isBlank()) {
|
||||
return Integer.parseInt(text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record ChildTaskContext(Integer roundIndex, Integer shopIndex) {
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ public class PriceTrackTaskService {
|
||||
private final SkipPriceAsinService skipPriceAsinService;
|
||||
private final PriceTrackExcelAssemblyService excelAssemblyService;
|
||||
private final PriceTrackTaskCacheService priceTrackTaskCacheService;
|
||||
private final PriceTrackLoopRunService priceTrackLoopRunService;
|
||||
|
||||
public PriceTrackDashboardVo dashboard(Long userId) {
|
||||
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");
|
||||
@@ -142,6 +143,7 @@ public class PriceTrackTaskService {
|
||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE));
|
||||
fileTaskMapper.deleteById(taskId);
|
||||
priceTrackTaskCacheService.deleteTaskCache(taskId);
|
||||
priceTrackLoopRunService.syncLoopRunAfterChildRemoved(taskId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -253,6 +255,9 @@ public class PriceTrackTaskService {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.insert(task);
|
||||
priceTrackTaskCacheService.touchTaskHeartbeat(task.getId());
|
||||
if (request.getLoopRunId() != null) {
|
||||
priceTrackLoopRunService.bindChildTask(request.getLoopRunId(), task.getId(), request.getRoundIndex(), request.getShopIndex());
|
||||
}
|
||||
|
||||
List<PriceTrackResultItemVo> snapshot = new ArrayList<>();
|
||||
for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : uniqueItems) {
|
||||
@@ -267,7 +272,10 @@ public class PriceTrackTaskService {
|
||||
fr.setSuccess(0);
|
||||
fr.setCreatedAt(LocalDateTime.now());
|
||||
fileResultMapper.insert(fr);
|
||||
snapshot.add(toSnapshotVo(fr, item, task.getStatus()));
|
||||
PriceTrackResultItemVo snapshotItem = toSnapshotVo(fr, item, task.getStatus());
|
||||
snapshotItem.setLoopRunId(request.getLoopRunId());
|
||||
snapshotItem.setRoundIndex(request.getRoundIndex());
|
||||
snapshot.add(snapshotItem);
|
||||
}
|
||||
|
||||
// 保存请求上下文快照,供 Python 消费和前端轮询展示使用
|
||||
@@ -280,6 +288,9 @@ public class PriceTrackTaskService {
|
||||
ctx.put("skipAsinsByCountry", skipAsinsByCountry);
|
||||
ctx.put("asinRowsByCountry", asinRowsByCountry);
|
||||
ctx.put("items", uniqueItems);
|
||||
ctx.put("loopRunId", request.getLoopRunId());
|
||||
ctx.put("roundIndex", request.getRoundIndex());
|
||||
ctx.put("shopIndex", request.getShopIndex());
|
||||
task.setRequestJson(objectMapper.writeValueAsString(ctx));
|
||||
task.setResultJson(objectMapper.writeValueAsString(snapshot));
|
||||
} catch (Exception ex) {
|
||||
@@ -441,6 +452,7 @@ public class PriceTrackTaskService {
|
||||
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(FileResultEntity::getId));
|
||||
if (latest.isEmpty()) {
|
||||
priceTrackLoopRunService.syncLoopRunAfterChildRemoved(taskId);
|
||||
fileTaskMapper.deleteById(taskId);
|
||||
cleanupTaskCacheIfTerminal(taskId, "DELETE_EMPTY");
|
||||
return;
|
||||
@@ -768,6 +780,7 @@ public class PriceTrackTaskService {
|
||||
vo.setCreatedAt(fmt(task.getCreatedAt()));
|
||||
vo.setUpdatedAt(fmt(task.getUpdatedAt()));
|
||||
vo.setFinishedAt(fmt(task.getFinishedAt()));
|
||||
mergeTaskLoopFields(vo, task);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -794,10 +807,34 @@ public class PriceTrackTaskService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
mergeLoopFields(payload, vo);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeTaskLoopFields(PriceTrackTaskItemVo vo, FileTaskEntity task) {
|
||||
if (task == null || task.getRequestJson() == null || task.getRequestJson().isBlank()) return;
|
||||
try {
|
||||
Map<String, Object> payload = objectMapper.readValue(task.getRequestJson(), new TypeReference<Map<String, Object>>() {});
|
||||
Object rawLoopRunId = payload.get("loopRunId");
|
||||
if (rawLoopRunId instanceof Number n) vo.setLoopRunId(n.longValue());
|
||||
else if (rawLoopRunId instanceof String s && !s.isBlank()) vo.setLoopRunId(Long.parseLong(s));
|
||||
Object rawRoundIndex = payload.get("roundIndex");
|
||||
if (rawRoundIndex instanceof Number n) vo.setRoundIndex(n.intValue());
|
||||
else if (rawRoundIndex instanceof String s && !s.isBlank()) vo.setRoundIndex(Integer.parseInt(s));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeLoopFields(Map<String, Object> payload, PriceTrackResultItemVo vo) {
|
||||
Object rawLoopRunId = payload.get("loopRunId");
|
||||
if (rawLoopRunId instanceof Number n) vo.setLoopRunId(n.longValue());
|
||||
else if (rawLoopRunId instanceof String s && !s.isBlank()) vo.setLoopRunId(Long.parseLong(s));
|
||||
Object rawRoundIndex = payload.get("roundIndex");
|
||||
if (rawRoundIndex instanceof Number n) vo.setRoundIndex(n.intValue());
|
||||
else if (rawRoundIndex instanceof String s && !s.isBlank()) vo.setRoundIndex(Integer.parseInt(s));
|
||||
}
|
||||
|
||||
private List<PriceTrackResultItemVo> buildSnapshotFromDb(Long taskId, String taskStatus) {
|
||||
List<FileResultEntity> rows = fileResultMapper.selectList(
|
||||
new LambdaQueryWrapper<FileResultEntity>()
|
||||
@@ -867,6 +904,9 @@ public class PriceTrackTaskService {
|
||||
log.warn("[price-track] status updated from latest rows taskId={} oldStatus={} newStatus={} ok={} fail={} allDone={} errorMessage={}",
|
||||
task.getId(), oldStatus, task.getStatus(), ok, fail, allDone, task.getErrorMessage());
|
||||
cleanupTaskCacheIfTerminal(task.getId(), task.getStatus());
|
||||
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
|
||||
priceTrackLoopRunService.syncLoopRunAfterChildTerminal(task.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupTaskCacheIfTerminal(Long taskId, String taskStatus) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS biz_price_track_loop_run (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'primary key',
|
||||
user_id BIGINT NOT NULL COMMENT 'user id',
|
||||
status VARCHAR(32) NOT NULL COMMENT 'PENDING/RUNNING/SUCCESS/FAILED/STOPPED',
|
||||
execution_mode VARCHAR(16) NOT NULL COMMENT 'FINITE/INFINITE',
|
||||
target_rounds INT NULL COMMENT 'configured rounds for finite mode',
|
||||
current_round INT NOT NULL DEFAULT 1 COMMENT '1-based current round',
|
||||
current_shop_index INT NOT NULL DEFAULT 0 COMMENT '0-based current shop index',
|
||||
status_mode TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'status mode enabled',
|
||||
asin_mode TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'asin mode enabled',
|
||||
asin_files_json MEDIUMTEXT NULL COMMENT 'asin file paths json',
|
||||
country_codes_json MEDIUMTEXT NOT NULL COMMENT 'country codes json',
|
||||
shops_json MEDIUMTEXT NOT NULL COMMENT 'shop queue items json',
|
||||
active_task_id BIGINT NULL COMMENT 'currently running child task id',
|
||||
stop_requested TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'manual stop requested',
|
||||
error_message VARCHAR(1000) NULL COMMENT 'terminal error message',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'updated time',
|
||||
finished_at DATETIME NULL COMMENT 'finished time',
|
||||
KEY idx_user_status (user_id, status),
|
||||
KEY idx_active_task_id (active_task_id)
|
||||
) COMMENT='price track loop runs';
|
||||
Reference in New Issue
Block a user