后台管理接口修复 后端BUG修复
This commit is contained in:
59
backend-java/docs/baota-start-commands.md
Normal file
59
backend-java/docs/baota-start-commands.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Multi-Server Start Commands
|
||||
|
||||
Use the same packaged jar on both servers. Do not switch code comments before packaging.
|
||||
|
||||
Requirements:
|
||||
- Build once: `mvn clean package -DskipTests`
|
||||
- Use profile: `server`
|
||||
- Put JVM options before `-jar`
|
||||
- Pass `AIIMAGE_INSTANCE_ID` and Redis address from the startup command
|
||||
- Result file job MQ is expected to stay enabled in packaged `server` deployments
|
||||
|
||||
1Panel server 121:
|
||||
|
||||
Environment variables:
|
||||
|
||||
```env
|
||||
SPRING_PROFILES_ACTIVE=server
|
||||
AIIMAGE_INSTANCE_ID=server-121
|
||||
AIIMAGE_REDIS_HOST=192.168.0.172
|
||||
AIIMAGE_REDIS_PORT=16379
|
||||
AIIMAGE_REDIS_PASSWORD=B6COTcY094TYe545
|
||||
AIIMAGE_REDIS_DATABASE=0
|
||||
AIIMAGE_SERVER_PORT=18080
|
||||
AIIMAGE_STORAGE_LOCAL_TEMP_DIR=/app/data/tmp
|
||||
AIIMAGE_RESULT_FILE_JOB_MQ_ENABLED=true
|
||||
AIIMAGE_ROCKETMQ_NAME_SERVER=121.196.149.225:9876
|
||||
```
|
||||
|
||||
Recommended startup command:
|
||||
|
||||
```bash
|
||||
java -Xmx2048M -Xms2048M -jar /app/aiimage-backend-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
Baota server 111:
|
||||
|
||||
```bash
|
||||
/www/server/java/jdk-21.0.2/bin/java -Xmx1024M -Xms256M -jar /app/java/aiimage-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=server --aiimage.instance-id=server-111 --spring.data.redis.host=47.111.163.154 --spring.data.redis.port=16379 --spring.data.redis.password=B6COTcY094TYe545 --server.port=18080 --aiimage.storage.local-temp-dir=/app/data/tmp
|
||||
```
|
||||
|
||||
Optional Baota server 121 command:
|
||||
|
||||
```bash
|
||||
/www/server/java/jdk-21.0.2/bin/java -Xmx1024M -Xms256M -jar /app/java/aiimage-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=server --aiimage.instance-id=server-121 --spring.data.redis.host=192.168.0.172 --spring.data.redis.port=16379 --spring.data.redis.password=B6COTcY094TYe545 --server.port=18080 --aiimage.storage.local-temp-dir=/app/data/tmp
|
||||
```
|
||||
|
||||
Successful startup should show a line similar to:
|
||||
|
||||
```text
|
||||
[instance] resolved instanceId=server-121 source=configured
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
[instance] resolved instanceId=server-111 source=configured
|
||||
```
|
||||
|
||||
If the instanceId in the log does not match the startup command, the actual process was not started with the expected command.
|
||||
@@ -1,21 +1,38 @@
|
||||
package com.nanri.aiimage.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class InstanceMetadata {
|
||||
|
||||
private final String instanceId;
|
||||
private final String hostname;
|
||||
private final String source;
|
||||
private final boolean stable;
|
||||
|
||||
public InstanceMetadata(@Value("${aiimage.instance-id:}") String configuredInstanceId) {
|
||||
this.hostname = resolveHostname();
|
||||
this.instanceId = configuredInstanceId == null || configuredInstanceId.isBlank()
|
||||
? this.hostname
|
||||
: configuredInstanceId.trim();
|
||||
String normalizedConfiguredInstanceId = configuredInstanceId == null ? "" : configuredInstanceId.trim();
|
||||
if (!normalizedConfiguredInstanceId.isBlank()) {
|
||||
this.instanceId = normalizedConfiguredInstanceId;
|
||||
this.source = "configured";
|
||||
this.stable = true;
|
||||
log.info("[instance] resolved instanceId={} source={} hostname={} continuityRisk=LOW",
|
||||
this.instanceId, this.source, this.hostname);
|
||||
} else {
|
||||
this.instanceId = this.hostname;
|
||||
this.source = detectFallbackSource(this.hostname);
|
||||
this.stable = false;
|
||||
log.warn("[instance] aiimage.instance-id is not configured, falling back to {}={} as instanceId. "
|
||||
+ "This may change after restart/redeploy and break task owner continuity. "
|
||||
+ "Set AIIMAGE_INSTANCE_ID in 1Panel/Docker env, or set -Daiimage.instance-id=<stable-id> in IDEA/local startup.",
|
||||
this.source, this.hostname);
|
||||
}
|
||||
}
|
||||
|
||||
public String getInstanceId() {
|
||||
@@ -26,6 +43,14 @@ public class InstanceMetadata {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public boolean isStable() {
|
||||
return stable;
|
||||
}
|
||||
|
||||
private static String resolveHostname() {
|
||||
try {
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
@@ -34,4 +59,12 @@ public class InstanceMetadata {
|
||||
return (envHostname == null || envHostname.isBlank()) ? "unknown-host" : envHostname;
|
||||
}
|
||||
}
|
||||
|
||||
private static String detectFallbackSource(String hostname) {
|
||||
String envHostname = System.getenv("HOSTNAME");
|
||||
if (envHostname != null && !envHostname.isBlank() && envHostname.equalsIgnoreCase(hostname)) {
|
||||
return "env.HOSTNAME";
|
||||
}
|
||||
return "os.hostname";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +43,18 @@ public class RequestTraceFilter extends OncePerRequestFilter {
|
||||
|
||||
response.setHeader("X-AIIMAGE-Instance", instanceMetadata.getInstanceId());
|
||||
response.setHeader("X-AIIMAGE-Host", instanceMetadata.getHostname());
|
||||
response.setHeader("X-AIIMAGE-Instance-Source", instanceMetadata.getSource());
|
||||
response.setHeader("X-AIIMAGE-Instance-Stable", String.valueOf(instanceMetadata.isStable()));
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
long costMs = System.currentTimeMillis() - start;
|
||||
log.info(
|
||||
"request-trace instance={} host={} method={} uri={} status={} remote={} forwardedHost={} forwardedProto={} forwardedPort={} requestId={} userAgent={} costMs={}",
|
||||
"request-trace instance={} source={} stable={} host={} method={} uri={} status={} remote={} forwardedHost={} forwardedProto={} forwardedPort={} requestId={} userAgent={} costMs={}",
|
||||
instanceMetadata.getInstanceId(),
|
||||
instanceMetadata.getSource(),
|
||||
instanceMetadata.isStable(),
|
||||
instanceMetadata.getHostname(),
|
||||
request.getMethod(),
|
||||
request.getRequestURI(),
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.nanri.aiimage.config;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "aiimage.storage")
|
||||
public class StorageProperties {
|
||||
@@ -12,4 +14,25 @@ public class StorageProperties {
|
||||
private long sourceRetentionHours = 24;
|
||||
private long resultRetentionHours = 24;
|
||||
private long transientPayloadRetentionHours = 72;
|
||||
|
||||
public String getLocalTempDir() {
|
||||
String configured = localTempDir == null ? "" : localTempDir.trim();
|
||||
if (configured.isBlank()) {
|
||||
configured = "./data/tmp";
|
||||
}
|
||||
try {
|
||||
Path raw = Path.of(configured);
|
||||
if (raw.isAbsolute()) {
|
||||
return raw.normalize().toString();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Fall through to resolve against a stable base directory.
|
||||
}
|
||||
|
||||
String userDir = System.getProperty("user.dir");
|
||||
if (userDir == null || userDir.isBlank()) {
|
||||
userDir = System.getProperty("java.io.tmpdir", ".");
|
||||
}
|
||||
return Path.of(userDir).resolve(configured).normalize().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +355,9 @@ public class AppearancePatentCozeClient {
|
||||
text(firstNonNull(node.get("result"),
|
||||
firstNonNull(node.get("conclusion"),
|
||||
firstNonNull(itemNode.get("result"), itemNode.get("conclusion"))))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("status"), firstNonNull(node.get("row_status"), node.get("rowStatus"))),
|
||||
firstNonNull(itemNode.get("status"), firstNonNull(itemNode.get("row_status"), itemNode.get("rowStatus"))))),
|
||||
text(firstNonNull(node.get("title_reason"),
|
||||
firstNonNull(node.get("titleReason"),
|
||||
firstNonNull(itemNode.get("title_reason"), itemNode.get("titleReason"))))),
|
||||
@@ -465,6 +468,7 @@ public class AppearancePatentCozeClient {
|
||||
row.setAppearanceRisk(result.appearance());
|
||||
row.setPatentRisk(result.patent());
|
||||
row.setConclusion(result.result());
|
||||
row.setStatus(result.status());
|
||||
row.setTitleReason(result.titleReason());
|
||||
row.setAppearanceReason(result.appearanceReason());
|
||||
row.setPatentReason(result.patentReason());
|
||||
@@ -490,6 +494,7 @@ public class AppearancePatentCozeClient {
|
||||
row.setTitle(source.getTitle());
|
||||
row.setError(source.getError());
|
||||
row.setDone(source.getDone());
|
||||
row.setStatus(source.getStatus());
|
||||
row.setTitleRisk(source.getTitleRisk());
|
||||
row.setAppearanceRisk(source.getAppearanceRisk());
|
||||
row.setPatentRisk(source.getPatentRisk());
|
||||
@@ -942,6 +947,7 @@ public class AppearancePatentCozeClient {
|
||||
String appearance,
|
||||
String patent,
|
||||
String result,
|
||||
String status,
|
||||
String titleReason,
|
||||
String appearanceReason,
|
||||
String patentReason
|
||||
|
||||
@@ -48,6 +48,10 @@ public class AppearancePatentResultRowDto {
|
||||
@Schema(description = "单行完成标记。当前主要使用请求体顶层 done 控制任务收尾,该字段仅作兼容。", example = "true")
|
||||
private Boolean done;
|
||||
|
||||
@JsonAlias({"row_status", "rowStatus", "Status"})
|
||||
@Schema(description = "Coze row status", example = "success", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String status;
|
||||
|
||||
@Schema(description = "Java 调用 Coze 后生成的标题维度检测结果,对应最终 xlsx 的“标题维度(商标)”列。Python 回传请求中不要传该字段;即使传入,后端也会以 Java/Coze 处理结果为准。", example = "标题未发现明显商标侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String titleRisk;
|
||||
|
||||
|
||||
@@ -118,7 +118,8 @@ public class AppearancePatentTaskService {
|
||||
"标题维度(商标)",
|
||||
"外观维度(外观设计专利)",
|
||||
"专利维度(发明/实用新型专利)",
|
||||
"结论"
|
||||
"结论",
|
||||
"status"
|
||||
);
|
||||
|
||||
private final LocalFileStorageService localFileStorageService;
|
||||
@@ -424,7 +425,7 @@ public class AppearancePatentTaskService {
|
||||
completeSubmittedChunk(context);
|
||||
return null;
|
||||
});
|
||||
submitCozeForSubmittedChunk(context);
|
||||
scheduleCozePipelineForSubmittedChunk(context);
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
@@ -510,7 +511,7 @@ public class AppearancePatentTaskService {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
}
|
||||
submitCozeForSubmittedChunk(context);
|
||||
scheduleCozePipelineForSubmittedChunk(context);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -560,14 +561,9 @@ public class AppearancePatentTaskService {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||
.last("limit 50"));
|
||||
List<FileTaskEntity> tasks = listStaleFinalizeCandidates(threshold);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
if (isJavaSideProcessing(task.getId())) {
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
if (!isOwnerCurrent(ownerFromTask(task))) {
|
||||
continue;
|
||||
}
|
||||
long heartbeatMillis = taskCacheService.getTaskHeartbeatMillis(task.getId());
|
||||
@@ -589,14 +585,9 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||
.last("limit 50"));
|
||||
List<FileTaskEntity> tasks = listStaleFinalizeCandidates(threshold);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
if (isJavaSideProcessing(task.getId())) {
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
if (!isOwnerCurrent(ownerFromTask(task))) {
|
||||
continue;
|
||||
}
|
||||
long heartbeatMillis = taskCacheService.getTaskHeartbeatMillis(task.getId());
|
||||
@@ -608,14 +599,46 @@ public class AppearancePatentTaskService {
|
||||
continue;
|
||||
}
|
||||
try (lockHandle) {
|
||||
FileTaskEntity latestTask = fileTaskMapper.selectById(task.getId());
|
||||
if (latestTask != null) {
|
||||
finalizeTask(latestTask, "Python interrupted before uploading final appearance patent result", allRowCount(latestTask), true);
|
||||
}
|
||||
finalizeStaleTask(task.getId(), "Python interrupted before uploading final appearance patent result");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void debugFinalizeStaleTask(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return;
|
||||
}
|
||||
if (transactionManager != null) {
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, 0L);
|
||||
if (lockHandle == null) {
|
||||
return;
|
||||
}
|
||||
try (lockHandle) {
|
||||
inNewTransaction(() -> {
|
||||
finalizeStaleTask(taskId, "Python interrupted before uploading final appearance patent result");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, 0L);
|
||||
if (lockHandle == null) {
|
||||
return;
|
||||
}
|
||||
try (lockHandle) {
|
||||
finalizeStaleTask(taskId, "Python interrupted before uploading final appearance patent result");
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileTaskEntity> listStaleFinalizeCandidates(LocalDateTime threshold) {
|
||||
return fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getCreatedAt, threshold)
|
||||
.orderByAsc(FileTaskEntity::getCreatedAt)
|
||||
.last("limit 200"));
|
||||
}
|
||||
|
||||
private SubmitContext persistSubmittedChunk(Long taskId, AppearancePatentSubmitResultRequest request) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
@@ -699,17 +722,24 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
List<TaskChunkEntity> chunks = loadSubmittedChunks(task.getId());
|
||||
if (chunks.isEmpty()) {
|
||||
log.info("[appearance-patent] skip stale recovery coze submission because no submitted chunks remain taskId={}",
|
||||
task.getId());
|
||||
return;
|
||||
}
|
||||
FileResultEntity result = findOrCreateResultRecordForAssembly(task, allRowCount(task));
|
||||
if (result == null) {
|
||||
log.warn("[appearance-patent] stale recovery could not create result record taskId={}", task.getId());
|
||||
return;
|
||||
}
|
||||
TaskFileJobEntity job = taskFileJobService.enqueueAssembleResult(
|
||||
task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task.getId()));
|
||||
task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task));
|
||||
if (job == null || "SUCCESS".equals(job.getStatus())) {
|
||||
log.info("[appearance-patent] stale recovery skipped coze submission because result job unavailable taskId={} resultId={} jobStatus={}",
|
||||
task.getId(), result.getId(), job == null ? null : job.getStatus());
|
||||
return;
|
||||
}
|
||||
log.info("[appearance-patent] stale recovery created assemble job taskId={} resultId={} jobId={} jobStatus={} chunkCount={}",
|
||||
task.getId(), result.getId(), job.getId(), job.getStatus(), chunks.size());
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
boolean pendingCoze = submitCozeBatches(task, result, job, chunks, allRowsByBaseId);
|
||||
saveCozePipelineProgress(task, job);
|
||||
@@ -722,14 +752,127 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleCozePipelineForSubmittedChunk(SubmitContext context) {
|
||||
if (context == null || context.task() == null || context.task().getId() == null) {
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(context.task().getId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || STATUS_SUCCESS.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
List<TaskChunkEntity> chunks = loadSubmittedChunks(task.getId());
|
||||
if (chunks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
FileResultEntity result = findOrCreateResultRecordForAssembly(task, allRowCount(task));
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
TaskFileJobEntity job = taskFileJobService.enqueueAssembleResult(
|
||||
task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task));
|
||||
if (job == null || "SUCCESS".equals(job.getStatus())) {
|
||||
return;
|
||||
}
|
||||
taskFileJobService.requeue(job.getId(), "Appearance patent result uploaded, scheduling Coze/file assembly");
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
}
|
||||
|
||||
private void finalizeStaleTask(Long taskId, String error) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !STATUS_RUNNING.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
if (tryRecoverTimedOutPythonTask(task)) {
|
||||
return;
|
||||
}
|
||||
finalizeTask(task, error, allRowCount(task), true);
|
||||
}
|
||||
|
||||
private boolean tryRecoverTimedOutPythonTask(FileTaskEntity task) {
|
||||
if (task == null || task.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
Long taskId = task.getId();
|
||||
boolean uploadComplete = isResultSubmissionComplete(taskId);
|
||||
long pendingCozeStates = countPendingCozeStates(taskId);
|
||||
long activeAssembleJobs = taskFileJobService.countActiveAssembleJobs(taskId, MODULE_TYPE);
|
||||
log.info("[appearance-patent] stale recovery probe taskId={} uploadComplete={} pendingCozeStates={} activeAssembleJobs={} persistedRows={}",
|
||||
taskId, uploadComplete, pendingCozeStates, activeAssembleJobs, hasPersistedResultRows(taskId));
|
||||
if (uploadComplete && (pendingCozeStates > 0 || activeAssembleJobs > 0)) {
|
||||
touchJavaSideTaskActivity(taskId);
|
||||
return true;
|
||||
}
|
||||
if (!hasPersistedResultRows(taskId)) {
|
||||
log.info("[appearance-patent] stale recovery aborted because no persisted rows taskId={}", taskId);
|
||||
return false;
|
||||
}
|
||||
if (!uploadComplete) {
|
||||
int forcedScopes = markSubmissionCompleteOnPythonTimeout(taskId);
|
||||
if (forcedScopes <= 0) {
|
||||
log.warn("[appearance-patent] stale recovery aborted because submission could not be marked complete taskId={}",
|
||||
taskId);
|
||||
return false;
|
||||
}
|
||||
int pendingRows = collectPendingCozeCandidates(task, loadSubmittedChunks(taskId)).size();
|
||||
log.warn("[appearance-patent] python heartbeat timed out, forcing coze flush taskId={} pendingRows={} pendingCozeStates={} activeAssembleJobs={} forcedScopes={}",
|
||||
taskId, pendingRows, pendingCozeStates, activeAssembleJobs, forcedScopes);
|
||||
} else {
|
||||
log.warn("[appearance-patent] stale running task resuming coze/file assembly after python timeout taskId={} pendingCozeStates={} activeAssembleJobs={}",
|
||||
taskId, pendingCozeStates, activeAssembleJobs);
|
||||
}
|
||||
submitCozeForSubmittedChunk(new SubmitContext(task, null, null, 0, true, null));
|
||||
touchJavaSideTaskActivity(taskId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int markSubmissionCompleteOnPythonTimeout(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
List<TaskScopeStateEntity> inputStates = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.isNull(TaskScopeStateEntity::getCozeStatus)
|
||||
.orderByDesc(TaskScopeStateEntity::getUpdatedAt));
|
||||
if (inputStates == null || inputStates.isEmpty()) {
|
||||
TaskChunkEntity latestChunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByDesc(TaskChunkEntity::getUpdatedAt)
|
||||
.last("limit 1"));
|
||||
if (latestChunk == null || latestChunk.getScopeHash() == null || latestChunk.getScopeHash().isBlank()) {
|
||||
log.warn("[appearance-patent] stale recovery found no latest chunk to complete taskId={}", taskId);
|
||||
return 0;
|
||||
}
|
||||
log.info("[appearance-patent] stale recovery recreating input scope from chunk taskId={} scopeKey={} scopeHash={} chunkTotal={}",
|
||||
taskId, latestChunk.getScopeKey(), latestChunk.getScopeHash(), latestChunk.getChunkTotal());
|
||||
upsertScopeState(taskId,
|
||||
firstNonBlank(latestChunk.getScopeKey(), "task:" + taskId),
|
||||
latestChunk.getScopeHash(),
|
||||
latestChunk.getChunkTotal(),
|
||||
null,
|
||||
true,
|
||||
false);
|
||||
return 1;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int updated = 0;
|
||||
for (TaskScopeStateEntity state : inputStates) {
|
||||
if (state == null || state.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
state.setCompleted(1);
|
||||
if (state.getLastChunkAt() == null) {
|
||||
state.setLastChunkAt(now);
|
||||
}
|
||||
state.setUpdatedAt(now);
|
||||
state.setStateJson("{\"phase\":\"RECEIVED\",\"coze\":\"PENDING\"}");
|
||||
taskScopeStateMapper.updateById(state);
|
||||
updated++;
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
private void upsertScopeState(Long taskId,
|
||||
String scopeKey,
|
||||
String scopeHash,
|
||||
@@ -765,6 +908,8 @@ public class AppearancePatentTaskService {
|
||||
if (scope.getId() == null) {
|
||||
try {
|
||||
taskScopeStateMapper.insert(scope);
|
||||
log.info("[appearance-patent] scope state inserted taskId={} scope={} scopeHash={} completed={} cozeDone={}",
|
||||
taskId, scopeKey, scopeHash, completed, cozeDone);
|
||||
return;
|
||||
} catch (DuplicateKeyException ex) {
|
||||
log.info("[appearance-patent] duplicate scope state inserted concurrently taskId={} scope={}", taskId, scopeKey);
|
||||
@@ -792,6 +937,8 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
taskScopeStateMapper.updateById(scope);
|
||||
log.info("[appearance-patent] scope state updated taskId={} scope={} scopeHash={} completed={} cozeDone={}",
|
||||
taskId, scopeKey, scopeHash, completed, cozeDone);
|
||||
}
|
||||
|
||||
private <T> T inNewTransaction(Supplier<T> action) {
|
||||
@@ -1085,6 +1232,7 @@ public class AppearancePatentTaskService {
|
||||
row.setUrl(firstNonBlank(representative.getUrl(), sibling.getUrl()));
|
||||
row.setTitle(firstNonBlank(representative.getTitle(), sibling.getTitle()));
|
||||
row.setError(representative.getError());
|
||||
row.setStatus(representative.getStatus());
|
||||
row.setTitleRisk(representative.getTitleRisk());
|
||||
row.setAppearanceRisk(representative.getAppearanceRisk());
|
||||
row.setPatentRisk(representative.getPatentRisk());
|
||||
@@ -1185,7 +1333,7 @@ public class AppearancePatentTaskService {
|
||||
return null;
|
||||
}
|
||||
TaskFileJobEntity job = taskFileJobService.enqueueAssembleResult(
|
||||
task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task.getId()));
|
||||
task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task));
|
||||
if (dispatchWhenIdle
|
||||
&& job != null
|
||||
&& "RUNNING".equals(job.getStatus())
|
||||
@@ -1220,11 +1368,6 @@ public class AppearancePatentTaskService {
|
||||
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
||||
throw new BusinessException("result file job arguments are incomplete");
|
||||
}
|
||||
if (!isJobOwnedByCurrentInstance(job)) {
|
||||
log.info("[appearance-patent] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}",
|
||||
job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||
return false;
|
||||
}
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(job.getTaskId(), TASK_LOCK_WAIT_MILLIS);
|
||||
if (lockHandle == null) {
|
||||
taskFileJobService.requeue(job.getId(), "Task is busy, waiting for appearance patent result merge");
|
||||
@@ -1241,6 +1384,13 @@ public class AppearancePatentTaskService {
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("task not found");
|
||||
}
|
||||
ensureTaskOwnedByCurrentInstance(task, "assemble result file");
|
||||
job.setScopeKey(buildTaskOwnerScopeKey(task));
|
||||
if (!isJobOwnedByCurrentInstance(job)) {
|
||||
log.info("[appearance-patent] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}",
|
||||
job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||
return false;
|
||||
}
|
||||
FileResultEntity result = fileResultMapper.selectById(job.getResultId());
|
||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||
throw new BusinessException("result record not found");
|
||||
@@ -1250,7 +1400,8 @@ public class AppearancePatentTaskService {
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
int cozeWorkUnits = countCozeWorkUnits(chunks, Math.max(1, properties.getCozeBatchSize()));
|
||||
int totalProgressUnits = Math.max(3, cozeWorkUnits + 3);
|
||||
int plannedCozeUnits = Math.max(cozeWorkUnits, countAllCozeStates(task.getId()));
|
||||
int totalProgressUnits = Math.max(3, plannedCozeUnits + 3);
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
@@ -1269,10 +1420,10 @@ public class AppearancePatentTaskService {
|
||||
if (STATUS_RUNNING.equals(task.getStatus()) && !isResultSubmissionComplete(task.getId())) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, Math.max(1, cozeWorkUnits), "等待 Python 继续回传数据");
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, Math.max(1, plannedCozeUnits), "等待 Python 继续回传数据");
|
||||
return false;
|
||||
}
|
||||
completeCozeFileJob(task, result, job, totalProgressUnits, cozeWorkUnits);
|
||||
completeCozeFileJob(task, result, job, totalProgressUnits);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1555,10 +1706,10 @@ public class AppearancePatentTaskService {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
||||
return;
|
||||
}
|
||||
if (!isOwnerCurrent(context.ownerInstanceId())) {
|
||||
log.info("[appearance-patent] coze poll skipped after context refresh because owner is another instance taskId={} stateId={} owner={} current={}",
|
||||
state.getTaskId(), state.getId(), context.ownerInstanceId(), currentInstanceId());
|
||||
return;
|
||||
if (!isOwnerCurrent(context.ownerInstanceId())) {
|
||||
log.info("[appearance-patent] coze poll skipped after context refresh because owner is another instance taskId={} stateId={} owner={} current={}",
|
||||
state.getTaskId(), state.getId(), context.ownerInstanceId(), currentInstanceId());
|
||||
return;
|
||||
}
|
||||
taskFileJobService.touchRunning(context.jobId());
|
||||
log.info("[appearance-patent] coze poll start taskId={} stateId={} executeId={} jobId={} chunk={} batch={}/{}",
|
||||
@@ -1924,6 +2075,9 @@ public class AppearancePatentTaskService {
|
||||
if (taskId == null || context == null || countPendingCozeStates(taskId) > 0) {
|
||||
return;
|
||||
}
|
||||
if (!isOwnerCurrent(context.ownerInstanceId())) {
|
||||
return;
|
||||
}
|
||||
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(taskId, 0L);
|
||||
if (taskLockHandle == null) {
|
||||
return;
|
||||
@@ -1974,12 +2128,11 @@ public class AppearancePatentTaskService {
|
||||
private void completeCozeFileJob(FileTaskEntity task,
|
||||
FileResultEntity result,
|
||||
TaskFileJobEntity job,
|
||||
int totalProgressUnits,
|
||||
int cozeWorkUnits) {
|
||||
int totalProgressUnits) {
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
throw new BusinessException("Coze 结果仍在处理中,暂不能生成结果文件");
|
||||
}
|
||||
int assembleProgress = Math.max(1, Math.min(totalProgressUnits - 2, cozeWorkUnits));
|
||||
int assembleProgress = Math.max(1, totalProgressUnits - 2);
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, assembleProgress, "正在组装 xlsx");
|
||||
assembleResultWorkbook(task, result);
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, totalProgressUnits - 1, "正在上传结果文件");
|
||||
@@ -2216,8 +2369,9 @@ public class AppearancePatentTaskService {
|
||||
return "coze:task:" + taskId + ":rows:" + DigestUtil.sha256Hex(rowKeys.toString());
|
||||
}
|
||||
|
||||
private String buildTaskOwnerScopeKey(Long taskId) {
|
||||
return "task:" + taskId + ":owner:" + currentInstanceId();
|
||||
private String buildTaskOwnerScopeKey(FileTaskEntity task) {
|
||||
Long taskId = task == null ? null : task.getId();
|
||||
return "task:" + taskId + ":owner:" + firstNonBlank(ownerFromTask(task), currentInstanceId());
|
||||
}
|
||||
|
||||
private String currentInstanceId() {
|
||||
@@ -2577,7 +2731,8 @@ public class AppearancePatentTaskService {
|
||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getTitleRisk()));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getAppearanceRisk()));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getPatentRisk()));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "未送检" : userFacingConclusion(resultRow));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "未送检" : userFacingConclusion(resultRow));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getStatus(), ""));
|
||||
}
|
||||
writeReasonSheet(workbook, headerStyle, rowsToWrite, resultMap);
|
||||
workbook.write(fos);
|
||||
@@ -2962,7 +3117,7 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
vo.setFileJobId(job.getId());
|
||||
vo.setFileStatus(job.getStatus());
|
||||
vo.setFileError(job.getErrorMessage());
|
||||
vo.setFileError(STATUS_FAILED.equals(job.getStatus()) ? firstNonBlank(job.getErrorMessage(), null) : null);
|
||||
attachFileProgress(vo, row, job);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ public class DebugRequestInfoController {
|
||||
) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("instanceId", instanceMetadata.getInstanceId());
|
||||
result.put("instanceSource", instanceMetadata.getSource());
|
||||
result.put("instanceStable", instanceMetadata.isStable());
|
||||
result.put("hostname", instanceMetadata.getHostname());
|
||||
result.put("serverName", request.getServerName());
|
||||
result.put("serverPort", request.getServerPort());
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.nanri.aiimage.modules.debug.controller;
|
||||
|
||||
import com.nanri.aiimage.modules.appearancepatent.service.AppearancePatentTaskService;
|
||||
import com.nanri.aiimage.modules.similarasin.service.SimilarAsinTaskService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/debug/task-recovery")
|
||||
public class DebugTaskRecoveryController {
|
||||
|
||||
private final AppearancePatentTaskService appearancePatentTaskService;
|
||||
private final SimilarAsinTaskService similarAsinTaskService;
|
||||
|
||||
public DebugTaskRecoveryController(AppearancePatentTaskService appearancePatentTaskService,
|
||||
SimilarAsinTaskService similarAsinTaskService) {
|
||||
this.appearancePatentTaskService = appearancePatentTaskService;
|
||||
this.similarAsinTaskService = similarAsinTaskService;
|
||||
}
|
||||
|
||||
@PostMapping("/run-stale-finalize")
|
||||
public Map<String, Object> runStaleFinalize(@RequestParam("module") String module,
|
||||
@RequestParam("taskId") Long taskId) {
|
||||
String normalizedModule = module == null ? "" : module.trim().toUpperCase(Locale.ROOT);
|
||||
if ("APPEARANCE_PATENT".equals(normalizedModule)) {
|
||||
appearancePatentTaskService.debugFinalizeStaleTask(taskId);
|
||||
} else if ("SIMILAR_ASIN".equals(normalizedModule)) {
|
||||
similarAsinTaskService.debugFinalizeStaleTask(taskId);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported module: " + module);
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("module", normalizedModule);
|
||||
result.put("taskId", taskId);
|
||||
result.put("accepted", true);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.nanri.aiimage.modules.shopkey.controller;
|
||||
|
||||
import com.nanri.aiimage.common.api.ApiResponse;
|
||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
||||
@@ -13,6 +14,9 @@ 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.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -24,24 +28,36 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/admin/skip-price-asins")
|
||||
@Tag(name = "跳过跟价 ASIN", description = "维护店铺按国家跳过跟价的 ASIN")
|
||||
@RequestMapping({
|
||||
"/admin/skip-price-asins",
|
||||
"/admin/skip-price-asin",
|
||||
"/api/admin/skip-price-asins",
|
||||
"/api/admin/skip-price-asin",
|
||||
"/skip-price-asins",
|
||||
"/skip-price-asin"
|
||||
})
|
||||
@Tag(name = "Skip Price ASIN", description = "Manage skip price asin data")
|
||||
public class SkipPriceAsinController {
|
||||
|
||||
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
|
||||
private final SkipPriceAsinService skipPriceAsinService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询跳过跟价 ASIN")
|
||||
@Operation(summary = "Page query skip price asins")
|
||||
public ApiResponse<SkipPriceAsinPageVo> page(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
||||
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
|
||||
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
|
||||
@Parameter(description = "ASIN") @RequestParam(required = false) String asin,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
|
||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
||||
@Parameter(description = "page") @RequestParam(name = "page", defaultValue = "1") Long page,
|
||||
@Parameter(description = "page size") @RequestParam(name = "page_size", defaultValue = "15") Long pageSize,
|
||||
@Parameter(description = "group id") @RequestParam(name = "group_id", required = false) Long groupId,
|
||||
@Parameter(description = "shop name") @RequestParam(name = "shop_name", required = false) String shopName,
|
||||
@Parameter(description = "asin") @RequestParam(name = "asin", required = false) String asin,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id", required = false) Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
||||
return ApiResponse.success(skipPriceAsinService.page(
|
||||
page,
|
||||
pageSize,
|
||||
@@ -52,59 +68,76 @@ public class SkipPriceAsinController {
|
||||
Boolean.TRUE.equals(superAdmin)));
|
||||
}
|
||||
|
||||
@GetMapping("/export")
|
||||
@Operation(summary = "Export skip price asins")
|
||||
public ResponseEntity<byte[]> export(
|
||||
@Parameter(description = "group id") @RequestParam(name = "group_id", required = false) Long groupId,
|
||||
@Parameter(description = "shop name") @RequestParam(name = "shop_name", required = false) String shopName,
|
||||
@Parameter(description = "asin") @RequestParam(name = "asin", required = false) String asin,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id", required = false) Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
||||
byte[] bytes = skipPriceAsinService.export(groupId, shopName, asin, operatorId, Boolean.TRUE.equals(superAdmin));
|
||||
String filename = "skip-price-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
||||
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.contentLength(bytes.length)
|
||||
.body(bytes);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "新增或覆盖跳过跟价 ASIN")
|
||||
@Operation(summary = "Create or update skip price asin")
|
||||
public ApiResponse<SkipPriceAsinItemVo> create(
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id") Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin,
|
||||
@Valid @RequestBody SkipPriceAsinCreateRequest request) {
|
||||
return ApiResponse.success("保存成功",
|
||||
return ApiResponse.success("\u4fdd\u5b58\u6210\u529f",
|
||||
skipPriceAsinService.createOrUpdate(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "导入新增跳过跟价 ASIN")
|
||||
@Operation(summary = "Import skip price asin")
|
||||
public ApiResponse<QueryAsinImportStartVo> importExcel(
|
||||
@Parameter(description = "xlsx/xls 文件,文件名必须等于店铺名", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "页面选择的分组ID", required = true) @RequestParam Long groupId,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
||||
return ApiResponse.success("开始导入",
|
||||
@Parameter(description = "xlsx or xls file", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "group id", required = true) @RequestParam(name = "group_id") Long groupId,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id") Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
||||
return ApiResponse.success("\u5f00\u59cb\u5bfc\u5165",
|
||||
skipPriceAsinService.startImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
||||
}
|
||||
|
||||
@GetMapping("/import/{importId}")
|
||||
@Operation(summary = "查询导入新增进度")
|
||||
@Operation(summary = "Query import progress")
|
||||
public ApiResponse<QueryAsinImportProgressVo> importProgress(@PathVariable String importId) {
|
||||
return ApiResponse.success(skipPriceAsinService.getImportProgress(importId));
|
||||
}
|
||||
|
||||
@PostMapping("/delete-import")
|
||||
@Operation(summary = "导入删除跳过跟价 ASIN")
|
||||
@Operation(summary = "Import delete skip price asin")
|
||||
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
|
||||
@Parameter(description = "xlsx/xls 文件,文件名必须等于店铺名", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "页面选择的分组ID", required = true) @RequestParam Long groupId,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
||||
return ApiResponse.success("开始删除",
|
||||
@Parameter(description = "xlsx or xls file", required = true) @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "group id", required = true) @RequestParam(name = "group_id") Long groupId,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id") Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
||||
return ApiResponse.success("\u5f00\u59cb\u5220\u9664",
|
||||
skipPriceAsinService.startDeleteImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
||||
}
|
||||
|
||||
@GetMapping("/delete-import/{importId}")
|
||||
@Operation(summary = "查询导入删除进度")
|
||||
@Operation(summary = "Query delete import progress")
|
||||
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(@PathVariable String importId) {
|
||||
return ApiResponse.success(skipPriceAsinService.getDeleteImportProgress(importId));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/countries/{country}")
|
||||
@Operation(summary = "编辑指定国家的跳过跟价 ASIN")
|
||||
@PutMapping({"/{id}/countries/{country}", "/{id}/country/{country}"})
|
||||
@Operation(summary = "Update one country skip price asin")
|
||||
public ApiResponse<SkipPriceAsinItemVo> updateCountry(
|
||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "国家编码", required = true) @PathVariable String country,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
||||
@Parameter(description = "id", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "country code", required = true) @PathVariable String country,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id") Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin,
|
||||
@Valid @RequestBody SkipPriceAsinCountryUpdateRequest request) {
|
||||
return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry(
|
||||
return ApiResponse.success("\u4fdd\u5b58\u6210\u529f", skipPriceAsinService.updateCountry(
|
||||
id,
|
||||
country,
|
||||
request.getAsin(),
|
||||
@@ -113,14 +146,14 @@ public class SkipPriceAsinController {
|
||||
Boolean.TRUE.equals(superAdmin)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/countries/{country}")
|
||||
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
|
||||
@DeleteMapping({"/{id}/countries/{country}", "/{id}/country/{country}"})
|
||||
@Operation(summary = "Delete one country skip price asin")
|
||||
public ApiResponse<Void> deleteCountry(
|
||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "国家编码", required = true) @PathVariable String country,
|
||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
||||
@Parameter(description = "id", required = true) @PathVariable Long id,
|
||||
@Parameter(description = "country code", required = true) @PathVariable String country,
|
||||
@Parameter(description = "operator user id") @RequestParam(name = "operator_id") Long operatorId,
|
||||
@Parameter(description = "is super admin") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
||||
skipPriceAsinService.deleteCountry(id, country, operatorId, Boolean.TRUE.equals(superAdmin));
|
||||
return ApiResponse.success("删除成功", null);
|
||||
return ApiResponse.success("\u5220\u9664\u6210\u529f", null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,19 @@ import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -46,6 +49,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class SkipPriceAsinService {
|
||||
|
||||
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
||||
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final SkipPriceAsinMapper skipPriceAsinMapper;
|
||||
private final ShopManageMapper shopManageMapper;
|
||||
@@ -57,32 +61,15 @@ public class SkipPriceAsinService {
|
||||
Long operatorId, boolean superAdmin) {
|
||||
long safePage = Math.max(page, 1);
|
||||
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
|
||||
String safeShopName = normalizeBlank(shopName);
|
||||
String safeAsin = normalizeBlank(asin);
|
||||
Set<Long> accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false);
|
||||
|
||||
LambdaQueryWrapper<SkipPriceAsinEntity> query = new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
||||
.eq(groupId != null && groupId > 0, SkipPriceAsinEntity::getGroupId, groupId)
|
||||
.like(!safeShopName.isEmpty(), SkipPriceAsinEntity::getShopName, safeShopName)
|
||||
.and(!safeAsin.isEmpty(), wrapper -> wrapper
|
||||
.like(SkipPriceAsinEntity::getAsinDe, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinUk, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinFr, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinIt, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinEs, safeAsin))
|
||||
.orderByDesc(SkipPriceAsinEntity::getId);
|
||||
|
||||
if (!superAdmin) {
|
||||
if (accessibleGroupIds.isEmpty()) {
|
||||
query.eq(SkipPriceAsinEntity::getId, -1L);
|
||||
} else {
|
||||
query.in(SkipPriceAsinEntity::getGroupId, accessibleGroupIds);
|
||||
}
|
||||
}
|
||||
|
||||
Long total = skipPriceAsinMapper.selectCount(query);
|
||||
List<SkipPriceAsinEntity> rows = skipPriceAsinMapper
|
||||
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
||||
Long total = countFilteredRows(groupId, shopName, asin, operatorId, superAdmin);
|
||||
List<SkipPriceAsinEntity> rows = listFilteredRows(
|
||||
groupId,
|
||||
shopName,
|
||||
asin,
|
||||
operatorId,
|
||||
superAdmin,
|
||||
(safePage - 1) * safePageSize,
|
||||
safePageSize);
|
||||
|
||||
Map<Long, String> groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream()
|
||||
.map(SkipPriceAsinEntity::getGroupId)
|
||||
@@ -98,6 +85,127 @@ public class SkipPriceAsinService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
public byte[] export(Long groupId, String shopName, String asin, Long operatorId, boolean superAdmin) {
|
||||
List<SkipPriceAsinEntity> rows = listFilteredRows(groupId, shopName, asin, operatorId, superAdmin, null, null);
|
||||
Map<Long, String> groupNameById = shopManageGroupService.buildGroupNameMap(rows.stream()
|
||||
.map(SkipPriceAsinEntity::getGroupId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.distinct()
|
||||
.toList());
|
||||
try (SXSSFWorkbook workbook = new SXSSFWorkbook(100);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("SkipPriceAsin");
|
||||
writeExportHeader(sheet);
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
writeExportRow(sheet, index + 1, rows.get(index), groupNameById.get(rows.get(index).getGroupId()));
|
||||
}
|
||||
applyExportColumnWidths(sheet);
|
||||
workbook.write(outputStream);
|
||||
workbook.dispose();
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("导出跳过跟价 ASIN 失败");
|
||||
}
|
||||
}
|
||||
|
||||
private Long countFilteredRows(Long groupId, String shopName, String asin, Long operatorId, boolean superAdmin) {
|
||||
return skipPriceAsinMapper.selectCount(buildFilterQuery(groupId, shopName, asin, operatorId, superAdmin));
|
||||
}
|
||||
|
||||
private List<SkipPriceAsinEntity> listFilteredRows(Long groupId, String shopName, String asin,
|
||||
Long operatorId, boolean superAdmin,
|
||||
Long offset, Long limit) {
|
||||
LambdaQueryWrapper<SkipPriceAsinEntity> query = buildFilterQuery(groupId, shopName, asin, operatorId, superAdmin);
|
||||
if (offset != null && limit != null) {
|
||||
query.last("LIMIT " + Math.max(0L, offset) + ", " + Math.max(1L, limit));
|
||||
}
|
||||
return skipPriceAsinMapper.selectList(query);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<SkipPriceAsinEntity> buildFilterQuery(Long groupId, String shopName, String asin,
|
||||
Long operatorId, boolean superAdmin) {
|
||||
String safeShopName = normalizeBlank(shopName);
|
||||
String safeAsin = normalizeBlank(asin);
|
||||
Set<Long> accessibleGroupIds = superAdmin ? Set.of() : shopManageGroupService.listAccessibleGroupIds(operatorId, false);
|
||||
LambdaQueryWrapper<SkipPriceAsinEntity> query = new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
||||
.eq(groupId != null && groupId > 0, SkipPriceAsinEntity::getGroupId, groupId)
|
||||
.like(!safeShopName.isEmpty(), SkipPriceAsinEntity::getShopName, safeShopName)
|
||||
.and(!safeAsin.isEmpty(), wrapper -> wrapper
|
||||
.like(SkipPriceAsinEntity::getAsinDe, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinUk, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinFr, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinIt, safeAsin)
|
||||
.or().like(SkipPriceAsinEntity::getAsinEs, safeAsin))
|
||||
.orderByDesc(SkipPriceAsinEntity::getId);
|
||||
if (!superAdmin) {
|
||||
if (accessibleGroupIds.isEmpty()) {
|
||||
query.eq(SkipPriceAsinEntity::getId, -1L);
|
||||
} else {
|
||||
query.in(SkipPriceAsinEntity::getGroupId, accessibleGroupIds);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private void writeExportHeader(Sheet sheet) {
|
||||
Row header = sheet.createRow(0);
|
||||
String[] headers = {
|
||||
"序号", "分组", "店铺名",
|
||||
"德国ASIN", "德国最低价",
|
||||
"英国ASIN", "英国最低价",
|
||||
"法国ASIN", "法国最低价",
|
||||
"意大利ASIN", "意大利最低价",
|
||||
"西班牙ASIN", "西班牙最低价",
|
||||
"创建时间", "更新时间"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
header.createCell(i).setCellValue(headers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeExportRow(Sheet sheet, int rowNo, SkipPriceAsinEntity entity, String groupName) {
|
||||
Row row = sheet.createRow(rowNo);
|
||||
int col = 0;
|
||||
row.createCell(col++).setCellValue(rowNo);
|
||||
row.createCell(col++).setCellValue(blankToEmpty(groupName));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(entity.getShopName()));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(entity.getAsinDe()));
|
||||
row.createCell(col++).setCellValue(formatExportPrice(entity.getMinimumPriceDe()));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(entity.getAsinUk()));
|
||||
row.createCell(col++).setCellValue(formatExportPrice(entity.getMinimumPriceUk()));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(entity.getAsinFr()));
|
||||
row.createCell(col++).setCellValue(formatExportPrice(entity.getMinimumPriceFr()));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(entity.getAsinIt()));
|
||||
row.createCell(col++).setCellValue(formatExportPrice(entity.getMinimumPriceIt()));
|
||||
row.createCell(col++).setCellValue(blankToEmpty(entity.getAsinEs()));
|
||||
row.createCell(col++).setCellValue(formatExportPrice(entity.getMinimumPriceEs()));
|
||||
row.createCell(col++).setCellValue(formatExportTime(entity.getCreatedAt()));
|
||||
row.createCell(col).setCellValue(formatExportTime(entity.getUpdatedAt()));
|
||||
}
|
||||
|
||||
private void applyExportColumnWidths(Sheet sheet) {
|
||||
int[] widths = {
|
||||
2800, 5200, 5200,
|
||||
4600, 3600,
|
||||
4600, 3600,
|
||||
4600, 3600,
|
||||
4600, 3600,
|
||||
4600, 3600,
|
||||
5200, 5200
|
||||
};
|
||||
for (int i = 0; i < widths.length; i++) {
|
||||
sheet.setColumnWidth(i, widths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private String formatExportPrice(BigDecimal value) {
|
||||
return value == null ? "" : value.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private String formatExportTime(LocalDateTime value) {
|
||||
return value == null ? "" : value.format(EXPORT_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SkipPriceAsinItemVo createOrUpdate(SkipPriceAsinCreateRequest request, Long operatorId, boolean superAdmin) {
|
||||
ShopManageGroupEntity group = shopManageGroupService.getAccessibleById(request.getGroupId(), operatorId, superAdmin);
|
||||
|
||||
@@ -259,12 +259,18 @@ public class SimilarAsinCozeClient {
|
||||
}
|
||||
|
||||
private Map<String, Object> buildParameters(List<SimilarAsinResultRowDto> rows, String prompt, String apiKey) {
|
||||
List<String> groupKeys = rows.stream().map(row -> nonBlank(row.getGroupKey(), rowKey(row.getId(), row.getAsin(), row.getCountry()))).toList();
|
||||
List<String> rowTokens = rows.stream().map(row -> nonBlank(row.getRowToken(), "")).toList();
|
||||
List<String> rowIds = rows.stream().map(row -> nonBlank(row.getId(), "")).toList();
|
||||
List<String> asins = rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList();
|
||||
List<String> countries = rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList();
|
||||
List<String> skus = rows.stream().map(row -> nonBlank(row.getSku(), "")).toList();
|
||||
List<String> titles = rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).toList();
|
||||
List<String> urls = rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList();
|
||||
List<List<String>> urlLists = rows.stream().map(SimilarAsinResultRowDto::getUrls).toList();
|
||||
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
parameters.put("items", buildItemObjects(rows, asins, urls, urlLists));
|
||||
parameters.put("items", buildItemObjects(groupKeys, rowTokens, rowIds, asins, countries, skus, titles, urls, urlLists));
|
||||
parameters.put("prompt", prompt == null ? "" : prompt);
|
||||
if (apiKey != null && !apiKey.isBlank()) {
|
||||
parameters.put("api_key", apiKey.trim());
|
||||
@@ -302,14 +308,25 @@ public class SimilarAsinCozeClient {
|
||||
return normalized.substring(0, 6) + "***" + normalized.substring(normalized.length() - 4);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildItemObjects(List<SimilarAsinResultRowDto> rows,
|
||||
private List<Map<String, Object>> buildItemObjects(List<String> groupKeys,
|
||||
List<String> rowTokens,
|
||||
List<String> rowIds,
|
||||
List<String> asins,
|
||||
List<String> countries,
|
||||
List<String> skus,
|
||||
List<String> titles,
|
||||
List<String> urls,
|
||||
List<List<String>> urlLists) {
|
||||
List<Map<String, Object>> items = new ArrayList<>(rows.size());
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
List<Map<String, Object>> items = new ArrayList<>(asins.size());
|
||||
for (int i = 0; i < asins.size(); i++) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("group_key", groupKeys.get(i));
|
||||
item.put("row_token", rowTokens.get(i));
|
||||
item.put("row_id", rowIds.get(i));
|
||||
item.put("asin", asins.get(i));
|
||||
item.put("country", countries.get(i));
|
||||
item.put("sku", skus.get(i));
|
||||
item.put("title", titles.get(i));
|
||||
item.put("url", urls.get(i));
|
||||
item.put("target_urls", urlLists.get(i));
|
||||
items.add(item);
|
||||
@@ -334,6 +351,9 @@ public class SimilarAsinCozeClient {
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("group_key"), node.get("groupKey")),
|
||||
firstNonNull(itemNode.get("group_key"), itemNode.get("groupKey")))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("row_token"), node.get("rowToken")),
|
||||
firstNonNull(itemNode.get("row_token"), itemNode.get("rowToken")))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("row_id"), firstNonNull(node.get("rowId"), node.get("id"))),
|
||||
firstNonNull(itemNode.get("row_id"), firstNonNull(itemNode.get("rowId"), itemNode.get("id"))))),
|
||||
@@ -341,6 +361,9 @@ public class SimilarAsinCozeClient {
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("country"), node.get("site")),
|
||||
firstNonNull(itemNode.get("country"), itemNode.get("site")))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("sku"), firstNonNull(node.get("SKU"), firstNonNull(node.get("seller_sku"), node.get("sellerSku")))),
|
||||
firstNonNull(itemNode.get("sku"), firstNonNull(itemNode.get("SKU"), firstNonNull(itemNode.get("seller_sku"), itemNode.get("sellerSku")))))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("price"), node.get("价格")),
|
||||
firstNonNull(itemNode.get("price"), itemNode.get("价格")))),
|
||||
@@ -351,6 +374,15 @@ public class SimilarAsinCozeClient {
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("similarity"), firstNonNull(node.get("similarity_rate"), node.get("相似度"))),
|
||||
firstNonNull(itemNode.get("similarity"), firstNonNull(itemNode.get("similarity_rate"), itemNode.get("相似度"))))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("is_conform"), firstNonNull(node.get("isConform"), firstNonNull(node.get("conform"), node.get("是否符合类目")))),
|
||||
firstNonNull(itemNode.get("is_conform"), firstNonNull(itemNode.get("isConform"), firstNonNull(itemNode.get("conform"), itemNode.get("是否符合类目")))))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("reason"), firstNonNull(node.get("原因"), node.get("不符合理由"))),
|
||||
firstNonNull(itemNode.get("reason"), firstNonNull(itemNode.get("原因"), itemNode.get("不符合理由"))))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("category"), firstNonNull(node.get("类目"), node.get("产品类目"))),
|
||||
firstNonNull(itemNode.get("category"), firstNonNull(itemNode.get("类目"), itemNode.get("产品类目"))))),
|
||||
text(firstNonNull(node.get("appearance"),
|
||||
firstNonNull(node.get("appearance_risk"),
|
||||
firstNonNull(itemNode.get("appearance"), itemNode.get("appearance_risk"))))),
|
||||
@@ -360,6 +392,9 @@ public class SimilarAsinCozeClient {
|
||||
text(firstNonNull(node.get("result"),
|
||||
firstNonNull(node.get("conclusion"),
|
||||
firstNonNull(itemNode.get("result"), itemNode.get("conclusion"))))),
|
||||
text(firstNonNull(
|
||||
firstNonNull(node.get("status"), firstNonNull(node.get("row_status"), node.get("rowStatus"))),
|
||||
firstNonNull(itemNode.get("status"), firstNonNull(itemNode.get("row_status"), itemNode.get("rowStatus"))))),
|
||||
text(firstNonNull(node.get("title_reason"),
|
||||
firstNonNull(node.get("titleReason"),
|
||||
firstNonNull(itemNode.get("title_reason"), itemNode.get("titleReason"))))),
|
||||
@@ -391,15 +426,14 @@ public class SimilarAsinCozeClient {
|
||||
}
|
||||
|
||||
private List<SimilarAsinResultRowDto> mergeRows(List<SimilarAsinResultRowDto> rows, List<CozeResult> results) {
|
||||
Map<String, CozeResult> resultByGroupKey = new LinkedHashMap<>();
|
||||
Map<String, CozeResult> resultByRowToken = new LinkedHashMap<>();
|
||||
Map<String, CozeResult> resultByCompositeKey = new LinkedHashMap<>();
|
||||
Map<String, CozeResult> resultByAsinCountry = new LinkedHashMap<>();
|
||||
Map<String, CozeResult> resultByAsin = new LinkedHashMap<>();
|
||||
Map<String, CozeResult> resultByRowId = new LinkedHashMap<>();
|
||||
for (CozeResult result : results) {
|
||||
String groupKey = normalize(result.groupKey());
|
||||
if (!groupKey.isBlank()) {
|
||||
resultByGroupKey.putIfAbsent(groupKey, result);
|
||||
String rowToken = normalize(result.rowToken());
|
||||
if (!rowToken.isBlank()) {
|
||||
resultByRowToken.putIfAbsent(rowToken, result);
|
||||
}
|
||||
String compositeKey = rowKey(result.rowId(), result.asin(), result.country());
|
||||
if (!compositeKey.isBlank()) {
|
||||
@@ -409,10 +443,6 @@ public class SimilarAsinCozeClient {
|
||||
if (!asinCountryKey.isBlank()) {
|
||||
resultByAsinCountry.putIfAbsent(asinCountryKey, result);
|
||||
}
|
||||
String asinKey = normalize(result.asin()).toUpperCase(Locale.ROOT);
|
||||
if (!asinKey.isBlank()) {
|
||||
resultByAsin.putIfAbsent(asinKey, result);
|
||||
}
|
||||
String rowIdKey = normalize(result.rowId());
|
||||
if (!rowIdKey.isBlank()) {
|
||||
resultByRowId.putIfAbsent(rowIdKey, result);
|
||||
@@ -423,7 +453,7 @@ public class SimilarAsinCozeClient {
|
||||
boolean allowIndexFallback = results.size() == rows.size() && results.stream().noneMatch(this::hasIdentity);
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
SimilarAsinResultRowDto row = copy(rows.get(i));
|
||||
CozeResult result = resultByGroupKey.get(normalize(row.getGroupKey()));
|
||||
CozeResult result = resultByRowToken.get(normalize(row.getRowToken()));
|
||||
if (result == null) {
|
||||
result = resultByCompositeKey.get(rowKey(row.getId(), row.getAsin(), row.getCountry()));
|
||||
}
|
||||
@@ -433,9 +463,6 @@ public class SimilarAsinCozeClient {
|
||||
if (result == null) {
|
||||
result = resultByAsinCountry.get(asinCountryKey(row.getAsin(), row.getCountry()));
|
||||
}
|
||||
if (result == null) {
|
||||
result = resultByAsin.get(normalize(row.getAsin()).toUpperCase(Locale.ROOT));
|
||||
}
|
||||
if (result == null) {
|
||||
result = resultByRowId.get(normalize(row.getId()));
|
||||
}
|
||||
@@ -460,7 +487,7 @@ public class SimilarAsinCozeClient {
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
return !normalize(result.groupKey()).isBlank()
|
||||
return !normalize(result.rowToken()).isBlank()
|
||||
|| !normalize(result.rowId()).isBlank()
|
||||
|| !normalize(result.asin()).isBlank();
|
||||
}
|
||||
@@ -469,7 +496,7 @@ public class SimilarAsinCozeClient {
|
||||
if (result == null || row == null) {
|
||||
return false;
|
||||
}
|
||||
if (!normalize(result.groupKey()).isBlank() || !normalize(result.rowId()).isBlank()) {
|
||||
if (!normalize(result.rowToken()).isBlank() || !normalize(result.rowId()).isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String resultAsin = normalize(result.asin()).toUpperCase(Locale.ROOT);
|
||||
@@ -481,12 +508,17 @@ public class SimilarAsinCozeClient {
|
||||
return;
|
||||
}
|
||||
row.setTitleRisk(result.title());
|
||||
row.setSku(nonBlank(result.sku(), row.getSku()));
|
||||
row.setPrice(nonBlank(result.price(), row.getPrice()));
|
||||
row.setIsStock(result.isStock());
|
||||
row.setSimilarity(result.similarity());
|
||||
row.setIsConform(result.isConform());
|
||||
row.setReason(result.reason());
|
||||
row.setCategory(result.category());
|
||||
row.setAppearanceRisk(result.appearance());
|
||||
row.setPatentRisk(result.patent());
|
||||
row.setConclusion(result.result());
|
||||
row.setStatus(result.status());
|
||||
row.setTitleReason(result.titleReason());
|
||||
row.setAppearanceReason(result.appearanceReason());
|
||||
row.setPatentReason(result.patentReason());
|
||||
@@ -508,11 +540,16 @@ public class SimilarAsinCozeClient {
|
||||
row.setId(source.getId());
|
||||
row.setAsin(source.getAsin());
|
||||
row.setCountry(source.getCountry());
|
||||
row.setSku(source.getSku());
|
||||
row.setPrice(source.getPrice());
|
||||
row.setUrls(source.getUrls());
|
||||
row.setTitle(source.getTitle());
|
||||
row.setError(source.getError());
|
||||
row.setDone(source.getDone());
|
||||
row.setStatus(source.getStatus());
|
||||
row.setIsConform(source.getIsConform());
|
||||
row.setReason(source.getReason());
|
||||
row.setCategory(source.getCategory());
|
||||
row.setTitleRisk(source.getTitleRisk());
|
||||
row.setAppearanceRisk(source.getAppearanceRisk());
|
||||
row.setPatentRisk(source.getPatentRisk());
|
||||
@@ -532,6 +569,12 @@ public class SimilarAsinCozeClient {
|
||||
if (row.getError() == null || row.getError().isBlank()) {
|
||||
row.setError(failureMessage);
|
||||
}
|
||||
if (row.getReason() == null || row.getReason().isBlank()) {
|
||||
row.setReason(failureMessage);
|
||||
}
|
||||
if (row.getStatus() == null || row.getStatus().isBlank()) {
|
||||
row.setStatus("FAILED");
|
||||
}
|
||||
if (row.getTitleRisk() == null || row.getTitleRisk().isBlank()) {
|
||||
row.setTitleRisk(reviewMessage);
|
||||
}
|
||||
@@ -721,6 +764,17 @@ public class SimilarAsinCozeClient {
|
||||
|| item.has("similarity")
|
||||
|| item.has("similarity_rate")
|
||||
|| item.has("相似度")
|
||||
|| item.has("is_conform")
|
||||
|| item.has("isConform")
|
||||
|| item.has("conform")
|
||||
|| item.has("是否符合类目")
|
||||
|| item.has("reason")
|
||||
|| item.has("原因")
|
||||
|| item.has("不符合理由")
|
||||
|| item.has("category")
|
||||
|| item.has("类目")
|
||||
|| item.has("产品类目")
|
||||
|| item.has("status")
|
||||
|| item.has("title_reason")
|
||||
|| item.has("titleReason")
|
||||
|| item.has("appearance_reason")
|
||||
@@ -973,16 +1027,22 @@ public class SimilarAsinCozeClient {
|
||||
|
||||
private record CozeResult(
|
||||
String groupKey,
|
||||
String rowToken,
|
||||
String rowId,
|
||||
String asin,
|
||||
String country,
|
||||
String sku,
|
||||
String price,
|
||||
String title,
|
||||
String isStock,
|
||||
String similarity,
|
||||
String isConform,
|
||||
String reason,
|
||||
String category,
|
||||
String appearance,
|
||||
String patent,
|
||||
String result,
|
||||
String status,
|
||||
String titleReason,
|
||||
String appearanceReason,
|
||||
String patentReason
|
||||
|
||||
@@ -120,7 +120,7 @@ public class SimilarAsinController {
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/result")
|
||||
@Operation(summary = "提交 Python 回传结果", description = "Python 回传商品数据接口。items 可以是一条或多条;Java 先原样保存回传数据,再内部攒够 10 条调用 Coze。done=true 表示 Python 已完成全部回传,Java 会强制处理剩余不足 10 条的数据并生成最终 xlsx。")
|
||||
@Operation(summary = "提交 Python 回传结果", description = "Python 回传商品数据接口。请通过 groups[].items[] 回传分组结果;Java 先原样保存回传数据,再内部攒批调用 Coze。done=true 表示 Python 已完成全部回传,Java 会强制处理剩余未满批的数据并生成最终 xlsx。")
|
||||
public ApiResponse<Void> result(
|
||||
@Parameter(description = "相似ASIN检测任务 ID。任务必须处于 RUNNING 状态。", required = true, example = "3938")
|
||||
@PathVariable Long taskId,
|
||||
|
||||
@@ -39,6 +39,10 @@ public class SimilarAsinResultRowDto {
|
||||
@Schema(description = "站点或国家。来自 Excel 的国家列,例如英国、德国、法国。", example = "英国")
|
||||
private String country;
|
||||
|
||||
@JsonAlias({"sku", "SKU", "seller_sku", "sellerSku", "msku", "货号"})
|
||||
@Schema(description = "商品 SKU。来自 Excel 解析或 Python 回传。", example = "SKU-001")
|
||||
private String sku;
|
||||
|
||||
@JsonAlias({"price", "价格"})
|
||||
@Schema(description = "商品价格。来自 Excel 解析或 Python 回传。", example = "12.29")
|
||||
private String price;
|
||||
@@ -61,12 +65,28 @@ public class SimilarAsinResultRowDto {
|
||||
@Schema(description = "Coze 返回的相似度结果。", example = "80%", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String similarity;
|
||||
|
||||
@JsonAlias({"is_conform", "isConform", "conform", "是否符合类目"})
|
||||
@Schema(description = "Coze 返回的是否符合类目结果。", example = "符合", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String isConform;
|
||||
|
||||
@JsonAlias({"reason", "原因", "不符合理由"})
|
||||
@Schema(description = "Coze 返回的不符合理由。", example = "类目不匹配", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String reason;
|
||||
|
||||
@JsonAlias({"category", "类目", "产品类目"})
|
||||
@Schema(description = "Coze 返回的产品类目。", example = "女装", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String category;
|
||||
|
||||
@Schema(description = "单行错误信息。通常用于记录 Python 单行处理异常;Coze 失败时后端会尽量保留原始行,不强行写入风险结果。", example = "图片地址为空")
|
||||
private String error;
|
||||
|
||||
@Schema(description = "单行完成标记。当前主要使用请求体顶层 done 控制任务收尾,该字段仅作兼容。", example = "true")
|
||||
private Boolean done;
|
||||
|
||||
@JsonAlias({"row_status", "rowStatus", "Status"})
|
||||
@Schema(description = "Coze row status", example = "success", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String status;
|
||||
|
||||
@Schema(description = "兼容旧版 Coze 返回中的标题维度结果字段;当前相似 ASIN 结果文件不再输出该列。", example = "标题未发现明显商标侵权风险。", accessMode = Schema.AccessMode.READ_ONLY)
|
||||
private String titleRisk;
|
||||
|
||||
|
||||
@@ -27,6 +27,4 @@ public class SimilarAsinSubmitResultRequest {
|
||||
@Schema(description = "本次回传的分组结果列表,推荐优先使用")
|
||||
private List<SimilarAsinResultGroupDto> groups = new ArrayList<>();
|
||||
|
||||
@Schema(description = "兼容旧链路的平铺结果列表")
|
||||
private List<SimilarAsinResultRowDto> items = new ArrayList<>();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ public class SimilarAsinParsedRowVo {
|
||||
@Schema(description = "商品价格。", example = "12.29")
|
||||
private String price;
|
||||
|
||||
@Schema(description = "商品 SKU。", example = "SKU-001")
|
||||
private String sku;
|
||||
|
||||
@Schema(description = "商品图片 URL 或商品 URL,供 Coze 检测使用。", example = "https://webstatic.aiproxy.vip/output/demo.jpg")
|
||||
private String url;
|
||||
|
||||
|
||||
@@ -114,11 +114,15 @@ public class SimilarAsinTaskService {
|
||||
private static final int PARSE_RESPONSE_PREVIEW_LIMIT = 100;
|
||||
private static final List<String> RESULT_HEADERS = List.of(
|
||||
"id",
|
||||
"sku",
|
||||
"asin",
|
||||
"国家",
|
||||
"价格",
|
||||
"是否有货",
|
||||
"相似度"
|
||||
"相似度",
|
||||
"是否符合类目",
|
||||
"不符合理由",
|
||||
"产品类目",
|
||||
"status"
|
||||
);
|
||||
|
||||
private final LocalFileStorageService localFileStorageService;
|
||||
@@ -316,8 +320,8 @@ public class SimilarAsinTaskService {
|
||||
vo.setDroppedRows(droppedRows);
|
||||
vo.setGroupCount(groups.size());
|
||||
vo.setAiPrompt(normalize(request.getAiPrompt()));
|
||||
vo.setItems(buildResponsePreviewRows(allRows));
|
||||
vo.setGroups(List.of());
|
||||
vo.setItems(new ArrayList<>(allRows));
|
||||
vo.setGroups(groups);
|
||||
long finishedAt = System.nanoTime();
|
||||
log.info("[similar-asin] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}",
|
||||
task.getId(),
|
||||
@@ -482,7 +486,7 @@ public class SimilarAsinTaskService {
|
||||
completeSubmittedChunk(context);
|
||||
return null;
|
||||
});
|
||||
submitCozeForSubmittedChunk(context);
|
||||
scheduleCozePipelineForSubmittedChunk(context);
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
@@ -568,7 +572,7 @@ public class SimilarAsinTaskService {
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
fileTaskMapper.updateById(task);
|
||||
}
|
||||
submitCozeForSubmittedChunk(context);
|
||||
scheduleCozePipelineForSubmittedChunk(context);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -620,19 +624,11 @@ public class SimilarAsinTaskService {
|
||||
if (transactionManager != null) {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||
.last("limit 50"));
|
||||
List<FileTaskEntity> tasks = listStaleFinalizeCandidates(threshold);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
if (!isOwnerCurrent(ownerFromTask(task))) {
|
||||
continue;
|
||||
}
|
||||
if (isJavaSideProcessing(task.getId())) {
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
continue;
|
||||
}
|
||||
long heartbeatMillis = taskCacheService.getTaskHeartbeatMillis(task.getId());
|
||||
if (heartbeatMillis > thresholdMillis) {
|
||||
continue;
|
||||
@@ -652,19 +648,11 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
|
||||
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getUpdatedAt, threshold)
|
||||
.last("limit 50"));
|
||||
List<FileTaskEntity> tasks = listStaleFinalizeCandidates(threshold);
|
||||
for (FileTaskEntity task : tasks) {
|
||||
if (!isOwnerCurrent(ownerFromTask(task))) {
|
||||
continue;
|
||||
}
|
||||
if (isJavaSideProcessing(task.getId())) {
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
continue;
|
||||
}
|
||||
long heartbeatMillis = taskCacheService.getTaskHeartbeatMillis(task.getId());
|
||||
if (heartbeatMillis > thresholdMillis) {
|
||||
continue;
|
||||
@@ -674,11 +662,46 @@ public class SimilarAsinTaskService {
|
||||
continue;
|
||||
}
|
||||
try (taskLockHandle) {
|
||||
finalizeTask(task, "Python interrupted before uploading final similar ASIN result", allRowCount(task), true);
|
||||
finalizeStaleTask(task.getId(), "Python interrupted before uploading final similar ASIN result");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void debugFinalizeStaleTask(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return;
|
||||
}
|
||||
if (transactionManager != null) {
|
||||
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(taskId, 0L);
|
||||
if (taskLockHandle == null) {
|
||||
return;
|
||||
}
|
||||
try (taskLockHandle) {
|
||||
inNewTransaction(() -> {
|
||||
finalizeStaleTask(taskId, "Python interrupted before uploading final similar ASIN result");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(taskId, 0L);
|
||||
if (taskLockHandle == null) {
|
||||
return;
|
||||
}
|
||||
try (taskLockHandle) {
|
||||
finalizeStaleTask(taskId, "Python interrupted before uploading final similar ASIN result");
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileTaskEntity> listStaleFinalizeCandidates(LocalDateTime threshold) {
|
||||
return fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.lt(FileTaskEntity::getCreatedAt, threshold)
|
||||
.orderByAsc(FileTaskEntity::getCreatedAt)
|
||||
.last("limit 200"));
|
||||
}
|
||||
|
||||
private SubmitContext persistSubmittedChunk(Long taskId, SimilarAsinSubmitResultRequest request) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
@@ -760,13 +783,26 @@ public class SimilarAsinTaskService {
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || STATUS_SUCCESS.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
TaskChunkEntity chunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, context.scopeHash())
|
||||
.eq(TaskChunkEntity::getChunkIndex, context.chunkIndex())
|
||||
.last("limit 1"));
|
||||
if (chunk == null || readChunkRows(chunk).isEmpty()) {
|
||||
List<TaskChunkEntity> chunks;
|
||||
if (context.scopeHash() == null || context.scopeHash().isBlank() || context.chunkIndex() == null) {
|
||||
chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
} else {
|
||||
TaskChunkEntity chunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, context.scopeHash())
|
||||
.eq(TaskChunkEntity::getChunkIndex, context.chunkIndex())
|
||||
.last("limit 1"));
|
||||
chunks = chunk == null ? List.of() : List.of(chunk);
|
||||
}
|
||||
chunks = chunks == null ? List.of() : chunks.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(chunk -> !readChunkRows(chunk).isEmpty())
|
||||
.toList();
|
||||
if (chunks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
FileResultEntity result = findOrCreateResultRecordForAssembly(task, allRowCount(task));
|
||||
@@ -779,24 +815,131 @@ public class SimilarAsinTaskService {
|
||||
return;
|
||||
}
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
boolean pendingCoze = submitCozeBatches(task, result, job, List.of(chunk), allRowsByBaseId);
|
||||
boolean pendingCoze = submitCozeBatches(task, result, job, chunks, allRowsByBaseId);
|
||||
if (pendingCoze) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
} else if (isResultSubmissionComplete(task.getId())) {
|
||||
maybeFinalizeCozeJobLocked(task.getId(), new CozeBatchContext(
|
||||
job.getId(), result.getId(), chunk.getScopeHash(), chunk.getChunkIndex(), 1, 1, currentInstanceId(), 0));
|
||||
job.getId(), result.getId(), context.scopeHash(), context.chunkIndex(), 1, 1, currentInstanceId(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleCozePipelineForSubmittedChunk(SubmitContext context) {
|
||||
if (context == null || context.task() == null || context.task().getId() == null) {
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(context.task().getId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || STATUS_SUCCESS.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
if (chunks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
FileResultEntity result = findOrCreateResultRecordForAssembly(task, allRowCount(task));
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
TaskFileJobEntity job = taskFileJobService.enqueueAssembleResult(
|
||||
task.getId(), MODULE_TYPE, result.getId(), buildTaskOwnerScopeKey(task));
|
||||
if (job == null || "SUCCESS".equals(job.getStatus())) {
|
||||
return;
|
||||
}
|
||||
taskFileJobService.requeue(job.getId(), "Similar ASIN result uploaded, scheduling Coze/file assembly");
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
}
|
||||
|
||||
private void finalizeStaleTask(Long taskId, String error) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !STATUS_RUNNING.equals(task.getStatus())) {
|
||||
return;
|
||||
}
|
||||
if (tryRecoverTimedOutPythonTask(task)) {
|
||||
return;
|
||||
}
|
||||
finalizeTask(task, error, allRowCount(task), true);
|
||||
}
|
||||
|
||||
private boolean tryRecoverTimedOutPythonTask(FileTaskEntity task) {
|
||||
if (task == null || task.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
Long taskId = task.getId();
|
||||
boolean uploadComplete = isResultSubmissionComplete(taskId);
|
||||
long pendingCozeStates = countPendingCozeStates(taskId);
|
||||
long activeAssembleJobs = taskFileJobService.countActiveAssembleJobs(taskId, MODULE_TYPE);
|
||||
if (uploadComplete && (pendingCozeStates > 0 || activeAssembleJobs > 0)) {
|
||||
touchJavaSideTaskActivity(taskId);
|
||||
return true;
|
||||
}
|
||||
if (!hasPersistedResultRows(taskId)) {
|
||||
return false;
|
||||
}
|
||||
if (!uploadComplete) {
|
||||
int forcedScopes = markSubmissionCompleteOnPythonTimeout(taskId);
|
||||
if (forcedScopes <= 0) {
|
||||
return false;
|
||||
}
|
||||
log.warn("[similar-asin] python heartbeat timed out, forcing finalize pipeline taskId={} pendingCozeStates={} activeAssembleJobs={} forcedScopes={}",
|
||||
taskId, pendingCozeStates, activeAssembleJobs, forcedScopes);
|
||||
} else {
|
||||
log.warn("[similar-asin] stale running task resuming coze/file assembly after python timeout taskId={} pendingCozeStates={} activeAssembleJobs={}",
|
||||
taskId, pendingCozeStates, activeAssembleJobs);
|
||||
}
|
||||
submitCozeForSubmittedChunk(new SubmitContext(task, null, null, null, true, null));
|
||||
touchJavaSideTaskActivity(taskId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int markSubmissionCompleteOnPythonTimeout(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
List<TaskScopeStateEntity> inputStates = taskScopeStateMapper.selectList(new LambdaQueryWrapper<TaskScopeStateEntity>()
|
||||
.eq(TaskScopeStateEntity::getTaskId, taskId)
|
||||
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
|
||||
.isNull(TaskScopeStateEntity::getCozeStatus)
|
||||
.orderByDesc(TaskScopeStateEntity::getUpdatedAt));
|
||||
if (inputStates == null || inputStates.isEmpty()) {
|
||||
TaskChunkEntity latestChunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByDesc(TaskChunkEntity::getUpdatedAt)
|
||||
.last("limit 1"));
|
||||
if (latestChunk == null || latestChunk.getScopeHash() == null || latestChunk.getScopeHash().isBlank()) {
|
||||
return 0;
|
||||
}
|
||||
upsertScopeState(taskId,
|
||||
firstNonBlank(latestChunk.getScopeKey(), "task:" + taskId),
|
||||
latestChunk.getScopeHash(),
|
||||
latestChunk.getChunkTotal(),
|
||||
null,
|
||||
true,
|
||||
false);
|
||||
return 1;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int updated = 0;
|
||||
for (TaskScopeStateEntity state : inputStates) {
|
||||
if (state == null || state.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
state.setCompleted(1);
|
||||
if (state.getLastChunkAt() == null) {
|
||||
state.setLastChunkAt(now);
|
||||
}
|
||||
state.setUpdatedAt(now);
|
||||
state.setStateJson("{\"phase\":\"RECEIVED\",\"coze\":\"PENDING\"}");
|
||||
taskScopeStateMapper.updateById(state);
|
||||
updated++;
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
private void upsertScopeState(Long taskId,
|
||||
String scopeKey,
|
||||
String scopeHash,
|
||||
@@ -905,20 +1048,14 @@ public class SimilarAsinTaskService {
|
||||
if (persistedRows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<SimilarAsinResultRowDto> unresolvedRows = pickGroupRepresentativesForCoze(persistedRows.values());
|
||||
List<SimilarAsinResultRowDto> unresolvedRows = collectPendingCozeRows(persistedRows.values());
|
||||
if (unresolvedRows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<SimilarAsinResultRowDto> cozeRows = applyCozeInBatches(unresolvedRows, task, progressHook);
|
||||
Map<String, SimilarAsinResultRowDto> mergedRows = new LinkedHashMap<>();
|
||||
for (SimilarAsinResultRowDto resultRow : cozeRows) {
|
||||
for (SimilarAsinResultRowDto expandedRow : expandRows(List.of(resultRow), allRowsByBaseId)) {
|
||||
mergedRows.put(rowKey(expandedRow), expandedRow);
|
||||
}
|
||||
}
|
||||
mergeChunkPayload(task.getId(), chunk.getScopeHash(), chunk.getChunkIndex(), new ArrayList<>(mergedRows.values()));
|
||||
mergeChunkPayload(task.getId(), chunk.getScopeHash(), chunk.getChunkIndex(), cozeRows);
|
||||
log.info("[similar-asin] async coze chunk merged taskId={} chunk={} unresolved={} merged={}",
|
||||
task.getId(), chunk.getChunkIndex(), unresolvedRows.size(), mergedRows.size());
|
||||
task.getId(), chunk.getChunkIndex(), unresolvedRows.size(), cozeRows.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,44 +1088,22 @@ public class SimilarAsinTaskService {
|
||||
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(oldPayload, storedPayload);
|
||||
}
|
||||
|
||||
private List<SimilarAsinResultRowDto> expandRows(List<SimilarAsinResultRowDto> representatives,
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
|
||||
if (representatives == null || representatives.isEmpty()) {
|
||||
private List<SimilarAsinResultRowDto> expandRows(List<SimilarAsinResultRowDto> rows,
|
||||
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<SimilarAsinResultRowDto> result = new ArrayList<>();
|
||||
Set<String> emittedKeys = new LinkedHashSet<>();
|
||||
for (SimilarAsinResultRowDto representative : representatives) {
|
||||
String key = firstNonBlank(normalize(representative.getRowToken()), rowKey(representative));
|
||||
for (SimilarAsinResultRowDto row : rows) {
|
||||
String key = firstNonBlank(normalize(row.getRowToken()), rowKey(row));
|
||||
if (emittedKeys.add(key)) {
|
||||
result.add(representative);
|
||||
result.add(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SimilarAsinParsedRowVo> resolveSiblingRows(SimilarAsinResultRowDto representative,
|
||||
Map<String, List<SimilarAsinParsedRowVo>> groupedRows) {
|
||||
if (representative == null || groupedRows == null || groupedRows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
String representativeGroupKey = normalize(representative.getGroupKey());
|
||||
if (!representativeGroupKey.isBlank()) {
|
||||
return groupedRows.getOrDefault(representativeGroupKey, List.of());
|
||||
}
|
||||
String representativeKey = rowKey(representative);
|
||||
String representativeLegacyKey = legacyRowKey(representative);
|
||||
for (List<SimilarAsinParsedRowVo> rows : groupedRows.values()) {
|
||||
for (SimilarAsinParsedRowVo row : rows) {
|
||||
if (Objects.equals(representativeKey, rowKey(row))
|
||||
|| Objects.equals(representativeLegacyKey, legacyRowKey(row))) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private Map<String, List<SimilarAsinParsedRowVo>> loadAllRowsByBaseId(FileTaskEntity task) {
|
||||
try {
|
||||
SimilarAsinParsedPayloadDto payload = readParsedPayload(task);
|
||||
@@ -1009,17 +1124,17 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<SimilarAsinResultRowDto> pickGroupRepresentativesForCoze(java.util.Collection<SimilarAsinResultRowDto> rows) {
|
||||
private List<SimilarAsinResultRowDto> collectPendingCozeRows(java.util.Collection<SimilarAsinResultRowDto> rows) {
|
||||
if (rows == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<SimilarAsinResultRowDto> representatives = new ArrayList<>();
|
||||
List<SimilarAsinResultRowDto> pendingRows = new ArrayList<>();
|
||||
for (SimilarAsinResultRowDto row : rows) {
|
||||
if (row != null && !hasResolvedCozeFields(row) && shouldInspectRow(row)) {
|
||||
representatives.add(row);
|
||||
pendingRows.add(row);
|
||||
}
|
||||
}
|
||||
return representatives;
|
||||
return pendingRows;
|
||||
}
|
||||
|
||||
private List<SimilarAsinParsedGroupVo> buildParsedGroups(List<SimilarAsinParsedRowVo> rows) {
|
||||
@@ -1065,6 +1180,7 @@ public class SimilarAsinTaskService {
|
||||
vo.setGroupKey(row.getGroupKey());
|
||||
vo.setAsin(row.getAsin());
|
||||
vo.setCountry(row.getCountry());
|
||||
vo.setSku(row.getSku());
|
||||
vo.setPrice(row.getPrice());
|
||||
vo.setUrl(row.getUrl());
|
||||
vo.setTitle(row.getTitle());
|
||||
@@ -1113,7 +1229,7 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
return request.getItems() == null ? List.of() : request.getItems();
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private int allRowCount(FileTaskEntity task) {
|
||||
@@ -1139,36 +1255,6 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private SimilarAsinResultRowDto copyResultToSibling(SimilarAsinResultRowDto representative, SimilarAsinParsedRowVo sibling) {
|
||||
SimilarAsinResultRowDto row = new SimilarAsinResultRowDto();
|
||||
row.setSourceFileKey(sibling.getSourceFileKey());
|
||||
row.setSourceFilename(sibling.getSourceFilename());
|
||||
row.setRowToken(sibling.getRowToken());
|
||||
row.setGroupKey(sibling.getGroupKey());
|
||||
row.setId(sibling.getDisplayId());
|
||||
row.setAsin(sibling.getAsin());
|
||||
row.setCountry(sibling.getCountry());
|
||||
row.setPrice(firstNonBlank(representative.getPrice(), sibling.getPrice()));
|
||||
if (representative.hasImageUrl()) {
|
||||
row.setUrls(representative.getUrls());
|
||||
} else {
|
||||
row.setUrl(sibling.getUrl());
|
||||
}
|
||||
row.setTitle(firstNonBlank(representative.getTitle(), sibling.getTitle()));
|
||||
row.setError(representative.getError());
|
||||
row.setTitleRisk(representative.getTitleRisk());
|
||||
row.setAppearanceRisk(representative.getAppearanceRisk());
|
||||
row.setPatentRisk(representative.getPatentRisk());
|
||||
row.setConclusion(representative.getConclusion());
|
||||
row.setIsStock(representative.getIsStock());
|
||||
row.setSimilarity(representative.getSimilarity());
|
||||
row.setTitleReason(representative.getTitleReason());
|
||||
row.setAppearanceReason(representative.getAppearanceReason());
|
||||
row.setPatentReason(representative.getPatentReason());
|
||||
row.setDone(representative.getDone());
|
||||
return row;
|
||||
}
|
||||
|
||||
private String readAiPrompt(FileTaskEntity task) {
|
||||
try {
|
||||
return readParsedPayload(task).getAiPrompt();
|
||||
@@ -1291,15 +1377,17 @@ public class SimilarAsinTaskService {
|
||||
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
||||
throw new BusinessException("result file job arguments are incomplete");
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("task not found");
|
||||
}
|
||||
ensureTaskOwnedByCurrentInstance(task, "assemble result file");
|
||||
job.setScopeKey(buildTaskOwnerScopeKey(task));
|
||||
if (!isJobOwnedByCurrentInstance(job)) {
|
||||
log.info("[similar-asin] skip result file job because owner is another instance jobId={} taskId={} owner={} current={}",
|
||||
job.getId(), job.getTaskId(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||
return false;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("task not found");
|
||||
}
|
||||
FileResultEntity result = fileResultMapper.selectById(job.getResultId());
|
||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||
throw new BusinessException("result record not found");
|
||||
@@ -1317,6 +1405,12 @@ public class SimilarAsinTaskService {
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result");
|
||||
return false;
|
||||
}
|
||||
if (STATUS_RUNNING.equals(task.getStatus()) && !isResultSubmissionComplete(task.getId())) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, Math.max(1, cozeWorkUnits), "Waiting for Python upload");
|
||||
return false;
|
||||
}
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 0, "Submitting Coze");
|
||||
boolean pendingCoze = submitCozeBatches(task, result, job, chunks, allRowsByBaseId);
|
||||
if (pendingCoze) {
|
||||
@@ -1325,12 +1419,6 @@ public class SimilarAsinTaskService {
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result");
|
||||
return false;
|
||||
}
|
||||
if (STATUS_RUNNING.equals(task.getStatus()) && !isResultSubmissionComplete(task.getId())) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, Math.max(1, cozeWorkUnits), "Waiting for Python upload");
|
||||
return false;
|
||||
}
|
||||
completeCozeFileJob(task, result, job, totalProgressUnits, cozeWorkUnits);
|
||||
return true;
|
||||
}
|
||||
@@ -1384,7 +1472,7 @@ public class SimilarAsinTaskService {
|
||||
if (persistedRows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<SimilarAsinResultRowDto> unresolvedRows = pickGroupRepresentativesForCoze(persistedRows.values());
|
||||
List<SimilarAsinResultRowDto> unresolvedRows = collectPendingCozeRows(persistedRows.values());
|
||||
if (unresolvedRows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -2232,7 +2320,7 @@ public class SimilarAsinTaskService {
|
||||
if (persistedRows.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int unresolved = pickGroupRepresentativesForCoze(persistedRows.values()).size();
|
||||
int unresolved = collectPendingCozeRows(persistedRows.values()).size();
|
||||
if (unresolved > 0) {
|
||||
total += Math.max(1, (unresolved + batchSize - 1) / batchSize);
|
||||
}
|
||||
@@ -2280,7 +2368,7 @@ public class SimilarAsinTaskService {
|
||||
SimilarAsinParsedPayloadDto parsed = readParsedPayload(task);
|
||||
Map<String, SimilarAsinResultRowDto> resultMap = loadPersistedResultRowsWithRetry(task.getId(), parsed.getAllItems().size());
|
||||
long resolvedRows = parsed.getAllItems().stream()
|
||||
.filter(row -> findResultRow(row, resultMap) != null || findResultRowByAsin(row.getAsin(), resultMap) != null)
|
||||
.filter(row -> findResultRow(row, resultMap) != null)
|
||||
.count();
|
||||
log.info("[similar-asin] assemble workbook taskId={} parsedRows={} resultRows={} resolvedRows={}",
|
||||
task.getId(), parsed.getAllItems().size(), resultMap.size(), resolvedRows);
|
||||
@@ -2381,19 +2469,18 @@ public class SimilarAsinTaskService {
|
||||
int rowIndex = 1;
|
||||
for (SimilarAsinParsedRowVo parsedRow : parsed.getAllItems()) {
|
||||
SimilarAsinResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
||||
if (resultRow == null) {
|
||||
resultRow = findResultRowByAsin(parsedRow.getAsin(), resultMap);
|
||||
}
|
||||
Row row = sheet.createRow(rowIndex++);
|
||||
int col = 0;
|
||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
|
||||
row.createCell(col++).setCellValue(firstNonBlank(resultRow == null ? parsedRow.getSku() : firstNonBlank(resultRow.getSku(), parsedRow.getSku()), ""));
|
||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
|
||||
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null
|
||||
? firstNonBlank(parsedRow.getPrice(), readValueByHeader(parsedRow, "价格", "price"))
|
||||
: firstNonBlank(resultRow.getPrice(), firstNonBlank(parsedRow.getPrice(), readValueByHeader(parsedRow, "价格", "price"))));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getIsStock(), ""));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getSimilarity(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getSimilarity(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getIsConform(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getReason(), ""));
|
||||
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getCategory(), ""));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getStatus(), ""));
|
||||
}
|
||||
workbook.write(fos);
|
||||
workbook.dispose();
|
||||
@@ -2402,26 +2489,6 @@ public class SimilarAsinTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private SimilarAsinResultRowDto findResultRowByAsin(String asin, Map<String, SimilarAsinResultRowDto> resultMap) {
|
||||
String normalizedAsin = normalize(asin).toUpperCase(Locale.ROOT);
|
||||
if (normalizedAsin.isBlank() || resultMap == null || resultMap.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
SimilarAsinResultRowDto fallback = null;
|
||||
for (SimilarAsinResultRowDto row : resultMap.values()) {
|
||||
if (row == null || !normalizedAsin.equals(normalize(row.getAsin()).toUpperCase(Locale.ROOT))) {
|
||||
continue;
|
||||
}
|
||||
if (hasResolvedCozeFields(row)) {
|
||||
return row;
|
||||
}
|
||||
if (fallback == null) {
|
||||
fallback = row;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private String imageUrlCellValue(SimilarAsinResultRowDto resultRow, String fallbackUrl) {
|
||||
if (resultRow == null) {
|
||||
return firstNonBlank(fallbackUrl, "");
|
||||
@@ -2446,6 +2513,7 @@ public class SimilarAsinTaskService {
|
||||
int idCol = findRequiredHeader(headerMap, "id");
|
||||
int asinCol = findRequiredHeader(headerMap, "asin");
|
||||
int countryCol = findRequiredHeader(headerMap, "国家", "country");
|
||||
int skuCol = findOptionalHeaderExact(headerMap, "sku", "seller sku", "seller_sku", "msku", "货号");
|
||||
int priceCol = findOptionalHeaderExact(headerMap, "价格", "price");
|
||||
int urlCol = findOptionalHeaderExact(headerMap,
|
||||
"url", "rul", "link", "image", "img", "pic", "picture",
|
||||
@@ -2489,6 +2557,7 @@ public class SimilarAsinTaskService {
|
||||
vo.setRowToken(buildRowToken(source.getFileKey(), vo.getRowIndex()));
|
||||
vo.setAsin(asin);
|
||||
vo.setCountry(country);
|
||||
vo.setSku(skuCol >= 0 ? cell(row, skuCol, formatter) : "");
|
||||
vo.setPrice(priceCol >= 0 ? cell(row, priceCol, formatter) : "");
|
||||
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
|
||||
vo.setTitle(titleCol >= 0 ? cell(row, titleCol, formatter) : "");
|
||||
@@ -2548,7 +2617,11 @@ public class SimilarAsinTaskService {
|
||||
|| hasUsableCozeField(row.getPatentRisk())
|
||||
|| hasUsableCozeField(row.getConclusion())
|
||||
|| hasUsableCozeField(row.getIsStock())
|
||||
|| hasUsableCozeField(row.getSimilarity());
|
||||
|| hasUsableCozeField(row.getSimilarity())
|
||||
|| hasUsableCozeField(row.getIsConform())
|
||||
|| hasUsableCozeField(row.getReason())
|
||||
|| hasUsableCozeField(row.getCategory())
|
||||
|| hasUsableCozeField(row.getStatus());
|
||||
}
|
||||
|
||||
private boolean hasUsableCozeField(String value) {
|
||||
@@ -2833,9 +2906,9 @@ public class SimilarAsinTaskService {
|
||||
payload.setApiKey(normalize(apiKey));
|
||||
payload.setSourceFiles(sourceFiles == null ? List.of() : sourceFiles);
|
||||
payload.setHeaders(headers == null ? List.of() : headers);
|
||||
payload.setItems(List.of());
|
||||
payload.setItems(allRows == null ? List.of() : new ArrayList<>(allRows));
|
||||
payload.setGroups(groups == null ? List.of() : groups);
|
||||
payload.setAllItems(List.of());
|
||||
payload.setAllItems(allRows == null ? List.of() : new ArrayList<>(allRows));
|
||||
return writeJson(payload, "保存解析结果失败");
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ public class TaskResultFileJobWorker {
|
||||
|
||||
public void process(TaskFileJobEntity job) {
|
||||
if (isOwnerScopedCozeJob(job) && !isOwnedByCurrentInstance(job)) {
|
||||
log.info("[task-file-job] skip owner-scoped job because owner is another instance jobId={} taskId={} moduleType={} owner={} current={}",
|
||||
log.debug("[task-file-job] skip owner-scoped job because owner is another instance jobId={} taskId={} moduleType={} owner={} current={}",
|
||||
job.getId(), job.getTaskId(), job.getModuleType(), ownerFromScopeKey(job.getScopeKey()), currentInstanceId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# 这个文件只是给你对照看的“环境变量配置示例”,不是 Spring Boot 自动加载的配置文件。
|
||||
# 你现在最简单的做法:把下面这些键值复制到 IDEA 的 Run/Debug Configuration -> Environment variables。
|
||||
# This file is only an environment-variable reference for local IDE startup.
|
||||
# Recommended local startup:
|
||||
# 1. Set SPRING_PROFILES_ACTIVE=local
|
||||
# 2. Copy the needed keys below into IDEA Run/Debug Configuration -> Environment variables
|
||||
# 3. Set AIIMAGE_INSTANCE_ID to a stable local value such as local-121
|
||||
|
||||
SPRING_PROFILES_ACTIVE=local
|
||||
AIIMAGE_INSTANCE_ID=local-121
|
||||
AIIMAGE_SERVER_PORT=18080
|
||||
|
||||
AIIMAGE_DB_URL=jdbc:mysql://127.0.0.1:3306/aiimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
|
||||
|
||||
19
backend-java/src/main/resources/application-server.yml
Normal file
19
backend-java/src/main/resources/application-server.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
# Common production override for all servers.
|
||||
# Activate with: SPRING_PROFILES_ACTIVE=server
|
||||
# Always pass AIIMAGE_INSTANCE_ID and Redis connection parameters from
|
||||
# the startup command or panel environment variables.
|
||||
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
username: ${AIIMAGE_REDIS_USERNAME:}
|
||||
host: ${AIIMAGE_REDIS_HOST}
|
||||
port: ${AIIMAGE_REDIS_PORT}
|
||||
password: ${AIIMAGE_REDIS_PASSWORD}
|
||||
database: ${AIIMAGE_REDIS_DATABASE:0}
|
||||
timeout: ${AIIMAGE_REDIS_TIMEOUT:5s}
|
||||
|
||||
aiimage:
|
||||
instance-id: ${AIIMAGE_INSTANCE_ID:}
|
||||
result-file-job:
|
||||
mq-enabled: ${AIIMAGE_RESULT_FILE_JOB_MQ_ENABLED:true}
|
||||
@@ -4,8 +4,6 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: aiimage-backend
|
||||
config:
|
||||
import: optional:classpath:application-local.yml
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 200MB
|
||||
@@ -14,9 +12,9 @@ spring:
|
||||
time-zone: Asia/Shanghai
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: ${AIIMAGE_DB_URL:jdbc:mysql://127.0.0.1:3306/aiimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}
|
||||
url: ${AIIMAGE_DB_URL:jdbc:mysql://47.110.241.161:3306/aiimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}
|
||||
username: ${AIIMAGE_DB_USERNAME:root}
|
||||
password: ${AIIMAGE_DB_PASSWORD:change-me}
|
||||
password: ${AIIMAGE_DB_PASSWORD:WTFrb5y6hNLz6hNy}
|
||||
hikari:
|
||||
maximum-pool-size: ${AIIMAGE_DB_POOL_MAX_SIZE:30}
|
||||
minimum-idle: ${AIIMAGE_DB_POOL_MIN_IDLE:5}
|
||||
@@ -28,9 +26,10 @@ spring:
|
||||
leak-detection-threshold: ${AIIMAGE_DB_POOL_LEAK_DETECT_MS:0}
|
||||
data:
|
||||
redis:
|
||||
host: ${AIIMAGE_REDIS_HOST:127.0.0.1}
|
||||
port: ${AIIMAGE_REDIS_PORT:6379}
|
||||
password: ${AIIMAGE_REDIS_PASSWORD:}
|
||||
username: ${AIIMAGE_REDIS_USERNAME:}
|
||||
host: ${AIIMAGE_REDIS_HOST:47.111.163.154}
|
||||
port: ${AIIMAGE_REDIS_PORT:16379}
|
||||
password: ${AIIMAGE_REDIS_PASSWORD:B6COTcY094TYe545}
|
||||
database: ${AIIMAGE_REDIS_DATABASE:0}
|
||||
timeout: ${AIIMAGE_REDIS_TIMEOUT:5s}
|
||||
|
||||
@@ -72,19 +71,22 @@ knife4j:
|
||||
language: zh_cn
|
||||
|
||||
aiimage:
|
||||
instance-id: ${AIIMAGE_INSTANCE_ID:}
|
||||
# Set a stable server-level ID in 1Panel/Docker with AIIMAGE_INSTANCE_ID.
|
||||
# For IDEA/local startup, set -Daiimage.instance-id=<stable-id> or configure the AIIMAGE_INSTANCE_ID env var.
|
||||
# Avoid relying on container hostname/container ID, otherwise task owner continuity may break after restart/redeploy.
|
||||
instance-id: ${AIIMAGE_INSTANCE_ID:local-dev}
|
||||
oss:
|
||||
region: ${AIIMAGE_OSS_REGION:cn-hangzhou}
|
||||
endpoint: ${AIIMAGE_OSS_ENDPOINT:oss-cn-hangzhou.aliyuncs.com}
|
||||
bucket: ${AIIMAGE_OSS_BUCKET:change-me}
|
||||
access-key-id: ${AIIMAGE_OSS_ACCESS_KEY_ID:change-me}
|
||||
access-key-secret: ${AIIMAGE_OSS_ACCESS_KEY_SECRET:change-me}
|
||||
bucket: ${AIIMAGE_OSS_BUCKET:nanri-ai-images}
|
||||
access-key-id: ${AIIMAGE_OSS_ACCESS_KEY_ID:LTAI5tNpyvzMNz9f2dHarsm8}
|
||||
access-key-secret: ${AIIMAGE_OSS_ACCESS_KEY_SECRET:bQSZnFH455i8tzyOgeahJmUzwmhynz}
|
||||
transient-storage:
|
||||
enabled: ${AIIMAGE_TRANSIENT_STORAGE_ENABLED:false}
|
||||
endpoint: ${AIIMAGE_TRANSIENT_STORAGE_ENDPOINT:}
|
||||
bucket: ${AIIMAGE_TRANSIENT_STORAGE_BUCKET:}
|
||||
access-key-id: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_ID:}
|
||||
access-key-secret: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_SECRET:}
|
||||
enabled: ${AIIMAGE_TRANSIENT_STORAGE_ENABLED:true}
|
||||
endpoint: ${AIIMAGE_TRANSIENT_STORAGE_ENDPOINT:http://121.196.149.225:9000}
|
||||
bucket: ${AIIMAGE_TRANSIENT_STORAGE_BUCKET:json-server}
|
||||
access-key-id: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_ID:YPkyFAymauf21pHMoK0V}
|
||||
access-key-secret: ${AIIMAGE_TRANSIENT_STORAGE_ACCESS_KEY_SECRET:XcPzJqT8jzAHDEQNCovhkPmlwWcphBwLdA36BTzL}
|
||||
region: ${AIIMAGE_TRANSIENT_STORAGE_REGION:us-east-1}
|
||||
connect-timeout-seconds: ${AIIMAGE_TRANSIENT_STORAGE_CONNECT_TIMEOUT_SECONDS:10}
|
||||
read-timeout-seconds: ${AIIMAGE_TRANSIENT_STORAGE_READ_TIMEOUT_SECONDS:60}
|
||||
@@ -131,7 +133,7 @@ aiimage:
|
||||
scope-payload-recovery-max-files: ${AIIMAGE_TASK_SCOPE_PAYLOAD_RECOVERY_MAX_FILES:200}
|
||||
scope-payload-cleanup-cron: ${AIIMAGE_TASK_SCOPE_PAYLOAD_CLEANUP_CRON:15 */30 * * * *}
|
||||
result-file-job:
|
||||
mq-enabled: ${AIIMAGE_RESULT_FILE_JOB_MQ_ENABLED:false}
|
||||
mq-enabled: ${AIIMAGE_RESULT_FILE_JOB_MQ_ENABLED:true}
|
||||
topic: ${AIIMAGE_RESULT_FILE_JOB_TOPIC:aiimage-result-file-job}
|
||||
consumer-group: ${AIIMAGE_RESULT_FILE_JOB_CONSUMER_GROUP:aiimage-result-file-job-consumer}
|
||||
local-dispatch-enabled: ${AIIMAGE_RESULT_FILE_JOB_LOCAL_DISPATCH_ENABLED:true}
|
||||
|
||||
Reference in New Issue
Block a user