fix(goauto): preserve unknown order evidence (#167)

This commit is contained in:
QiuSW
2026-08-31 14:28:59 +08:00
parent d90bf66cc2
commit ff0cf99be3
11 changed files with 252 additions and 36 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "cn.ilapage.goauto.agent"
minSdk = 23
targetSdk = 34
versionCode = 45
versionName = "0.9.32"
versionCode = 46
versionName = "0.9.33"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -20,6 +20,7 @@ data class FinalConfirmationEvidence(
)
data class PurchaseOrderEvidence(val orderNo: String, val submittedAt: String)
data class PurchaseOrderReadFailure(val code: String, val message: String)
class PurchaseLiveException(val code: String, message: String) : IllegalStateException(message)
@@ -33,6 +34,8 @@ class PurchaseLiveAutomation(
private val pause: (Long) -> Unit = Thread::sleep,
) {
private var submitAttempted = false
var lastOrderReadFailure: PurchaseOrderReadFailure? = null
private set
fun updateShippingAddress(addressSuffix: String): ShippingAddressProof {
if (!addressSuffix.matches(Regex("^_cg[1-9][0-9]*$"))) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "采购任务的地址标记无效")
var snapshot = driver.capture()
@@ -108,6 +111,11 @@ class PurchaseLiveAutomation(
/** Reads the order flow with bounded chooser/payment Back actions and one verified PDD foreground request. */
fun readOrderResult(): PurchaseOrderEvidence? {
lastOrderReadFailure = null
fun unknown(code: String, message: String): PurchaseOrderEvidence? {
lastOrderReadFailure = PurchaseOrderReadFailure(code, message)
return null
}
val labels = linkedSetOf<String>()
var backedOutOfPayment = false
var backedOutOfChooser = false
@@ -119,30 +127,40 @@ class PurchaseLiveAutomation(
val snapshot = driver.capture()
if (snapshot.packageName.isNullOrBlank()) {
consecutiveEmptySnapshots++
if (consecutiveEmptySnapshots >= ORDER_RESULT_MAX_EMPTY_SAMPLES) return null
if (consecutiveEmptySnapshots >= ORDER_RESULT_MAX_EMPTY_SAMPLES) {
return unknown("PURCHASE_ORDER_EMPTY_TIMEOUT", "等待订单页面时无障碍窗口持续为空")
}
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
return@repeat
}
consecutiveEmptySnapshots = 0
val currentLabels = snapshot.nodes.filter { it.visible }.map { it.label }
if (isKnownAndroidWechatChooser(snapshot, currentLabels)) {
if (backedOutOfChooser || !driver.backPurchase()) return null
if (backedOutOfChooser || !driver.backPurchase()) {
return unknown("PURCHASE_ORDER_CHOOSER_BACK_FAILED", "系统应用选择页无法安全返回")
}
backedOutOfChooser = true
pause(500)
return@repeat
}
if (snapshot.packageName == WECHAT_PACKAGE) {
if (!restoredFromWechat) {
if (!driver.bringPddToForeground()) return null
if (!driver.bringPddToForeground()) {
return unknown("PURCHASE_ORDER_WECHAT_RESTORE_FAILED", "从微信恢复拼多多的请求失败")
}
restoredFromWechat = true
pause(500)
return@repeat
}
if (pddObservedAfterWechatRestore || ++wechatRestorePendingSamples >= ORDER_RESULT_WECHAT_RESTORE_MAX_SAMPLES) return null
if (pddObservedAfterWechatRestore || ++wechatRestorePendingSamples >= ORDER_RESULT_WECHAT_RESTORE_MAX_SAMPLES) {
return unknown("PURCHASE_ORDER_WECHAT_RESTORE_TIMEOUT", "从微信恢复拼多多后未在限定时间到达订单页面")
}
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
return@repeat
}
if (snapshot.packageName != PDD_PACKAGE) return null
if (snapshot.packageName != PDD_PACKAGE) {
return unknown("PURCHASE_ORDER_UNEXPECTED_APP", "核单期间出现未授权应用")
}
if (restoredFromWechat) pddObservedAfterWechatRestore = true
val paymentVisible = isKnownPddPaymentActivity(snapshot) ||
currentLabels.any { label -> PAYMENT_MARKERS.any(label::contains) }
@@ -155,16 +173,19 @@ class PurchaseLiveAutomation(
parseOrderEvidence(labels)?.let { return it }
if (paymentVisible && !backedOutOfPayment) {
backedOutOfPayment = true
if (!driver.backPurchase()) return null
if (!driver.backPurchase()) {
return unknown("PURCHASE_ORDER_PAYMENT_BACK_FAILED", "支付页无法安全返回订单详情")
}
pause(500)
} else if (paymentVisible) {
return null
return unknown("PURCHASE_ORDER_PAYMENT_REPEATED", "支付页重复出现,已停止自动核单")
} else if (index > 0 && index % 15 == 0) {
driver.swipePurchase(SwipeDirection.UP, 400)
}
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
}
return null
val failure = orderEvidenceFailure(labels)
return unknown(failure.code, failure.message)
}
private fun isKnownAndroidWechatChooser(snapshot: UiSnapshot, labels: Collection<String>): Boolean {
@@ -369,6 +390,27 @@ class PurchaseLiveAutomation(
return PurchaseOrderEvidence(orderNumbers.single(), output)
}
private fun orderEvidenceFailure(labels: Collection<String>): PurchaseOrderReadFailure {
val text = labels.joinToString(" ")
if (labels.none { label -> ORDER_CONTEXT_MARKERS.any(label::contains) }) {
return PurchaseOrderReadFailure("PURCHASE_ORDER_CONTEXT_NOT_FOUND", "限定时间内没有出现订单详情证据")
}
val orderNumbers = ORDER_NO.findAll(text).map { it.groupValues[1] }.distinct().toList()
if (orderNumbers.isEmpty()) return PurchaseOrderReadFailure("PURCHASE_ORDER_NO_MISSING", "订单详情缺少可读取的订单号")
if (orderNumbers.size > 1) return PurchaseOrderReadFailure("PURCHASE_ORDER_NO_AMBIGUOUS", "订单详情出现多个订单号,无法唯一确认")
val rawTime = ORDER_TIME.find(text)?.groupValues?.get(1)
?: return PurchaseOrderReadFailure("PURCHASE_ORDER_TIME_MISSING", "订单详情缺少可读取的下单时间")
val normalized = rawTime.replace('年', '-').replace('月', '-').replace("日", "").replace('/', '-').replace('.', '-')
val validTime = listOf("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm").any { pattern ->
runCatching { SimpleDateFormat(pattern, Locale.ROOT).apply { isLenient = false }.parse(normalized) }.getOrNull() != null
}
if (!validTime) return PurchaseOrderReadFailure("PURCHASE_ORDER_TIME_INVALID", "订单详情的下单时间格式无法确认")
if (labels.none { label -> UNPAID_MARKERS.any(label::contains) || PAYMENT_MARKERS.any(label::contains) }) {
return PurchaseOrderReadFailure("PURCHASE_ORDER_UNPAID_EVIDENCE_MISSING", "订单详情缺少待付款状态证据")
}
return PurchaseOrderReadFailure("PURCHASE_ORDER_RESULT_UNKNOWN", "订单详情证据不完整,无法唯一确认")
}
private fun fail(code: String, message: String): Nothing = throw PurchaseLiveException(code, message)
private companion object {
@@ -91,13 +91,25 @@ class PurchaseRehearsalExecutor(
live.submitOrderOnce()
null
} catch (error: PurchaseLiveException) {
if (irreversibleStarted) PurchaseExecutionOutcome("order_result_unknown", "PURCHASE_ORDER_RESULT_UNKNOWN", "无法确认订单是否创建,请人工检查")
if (irreversibleStarted) PurchaseExecutionOutcome(
"order_result_unknown",
"PURCHASE_ORDER_RESULT_UNKNOWN",
"无法确认订单是否创建,请人工检查",
actualUnitPriceCent = observedPrice,
)
else failure(error.code, error.message ?: "创建订单前检查失败")
}
PurchaseActionType.READ_ORDER_RESULT -> {
if (!irreversibleStarted) failure("PURCHASE_RULE_INVALID", "尚未进入创建订单边界")
else live.readOrderResult()?.let { PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = it.orderNo, orderSubmittedAt = it.submittedAt, actualUnitPriceCent = observedPrice) }
?: PurchaseExecutionOutcome("order_result_unknown", "PURCHASE_ORDER_RESULT_UNKNOWN", "无法确认订单是否创建,请人工检查")
?: live.lastOrderReadFailure.let { readFailure ->
PurchaseExecutionOutcome(
"order_result_unknown",
readFailure?.code ?: "PURCHASE_ORDER_RESULT_UNKNOWN",
readFailure?.message ?: "无法确认订单是否创建,请人工检查",
actualUnitPriceCent = observedPrice,
)
}
}
}
if (failure != null) {
@@ -559,9 +559,15 @@ class AgentForegroundService : Service() {
val boundaryRequestId = interrupted.orderSubmitRequestId ?: return@forEach
try {
api.markPurchaseOrderSubmitStarted(interrupted.taskId, boundaryRequestId, token)
val evidence = GoAutoAccessibilityService.instance?.let { PurchaseLiveAutomation(it).readOrderResult() }
val automation = GoAutoAccessibilityService.instance?.let(::PurchaseLiveAutomation)
val evidence = automation?.readOrderResult()
val outcome = if (evidence == null) {
PurchaseExecutionOutcome("order_result_unknown", "PURCHASE_ORDER_RESULT_UNKNOWN", "无法确认订单是否创建,请人工检查")
val readFailure = automation?.lastOrderReadFailure
PurchaseExecutionOutcome(
"order_result_unknown",
readFailure?.code ?: "PURCHASE_ORDER_RESULT_UNKNOWN",
readFailure?.message ?: "无法确认订单是否创建,请人工检查",
)
} else {
PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = evidence.orderNo, orderSubmittedAt = evidence.submittedAt)
}
@@ -597,24 +603,6 @@ class AgentForegroundService : Service() {
runningTaskId.set(purchaseStore.activeTaskId())
}
private fun purchaseResultPayload(requestId: String, attemptId: String, outcome: PurchaseExecutionOutcome): String =
JSONObject()
.put("requestId", requestId)
.put("taskAttemptId", attemptId)
.put("resultType", outcome.resultType)
.apply {
if (outcome.resultType == "failed") {
put("errorCode", outcome.errorCode ?: "AGENT_EXECUTION_ERROR")
put("errorMessage", outcome.message.take(1000))
}
outcome.probedSpecs?.let { put("probedSpecs", JSONObject(it)) }
outcome.pddOrderNo?.let { put("pddOrderNo", it) }
outcome.orderSubmittedAt?.let { put("orderSubmittedAt", it) }
outcome.actualUnitPriceCent?.let { put("actualUnitPriceCent", it) }
}
.toString()
private fun executeTask(
api: AgentApiClient,
initialTask: cn.ilapage.goauto.agent.network.AgentTask,
@@ -0,0 +1,24 @@
package cn.ilapage.goauto.agent.service
import cn.ilapage.goauto.agent.automation.PurchaseExecutionOutcome
import org.json.JSONObject
internal fun purchaseResultPayload(
requestId: String,
attemptId: String,
outcome: PurchaseExecutionOutcome,
): String = JSONObject()
.put("requestId", requestId)
.put("taskAttemptId", attemptId)
.put("resultType", outcome.resultType)
.apply {
if (outcome.resultType == "failed" || outcome.resultType == "order_result_unknown") {
put("errorCode", outcome.errorCode ?: "AGENT_EXECUTION_ERROR")
put("errorMessage", outcome.message.take(1000))
}
outcome.probedSpecs?.let { put("probedSpecs", JSONObject(it)) }
outcome.pddOrderNo?.let { put("pddOrderNo", it) }
outcome.orderSubmittedAt?.let { put("orderSubmittedAt", it) }
outcome.actualUnitPriceCent?.let { put("actualUnitPriceCent", it) }
}
.toString()
@@ -185,6 +185,7 @@ class PurchaseLiveAutomationTest {
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_EMPTY_TIMEOUT", automation.lastOrderReadFailure?.code)
assertEquals(15, driver.postSubmitCaptureCount)
assertEquals(0, driver.pddRestoreCount)
assertEquals(0, driver.postSubmitBackCount)
@@ -200,6 +201,7 @@ class PurchaseLiveAutomationTest {
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_UNEXPECTED_APP", automation.lastOrderReadFailure?.code)
assertEquals(1, driver.postSubmitCaptureCount)
assertEquals(0, driver.pddRestoreCount)
assertEquals(0, driver.postSubmitBackCount)
@@ -215,6 +217,7 @@ class PurchaseLiveAutomationTest {
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_CONTEXT_NOT_FOUND", automation.lastOrderReadFailure?.code)
assertEquals(1, driver.pddRestoreCount)
assertEquals(0, driver.postSubmitBackCount)
assertEquals(0, driver.genericSwipes)
@@ -229,6 +232,7 @@ class PurchaseLiveAutomationTest {
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_WECHAT_RESTORE_TIMEOUT", automation.lastOrderReadFailure?.code)
assertEquals(1, driver.pddRestoreCount)
assertEquals(0, driver.genericSwipes)
assertEquals(0, driver.postSubmitBackCount)
@@ -246,6 +250,18 @@ class PurchaseLiveAutomationTest {
assertEquals(0, driver.postSubmitBackCount)
}
@Test
fun `order context without submitted time reports stable missing time stage`() {
val driver = LiveDriver(postSubmitCaptureSequence = listOf("order-no-time"))
val automation = PurchaseLiveAutomation(driver, pause = {})
val address = automation.updateShippingAddress("_cg37")
automation.finalConfirmation(input().copy(addressSuffix = "_cg37"), address)
automation.submitOrderOnce()
assertEquals(null, automation.readOrderResult())
assertEquals("PURCHASE_ORDER_TIME_MISSING", automation.lastOrderReadFailure?.code)
}
@Test
fun `clipped address is revealed by swiping the unique purchase panel`() {
val driver = LiveDriver(addressClipped = true)
@@ -343,6 +359,7 @@ class PurchaseLiveAutomationTest {
node("address-summary", if (savedTransitionHidesSuffix) "已保存的收货信息" else address.substring(address.lastIndexOf("_cg"))),
))
"order" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("time", "下单时间:2026-08-21 10:30:00"), node("pay", "立即支付", clickable = true)))
"order-no-time" -> snapshot(listOf(node("status", "待付款"), node("order", "订单号:PDD-202608210001"), node("pay", "去支付", clickable = true)))
"chooser" -> UiSnapshot(if (trustedChooser) "android" else "example.untrusted", "com.android.internal.app.ChooserActivity", listOf(
node("chooser-title", "选择要使用的应用"), node("wechat-1", "微信"), node("wechat-2", "微信分身"),
))
@@ -0,0 +1,30 @@
package cn.ilapage.goauto.agent
import cn.ilapage.goauto.agent.automation.PurchaseExecutionOutcome
import cn.ilapage.goauto.agent.service.purchaseResultPayload
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
class PurchaseResultPayloadTest {
@Test
fun `unknown order result keeps stable failure and verified price without order fields`() {
val payload = JSONObject(purchaseResultPayload(
requestId = "request-1",
attemptId = "attempt-1",
outcome = PurchaseExecutionOutcome(
resultType = "order_result_unknown",
errorCode = "PURCHASE_ORDER_TIME_MISSING",
message = "订单详情缺少可读取的下单时间",
actualUnitPriceCent = 1999,
),
))
assertEquals("order_result_unknown", payload.getString("resultType"))
assertEquals("PURCHASE_ORDER_TIME_MISSING", payload.getString("errorCode"))
assertEquals(1999L, payload.getLong("actualUnitPriceCent"))
assertFalse(payload.has("pddOrderNo"))
assertFalse(payload.has("orderSubmittedAt"))
}
}
+4 -3
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Android-Agent-API-Contract
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.-
wiki_revision: 484a645058de780abe2ff2951afd25d38a03b74e
synchronized_at: 2026-08-31T03:41:42Z
wiki_revision: 6990b1a92006572069d13bdffba1ffc5bfc0f13d
synchronized_at: 2026-08-31T06:22:58Z
<!-- gitea-wiki-mirror:end -->
<!-- gitea-wiki-mirror:start -->
@@ -544,7 +544,8 @@ Android #42/#36 使用本地 SQLite 保存恢复与重传所需的任务、attem
创建订单前先在本地事务保存 `order_submit_started`、不可逆时间、稳定的 `orderSubmitRequestId` 和脱敏最终确认快照,再调用服务端同名接口;只有两侧标记完成并重新校验商品、规格、数量、单价、地址后缀、PDD 包/Activity 与唯一创建订单按钮后,才点击一次。重启时重放同一标记请求并只读核单;无法取得唯一未付款订单号和 PDD 下单时间时提交 `order_result_unknown`。支付文字仅用于识别未付款/离开支付页,永不点击。服务端数据库仍是最终事实来源;双方均不保存原始控件树、截图、PDD 凭据或完整收货地址。
创建订单后若出现 Android 多微信应用选择器,只读核单器必须同时确认前台包为 `android` / `com.android.intentresolver`、Activity 为白名单 `ChooserActivity` / `ResolverActivity`、页面出现已知系统选择器标题且至少一个候选以“微信”开头,才允许执行一次系统返回;不得点击任何微信候选。若前台已经是精确微信包 `com.tencent.mm`,只读核单器不得点击、输入、登录、支付或强制停止微信,只允许执行一次无参数 PDD 启动 Intent 并等待既有 PDD 任务栈回到前台;拉起失败或第二次仍见微信立即返回未知结果。随后若前台为 PDD `com.xunmeng.pinduoduo.app_pay.core.PayActivity` 或当前页面出现支付动作文字,最多再返回一次。返回后只读解析唯一订单号和下单时间;选择器或支付页重复出现、白名单不成立、恢复动作重复、无法到达订单详情、结果不唯一或超时均返回未知结果,禁止再次点击创建订单、取消订单或支付。
创建订单后若出现 Android 多微信应用选择器,只读核单器必须同时确认前台包为 `android` / `com.android.intentresolver`、Activity 为白名单 `ChooserActivity` / `ResolverActivity`、页面出现已知系统选择器标题且至少一个候选以“微信”开头,才允许执行一次系统返回;不得点击任何微信候选。若前台已经是精确微信包 `com.tencent.mm`,只读核单器不得点击、输入、登录、支付或强制停止微信,只允许执行一次无参数 PDD 启动 Intent 并等待既有 PDD 任务栈回到前台;`startActivity()` 成功只表示恢复请求已发起。请求后在固定最多 15 次、每次 200ms 的宽限期内允许微信或空窗口短暂残留,不执行点击、返回、输入或滑动;观察到 PDD 后结束宽限,宽限超时、已经观察到 PDD 后再次进入微信或出现稳定未知应用时返回未知结果。随后若前台为 PDD `com.xunmeng.pinduoduo.app_pay.core.PayActivity` 或当前页面出现支付动作文字,最多再返回一次。返回后只读解析唯一订单号和下单时间;选择器或支付页重复出现、白名单不成立、恢复动作重复、无法到达订单详情、结果不唯一或超时均返回未知结果,禁止再次点击创建订单、取消订单或支付。
`order_result_unknown` 保持订单号和下单时间为空,但允许携带创建订单前已经严格验证的 `actualUnitPriceCent`。Agent 同时提交脱敏稳定失败阶段,服务端只接受白名单并按错误码写入固定提示,不信任或保存页面原文;阶段覆盖空窗口超时、选择器返回失败、微信恢复失败/超时、未知应用、支付页返回失败/重复、订单上下文缺失、订单号缺失/歧义、下单时间缺失/无效和待付款证据缺失。任务与 attempt 保存同一错误阶段,Admin 和 Agent 历史读取数据库最终事实;旧 Agent 未提交阶段时归一为 `PURCHASE_ORDER_RESULT_UNKNOWN`。
| 错误码 | 普通提示 |
|---|---|
+39
View File
@@ -369,8 +369,18 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
if t.ExecutionMode != models.PurchaseExecutionModeLive || t.Status != models.PurchaseTaskStatusOrderSubmitStarted {
return TaskPayload{}, fail(CodeStateConflict, "当前任务不能标记订单结果未知")
}
if strings.TrimSpace(req.PDDOrderNo) != "" || req.OrderSubmittedAt != nil {
return TaskPayload{}, fail(CodeInvalidRequest, "订单结果未知时不能提交订单号或下单时间")
}
failureCode, failureMessage, failureErr := normalizeOrderUnknownFailure(req.ErrorCode, req.ErrorMessage)
if failureErr != nil {
return TaskPayload{}, failureErr
}
next = models.PurchaseTaskStatusOrderResultUnknown
a.Status = models.PurchaseAttemptStatusFailed
a.ErrorCode, a.ErrorMessage = &failureCode, &failureMessage
t.ErrorCode, t.ErrorMessage = &failureCode, &failureMessage
t.ActualUnitPriceCent = req.ActualUnitPriceCent
case "failed":
next = models.PurchaseTaskStatusFailed
a.Status = models.PurchaseAttemptStatusFailed
@@ -406,6 +416,35 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
return s.resolveProbedSpecs(ctx, taskID, req.TaskAttemptID, req.ProbedSpecs)
}
var allowedOrderUnknownFailures = map[string]string{
CodeOrderResultUnknown: "无法确认订单是否创建,请人工检查",
CodeOrderEmptyTimeout: "等待订单页面时无障碍窗口持续为空",
CodeOrderChooserBack: "系统应用选择页无法安全返回",
CodeOrderWechatRestore: "从微信恢复拼多多的请求失败",
CodeOrderWechatTimeout: "从微信恢复拼多多后未在限定时间到达订单页面",
CodeOrderUnexpectedApp: "核单期间出现未授权应用",
CodeOrderPaymentBack: "支付页无法安全返回订单详情",
CodeOrderPaymentRepeat: "支付页重复出现,已停止自动核单",
CodeOrderContextMissing: "限定时间内没有出现订单详情证据",
CodeOrderNoMissing: "订单详情缺少可读取的订单号",
CodeOrderNoAmbiguous: "订单详情出现多个订单号,无法唯一确认",
CodeOrderTimeMissing: "订单详情缺少可读取的下单时间",
CodeOrderTimeInvalid: "订单详情的下单时间格式无法确认",
CodeOrderUnpaidMissing: "订单详情缺少待付款状态证据",
}
func normalizeOrderUnknownFailure(code, message string) (string, string, error) {
code, message = strings.TrimSpace(code), strings.TrimSpace(message)
if code == "" && message == "" {
return CodeOrderResultUnknown, "无法确认订单是否创建,请人工检查", nil
}
canonicalMessage, ok := allowedOrderUnknownFailures[code]
if !ok || message == "" {
return "", "", fail(CodeInvalidRequest, "订单核单失败阶段无效")
}
return code, canonicalMessage, nil
}
func (s *Service) ApplySpecDecision(ctx context.Context, taskID uint64, req SpecDecisionRequest) (models.PurchaseTask, bool, error) {
operatorID := req.OperatorID
return s.applySpecDecision(ctx, taskID, req, &operatorID)
+50 -1
View File
@@ -478,15 +478,64 @@ func TestOrderUnknownIsNotAutomaticallyRedispatched(t *testing.T) {
s.Claim(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
started, _ := s.Start(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
s.MarkOrderSubmitStarted(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
if _, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID, ResultType: "order_result_unknown"}, f.token); err != nil {
actual := int64(1999)
if _, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{
RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID, ResultType: "order_result_unknown",
ActualUnitPriceCent: &actual, ErrorCode: CodeOrderTimeMissing, ErrorMessage: "订单详情缺少可读取的下单时间",
}, 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.ActualUnitPriceCent == nil || *saved.ActualUnitPriceCent != actual || saved.PDDOrderNo != nil || saved.OrderSubmittedAt != nil || saved.ErrorCode == nil || *saved.ErrorCode != CodeOrderTimeMissing {
t.Fatalf("unknown task evidence not preserved: %+v", saved)
}
var attempt models.PurchaseTaskAttempt
if err := db.Where("task_id = ?", task.ID).First(&attempt).Error; err != nil {
t.Fatal(err)
}
if attempt.ErrorCode == nil || *attempt.ErrorCode != CodeOrderTimeMissing || attempt.ErrorMessage == nil {
t.Fatalf("unknown attempt failure not preserved: %+v", attempt)
}
next, err := s.Next(context.Background(), f.token)
if err != nil || next != nil {
t.Fatalf("unknown task redispatched: %+v %v", next, err)
}
}
func TestOrderUnknownRejectsOrderFieldsAndUnapprovedFailureCode(t *testing.T) {
db := testDB(t)
f := seed(t, db, liveCaps(), true)
s := testService(db)
task, _ := createLive(t, s, f)
s.Claim(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
started, _ := s.Start(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
s.MarkOrderSubmitStarted(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token)
if _, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{
RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID, ResultType: "order_result_unknown",
ErrorCode: "UNAPPROVED", ErrorMessage: "invalid",
}, f.token); code(err) != CodeInvalidRequest {
t.Fatalf("unapproved code error=%v", err)
}
submitted := time.Now()
if _, err := s.SubmitResult(context.Background(), task.ID, ResultRequest{
RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID, ResultType: "order_result_unknown",
PDDOrderNo: "PDD-should-not-save", OrderSubmittedAt: &submitted,
}, f.token); code(err) != CodeInvalidRequest {
t.Fatalf("unknown order fields error=%v", err)
}
}
func TestNormalizeOrderUnknownFailureKeepsLegacyAgentCompatibility(t *testing.T) {
gotCode, gotMessage, err := normalizeOrderUnknownFailure("", "")
if err != nil || gotCode != CodeOrderResultUnknown || gotMessage == "" {
t.Fatalf("legacy unknown normalization code=%q message=%q err=%v", gotCode, gotMessage, err)
}
}
func TestClaimLocksKnownPDDAccountAcrossDevices(t *testing.T) {
db := testDB(t)
first := seed(t, db, liveCaps(), true)
+14
View File
@@ -21,6 +21,20 @@ const (
CodeRetryUnsafe = "PURCHASE_RETRY_UNSAFE"
CodeRetryStale = "PURCHASE_RETRY_STALE"
CodeSpecReprobeRejected = "PURCHASE_SPEC_REPROBE_REJECTED"
CodeOrderResultUnknown = "PURCHASE_ORDER_RESULT_UNKNOWN"
CodeOrderEmptyTimeout = "PURCHASE_ORDER_EMPTY_TIMEOUT"
CodeOrderChooserBack = "PURCHASE_ORDER_CHOOSER_BACK_FAILED"
CodeOrderWechatRestore = "PURCHASE_ORDER_WECHAT_RESTORE_FAILED"
CodeOrderWechatTimeout = "PURCHASE_ORDER_WECHAT_RESTORE_TIMEOUT"
CodeOrderUnexpectedApp = "PURCHASE_ORDER_UNEXPECTED_APP"
CodeOrderPaymentBack = "PURCHASE_ORDER_PAYMENT_BACK_FAILED"
CodeOrderPaymentRepeat = "PURCHASE_ORDER_PAYMENT_REPEATED"
CodeOrderContextMissing = "PURCHASE_ORDER_CONTEXT_NOT_FOUND"
CodeOrderNoMissing = "PURCHASE_ORDER_NO_MISSING"
CodeOrderNoAmbiguous = "PURCHASE_ORDER_NO_AMBIGUOUS"
CodeOrderTimeMissing = "PURCHASE_ORDER_TIME_MISSING"
CodeOrderTimeInvalid = "PURCHASE_ORDER_TIME_INVALID"
CodeOrderUnpaidMissing = "PURCHASE_ORDER_UNPAID_EVIDENCE_MISSING"
CodeInternal = "INTERNAL_ERROR"
)