fix: order_result_unknown 补充"是否见过支付/待付款页"诊断标记 (#302)

order_result_unknown 目前是全有或全无:parseOrderEvidence 要求订单号、下单
时间、待付款/支付文案同时命中才算 order_created,任何一项缺失就落进同一个
order_result_unknown,无法区分"确实到过支付页只是没读全证据"和"根本没到
那一步"——前者大概率已在 PDD 建了真实订单。

Agent:readOrderResult 采样循环中,只要命中过支付页 Activity 或
unpaidContextVisible(待付款/待支付/去支付文案),记 paymentPageObserved,
与订单号是否解析成功无关,随 order_result_unknown 一起上报。

服务端:PurchaseTask 新增 PaymentPageObservedAt,仅在请求带
paymentPageObserved=true 时写入服务端当前时间;不回填既有 107 笔历史记录,
无法从历史数据反推当时是否见过支付页。

不改判定结果本身,order_created 的四项条件、批量重试逻辑均未动。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
QiuSW
2026-09-17 14:25:47 +08:00
co-authored by Claude Sonnet 5
parent 57f6cec803
commit f634996dad
10 changed files with 156 additions and 8 deletions
@@ -20,7 +20,15 @@ data class FinalConfirmationEvidence(
)
data class PurchaseOrderEvidence(val orderNo: String, val submittedAt: String)
data class PurchaseOrderReadFailure(val code: String, val message: String)
data class PurchaseOrderReadFailure(
val code: String,
val message: String,
// `[必须]` 与是否读到订单号无关:只要采样期间出现过待付款/支付相关文案,
// 就说明大概率已经点通了下单、PDD 侧可能已经建了订单,只是没能读全证据
// (比如返回窗口太短、错过了订单详情页)。给 order_result_unknown 分优先级用,
// 不改变判定结果本身(#302)。
val paymentPageObserved: Boolean = false,
)
class PurchaseLiveException(val code: String, message: String, val actualUnitPriceCent: Long? = null) : IllegalStateException(message)
@@ -340,8 +348,9 @@ class PurchaseLiveAutomation(
/** Reads the order flow with bounded chooser/payment Back actions and one verified PDD foreground request. */
fun readOrderResult(): PurchaseOrderEvidence? {
lastOrderReadFailure = null
var paymentPageObserved = false
fun unknown(code: String, message: String): PurchaseOrderEvidence? {
lastOrderReadFailure = PurchaseOrderReadFailure(code, message)
lastOrderReadFailure = PurchaseOrderReadFailure(code, message, paymentPageObserved)
return null
}
val labels = linkedSetOf<String>()
@@ -399,6 +408,7 @@ class PurchaseLiveAutomation(
// it permits only bounded reading gestures below, never a payment click.
val paymentVisible = isKnownPddPaymentActivity(snapshot) && !orderContextVisible && !unpaidContextVisible
if (paymentVisible) {
paymentPageObserved = true
if (paymentBackAttempts == 0) {
paymentBackAttempts++
if (!driver.backPurchase()) {
@@ -420,6 +430,7 @@ class PurchaseLiveAutomation(
return@repeat
}
consecutivePaymentSamplesAfterBack = 0
if (unpaidContextVisible) paymentPageObserved = true
if (!orderContextVisible && !unpaidContextVisible) {
val entries = orderDetailEntryTargets(snapshot)
if (entries.size > 1) {
@@ -452,6 +463,9 @@ class PurchaseLiveAutomation(
return unknown(failure.code, failure.message)
}
// orderEvidenceFailure 之外的路径(选择器卡住、微信恢复超时等)同样要带上
// 已经采集到的 paymentPageObserved,不能让 unknown() 之外的 return 漏标。
private fun isKnownAndroidWechatChooser(snapshot: UiSnapshot, labels: Collection<String>): Boolean {
val packageKnown = snapshot.packageName in ANDROID_CHOOSER_PACKAGES
val activity = snapshot.activityName.orEmpty()
@@ -77,6 +77,8 @@ data class PurchaseExecutionOutcome(
val pddOrderNo: String? = null,
val orderSubmittedAt: String? = null,
val actualUnitPriceCent: Long? = null,
// 仅 order_result_unknown 有意义:见 PurchaseOrderReadFailure 的说明(#302)。
val paymentPageObserved: Boolean = false,
)
class PurchaseRehearsalExecutor(
@@ -140,6 +142,8 @@ class PurchaseRehearsalExecutor(
live.submitOrderOnce()
null
} catch (error: PurchaseLiveException) {
// paymentPageObserved 留默认 false:此处是 submitOrderOnce() 本身失败
// (提交按钮点击不明确),readOrderResult 还没跑过,谈不上见没见过支付页。
if (irreversibleStarted) PurchaseExecutionOutcome(
"order_result_unknown",
"PURCHASE_ORDER_RESULT_UNKNOWN",
@@ -157,6 +161,7 @@ class PurchaseRehearsalExecutor(
readFailure?.code ?: "PURCHASE_ORDER_RESULT_UNKNOWN",
readFailure?.message ?: "无法确认订单是否创建,请人工检查",
actualUnitPriceCent = observedPrice,
paymentPageObserved = readFailure?.paymentPageObserved ?: false,
)
}
}
@@ -619,6 +619,7 @@ class AgentForegroundService : Service() {
"order_result_unknown",
readFailure?.code ?: "PURCHASE_ORDER_RESULT_UNKNOWN",
readFailure?.message ?: "无法确认订单是否创建,请人工检查",
paymentPageObserved = readFailure?.paymentPageObserved ?: false,
)
} else {
PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = evidence.orderNo, orderSubmittedAt = evidence.submittedAt)
@@ -16,6 +16,10 @@ internal fun purchaseResultPayload(
put("errorCode", outcome.errorCode ?: "AGENT_EXECUTION_ERROR")
put("errorMessage", outcome.message.take(1000))
}
// 仅 order_result_unknown 有意义;服务端只在这个结果类型下读取,其它类型忽略。
if (outcome.resultType == "order_result_unknown" && outcome.paymentPageObserved) {
put("paymentPageObserved", true)
}
outcome.probedSpecs?.let { put("probedSpecs", JSONObject(it)) }
outcome.pddOrderNo?.let { put("pddOrderNo", it) }
outcome.orderSubmittedAt?.let { put("orderSubmittedAt", it) }
@@ -317,6 +317,35 @@ class PurchaseLiveAutomationTest {
assertFalse(driver.clicked.any { it.contains("支付") })
}
// `[必须]` #302:卡在支付页放弃时,paymentPageObserved 必须为 true——这正是
// 「大概率已经下单只是没读到证据」的信号,区分于真的什么都没发生的失败。
@Test
fun `payment repeated failure marks paymentPageObserved so it can be triaged later`() {
val driver = LiveDriver(postSubmitCaptureSequence = List(4) { "payment" })
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg55")
automation.finalConfirmation(input().copy(addressSuffix = "_cg55"), address)
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_PAYMENT_REPEATED", automation.lastOrderReadFailure?.code)
assertTrue(automation.lastOrderReadFailure?.paymentPageObserved ?: false)
}
// 从未出现过支付/待付款文案的失败不该被标记,否则这个字段就没有区分价值了。
@Test
fun `a failure that never saw the payment page leaves paymentPageObserved false`() {
val driver = LiveDriver(postSubmitCaptureSequence = List(15) { "empty" })
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg30")
automation.finalConfirmation(input().copy(addressSuffix = "_cg30"), address)
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_EMPTY_TIMEOUT", automation.lastOrderReadFailure?.code)
assertFalse(automation.lastOrderReadFailure?.paymentPageObserved ?: true)
}
@Test
fun `continuous payment activity still stops at bounded post back samples without payment clicks`() {
val driver = LiveDriver(postSubmitCaptureSequence = List(4) { "payment" })
+10 -6
View File
@@ -138,12 +138,16 @@ type PurchaseTask struct {
StatusVersion uint64 `json:"statusVersion" gorm:"not null;default:1"`
StatusChangedAt time.Time `json:"statusChangedAt" gorm:"not null"`
PDDOrderNo *string `json:"pddOrderNo" gorm:"size:100;index"`
OrderSubmittedAt *time.Time `json:"orderSubmittedAt"`
IrreversibleAt *time.Time `json:"irreversibleAt"`
PaymentReviewStatus string `json:"paymentReviewStatus" gorm:"size:16;not null;default:pending;check:ck_purchase_task_payment_review,payment_review_status IN ('pending','paid','unpaid')"`
PaymentReviewedAt *time.Time `json:"paymentReviewedAt"`
PaymentReviewedBy *uint64 `json:"paymentReviewedBy"`
PDDOrderNo *string `json:"pddOrderNo" gorm:"size:100;index"`
OrderSubmittedAt *time.Time `json:"orderSubmittedAt"`
IrreversibleAt *time.Time `json:"irreversibleAt"`
// PaymentPageObservedAt 仅在 order_result_unknown 时可能有值:采样期间见过
// 待付款/支付相关文案,说明大概率已经在 PDD 建了订单,只是没能读全证据。
// 用于给积压的 order_result_unknown 记录分优先级,不改变判定结果本身(#302)。
PaymentPageObservedAt *time.Time `json:"paymentPageObservedAt"`
PaymentReviewStatus string `json:"paymentReviewStatus" gorm:"size:16;not null;default:pending;check:ck_purchase_task_payment_review,payment_review_status IN ('pending','paid','unpaid')"`
PaymentReviewedAt *time.Time `json:"paymentReviewedAt"`
PaymentReviewedBy *uint64 `json:"paymentReviewedBy"`
TrackingNo *string `json:"trackingNo" gorm:"size:120"`
TrackingCollectedAt *time.Time `json:"trackingCollectedAt"`
+3
View File
@@ -395,6 +395,9 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
a.ErrorCode, a.ErrorMessage = &failureCode, &failureMessage
t.ErrorCode, t.ErrorMessage = &failureCode, &failureMessage
t.ActualUnitPriceCent = req.ActualUnitPriceCent
if req.PaymentPageObserved {
t.PaymentPageObservedAt = &now
}
case "failed":
next = models.PurchaseTaskStatusFailed
a.Status = models.PurchaseAttemptStatusFailed
@@ -153,3 +153,57 @@ func TestSubmitFailedResultRecordsErrorOnAttempt(t *testing.T) {
t.Fatalf("attempt error message not recorded: %+v", attempt.ErrorMessage)
}
}
// `[必须]` 采样期间见过待付款/支付页时必须落库,这是后续按优先级处理积压的
// order_result_unknown 记录的唯一依据(#302)。
func TestSubmitOrderResultUnknownRecordsPaymentPageObserved(t *testing.T) {
db := testDB(t)
f := seed(t, db, liveCaps(), true)
s := testService(db)
task, _ := createLive(t, s, f)
started := startLivePurchaseAfterProbe(t, s, f, task)
if _, err := s.MarkOrderSubmitStarted(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
t.Fatal(err)
}
if _, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{
RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID, ResultType: "order_result_unknown",
ErrorCode: CodeOrderPaymentRepeat, ErrorMessage: paymentRepeatAgentMessage,
PaymentPageObserved: true,
}, f.token); err != nil {
t.Fatal(err)
}
var saved models.PurchaseTask
if err := db.First(&saved, task.ID).Error; err != nil {
t.Fatal(err)
}
if saved.PaymentPageObservedAt == nil {
t.Fatal("paymentPageObserved=true 却没有落库时间戳")
}
}
// 没见过支付页时不能编造一个时间戳——这个字段的价值就在于它只在真见过时才有值。
func TestSubmitOrderResultUnknownLeavesPaymentPageObservedEmptyByDefault(t *testing.T) {
db := testDB(t)
f := seed(t, db, liveCaps(), true)
s := testService(db)
task, _ := createLive(t, s, f)
started := startLivePurchaseAfterProbe(t, s, f, task)
if _, err := s.MarkOrderSubmitStarted(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
t.Fatal(err)
}
if _, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{
RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID, ResultType: "order_result_unknown",
ErrorCode: CodeOrderPaymentRepeat, ErrorMessage: paymentRepeatAgentMessage,
}, f.token); err != nil {
t.Fatal(err)
}
var saved models.PurchaseTask
if err := db.First(&saved, task.ID).Error; err != nil {
t.Fatal(err)
}
if saved.PaymentPageObservedAt != nil {
t.Fatal("未上报 paymentPageObserved 时不该写入时间戳")
}
}
+3
View File
@@ -159,6 +159,9 @@ type ResultRequest struct {
ErrorCode string `json:"errorCode,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
ProbedSpecs json.RawMessage `json:"probedSpecs,omitempty"`
// PaymentPageObserved 仅 order_result_unknown 使用;见 models.PurchaseTask
// 的 PaymentPageObservedAt 说明(#302)。
PaymentPageObserved bool `json:"paymentPageObserved,omitempty"`
}
type SpecDecisionRequest struct {
@@ -0,0 +1,31 @@
package version_local
import (
"runtime"
goautomigrations "go-admin/app/goauto/migrations"
"go-admin/cmd/migrate/migration"
common "go-admin/common/models"
"gorm.io/gorm"
)
func init() {
_, file, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(file), migratePurchaseTaskPaymentPageObserved)
}
// migratePurchaseTaskPaymentPageObserved adds the diagnostic column so
// order_result_unknown records can be prioritized by whether the Agent ever
// saw a payment/unpaid page (#302).
//
// `[必须]` 只加列,不回填。既有 107 笔历史 order_result_unknown 记录无法从数据里
// 反推当时是否见过支付页,留空表示"未知",不能猜成 true 或 false。
func migratePurchaseTaskPaymentPageObserved(db *gorm.DB, version string) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := goautomigrations.Migrate(tx); err != nil {
return err
}
return tx.Create(&common.Migration{Version: version}).Error
})
}