diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt index 24a6ee8..ee5d550 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/service/AgentForegroundService.kt @@ -417,6 +417,17 @@ class AgentForegroundService : Service() { }) } + /** + * 把采购员看得懂的文案和内部原因拼在一起:文案在前,原始说法括注在后。 + * + * 内部原因只描述页面形态与归位结果,不含凭据、账号、订单和个人数据(#294)。 + */ + private fun imageSearchFailureMessage(code: String, internal: String): String { + if (!code.startsWith("IMAGE_SEARCH")) return internal + val user = imageSearchUserMessage(code) + return if (internal.isBlank() || internal == user) user else "$user[$internal]" + } + /** #277:图搜任务失败码到人话文案的映射,采购员看得懂即可,不暴露内部状态机细节。 */ private fun imageSearchUserMessage(code: String): String = when (code) { "IMAGE_SEARCH_PERMISSION_REQUIRED" -> "未授予相册权限,无法准备图搜参考图,请到“状态”页开启相册权限后重试。" @@ -763,7 +774,11 @@ class AgentForegroundService : Service() { // `[必须]` 归位与选图是同一个调用:中间多一次无障碍树抓取就可能错过 // 拼多多摄像头自动弹出重试弹窗前的约 4 秒窗口,详见 enterAndSelectImage。 val entry = imageSearch.enterAndSelectImage(::launchPddToForeground, ::resetPddToHome) - if (!entry.successful) throw TaskFailure(entry.code, imageSearchUserMessage(entry.code).takeIf { entry.code.startsWith("IMAGE_SEARCH") } ?: entry.message) + // `[必须]` 保留归位状态机的原始说法。IMAGE_SEARCH_ENTRY_NOT_FOUND 有两个 + // 来源——「找不到唯一可点的拍照搜索入口」和「归位失败(预算耗尽)」—— + // 统一文案会把两者抹平,排查任务 156 时只能靠失败耗时反推(#294)。 + // 括注写法与下面候选点击失败一致。 + if (!entry.successful) throw TaskFailure(entry.code, imageSearchFailureMessage(entry.code, entry.message)) if (!imageSearch.waitForResultsPage()) { throw TaskFailure("IMAGE_SEARCH_RESULTS_NOT_READY", imageSearchUserMessage("IMAGE_SEARCH_RESULTS_NOT_READY")) } diff --git a/server/app/goauto/task/image_search_link.go b/server/app/goauto/task/image_search_link.go index 67fe88a..e2448af 100644 --- a/server/app/goauto/task/image_search_link.go +++ b/server/app/goauto/task/image_search_link.go @@ -3,7 +3,9 @@ package task import ( "context" "errors" + "fmt" "log" + "strings" "go-admin/app/goauto/purchase" @@ -112,9 +114,48 @@ func (service *Service) matchSpecsAfterImageSearch(ctx context.Context, linked a // 先把该虾皮商品的完整颜色尺码补齐,再匹配:只匹配本次明细会留下空洞, // 新订单带来新组合时又得重来(#290)。 service.syncShopeeSpecs(ctx, linked.ShopeeProductID, linked.PDDProductID) - if _, err := purchase.NewService(service.DB).BatchSpecMatch(ctx, purchase.BatchSpecMatchRequest{ + purchaseService := purchase.NewService(service.DB) + response, err := purchaseService.BatchSpecMatch(ctx, purchase.BatchSpecMatchRequest{ SYBProductIDs: linked.SYBProductIDs, - }); err != nil { + }) + if err != nil { log.Printf("image search spec match failed for syb products %v: %v", linked.SYBProductIDs, err) + } else { + // `[必须]` 逐条结果必须落日志。此前这里是 `if _, err := ...`,23 条明细因 AI + // 服务 503 全部失败时日志一个字都没有,真实原因只能靠手工重放接口拿到(#294)。 + // 只记录计数与规格层面的原因,不记录凭据、账号、订单和个人数据。 + log.Printf("image search spec match for shopee product %d: confirmed=%d pending=%d failed=%d skipped=%d; %s", + linked.ShopeeProductID, response.AutoConfirmedCount, response.PendingCount, + response.FailedCount, response.SkippedCount, batchSpecMatchDigest(response)) } + // 同步补进档案的规格值没有对应明细,上面那轮碰不到它们;等订单真来了还得人工 + // 点一次匹配,正是 #290 想消除的动作。这里对档案里所有未映射的值再跑一轮(#293)。 + archive, err := purchaseService.MatchArchiveSpecs(ctx, linked.ShopeeProductID) + if err != nil { + log.Printf("archive spec match failed for shopee product %d: %v", linked.ShopeeProductID, err) + return + } + log.Printf("archive spec match for shopee product %d: confirmed=%d skipped=%d failed=%d", + linked.ShopeeProductID, archive.Confirmed, archive.Skipped, archive.Failed) } + +// batchSpecMatchDigest renders the first few non-successful reasons so a total +// failure says why, without dumping every row (#294). +func batchSpecMatchDigest(response purchase.BatchSpecMatchResponse) string { + reasons := make([]string, 0, specMatchDigestLimit) + for _, item := range response.Items { + if item.Status == purchase.BatchSpecMatchAutoConfirmed || strings.TrimSpace(item.Reason) == "" { + continue + } + reasons = append(reasons, fmt.Sprintf("syb %d %s: %s", item.SYBProductID, item.Status, item.Reason)) + if len(reasons) == specMatchDigestLimit { + break + } + } + if len(reasons) == 0 { + return "no blocking reasons" + } + return strings.Join(reasons, " | ") +} + +const specMatchDigestLimit = 3