diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b972cba..7450d76 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "cn.ilapage.goauto.agent" minSdk = 23 targetSdk = 34 - versionCode = 47 - versionName = "0.9.34" + versionCode = 48 + versionName = "0.9.35" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/java/cn/ilapage/goauto/agent/network/AgentApiClient.kt b/android/app/src/main/java/cn/ilapage/goauto/agent/network/AgentApiClient.kt index 517c37e..73da5c5 100644 --- a/android/app/src/main/java/cn/ilapage/goauto/agent/network/AgentApiClient.kt +++ b/android/app/src/main/java/cn/ilapage/goauto/agent/network/AgentApiClient.kt @@ -456,7 +456,7 @@ class AgentApiClient(private val serverUrl: String) { val specs = specsJson.keys().asSequence().associateWith(specsJson::getString) HistorySku(specs, item.getLong("priceCent"), item.getBoolean("available"), item.getBoolean("complete")) }, - missing = data.getJSONArray("missing").strings(), + missing = data.stringArrayOrEmpty("missing"), replacementEligible = data.optBoolean("replacementEligible"), replacementDisabledReason = data.nullableString("replacementDisabledReason"), replacementMappingStatus = data.nullableString("replacementMappingStatus"), @@ -624,3 +624,5 @@ private fun JSONArray.objects(mapper: (JSONObject) -> T): List = (0 until private fun JSONArray.strings(): List = (0 until length()).map(::getString) private fun JSONObject.nullableString(key: String): String? = if (!has(key) || isNull(key)) null else getString(key) private fun JSONObject.nullableLong(key: String): Long? = if (!has(key) || isNull(key)) null else getLong(key) + +internal fun JSONObject.stringArrayOrEmpty(key: String): List = optJSONArray(key)?.strings().orEmpty() diff --git a/android/app/src/test/java/cn/ilapage/goauto/agent/AgentApiClientJsonTest.kt b/android/app/src/test/java/cn/ilapage/goauto/agent/AgentApiClientJsonTest.kt new file mode 100644 index 0000000..2418f7c --- /dev/null +++ b/android/app/src/test/java/cn/ilapage/goauto/agent/AgentApiClientJsonTest.kt @@ -0,0 +1,22 @@ +package cn.ilapage.goauto.agent + +import cn.ilapage.goauto.agent.network.stringArrayOrEmpty +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Test + +class AgentApiClientJsonTest { + @Test + fun `missing arrays preserve values`() { + val data = JSONObject().put("missing", JSONArray().put("size").put("price:red")) + + assertEquals(listOf("size", "price:red"), data.stringArrayOrEmpty("missing")) + } + + @Test + fun `missing null or absent values become an empty list`() { + assertEquals(emptyList(), JSONObject().put("missing", JSONObject.NULL).stringArrayOrEmpty("missing")) + assertEquals(emptyList(), JSONObject().stringArrayOrEmpty("missing")) + } +} diff --git a/server/app/goauto/apprelease/apk_test.go b/server/app/goauto/apprelease/apk_test.go index 46002e5..51b5d83 100644 --- a/server/app/goauto/apprelease/apk_test.go +++ b/server/app/goauto/apprelease/apk_test.go @@ -18,7 +18,7 @@ func TestParseBuiltAgentAPKWhenAvailable(t *testing.T) { if err != nil { t.Fatal(err) } - if metadata.VersionCode != 41 || metadata.VersionName != "0.9.28" { + if metadata.VersionCode != 48 || metadata.VersionName != "0.9.35" { t.Fatalf("metadata=%+v", metadata) } } diff --git a/server/app/goauto/task/admin_result_test.go b/server/app/goauto/task/admin_result_test.go index 2188c56..1c3df1b 100644 --- a/server/app/goauto/task/admin_result_test.go +++ b/server/app/goauto/task/admin_result_test.go @@ -262,6 +262,31 @@ func TestNormalizeResultRejectsCurrencyColorWithPricesAndSKUs(t *testing.T) { } } +func TestNormalizeResultPreservesEmptyMissingArray(t *testing.T) { + normalized := normalizeResultRequest(ResultRequest{Status: models.TaskStatusCompleted, Missing: []string{}}) + if normalized.Request.Missing == nil || len(normalized.Request.Missing) != 0 { + t.Fatalf("empty missing array degraded to nil: %#v", normalized.Request.Missing) + } + raw, err := json.Marshal(normalized.Request.Missing) + if err != nil || string(raw) != "[]" { + t.Fatalf("empty missing array serialized unexpectedly: %s %v", raw, err) + } +} + +func TestDecodeMissingValuesNormalizesLegacyNullAndInvalidJSON(t *testing.T) { + empty, jsonNull, invalid, populated := "[]", "null", "{", `["size"]` + for name, value := range map[string]*string{ + "sql-null": nil, "empty": &empty, "json-null": &jsonNull, "invalid": &invalid, + } { + if decoded := decodeMissingValues(value); decoded == nil || len(decoded) != 0 { + t.Fatalf("%s did not normalize to a non-nil empty array: %#v", name, decoded) + } + } + if decoded := decodeMissingValues(&populated); len(decoded) != 1 || decoded[0] != "size" { + t.Fatalf("populated missing values changed: %#v", decoded) + } +} + func TestResetRejectsDeletedRuleWithoutClearingResult(t *testing.T) { db := openTaskDatabase(t) deviceRecord, _ := registerTaskDevice(t, db, "device-one") diff --git a/server/app/goauto/task/agent_history.go b/server/app/goauto/task/agent_history.go index 9148c72..e379434 100644 --- a/server/app/goauto/task/agent_history.go +++ b/server/app/goauto/task/agent_history.go @@ -2,7 +2,6 @@ package task import ( "context" - "encoding/json" "errors" "strconv" "strings" @@ -187,12 +186,7 @@ func (service *Service) AgentHistoryDetail(ctx context.Context, taskID uint64, t } func agentCollectionItem(record models.CollectionTask) AgentCollectionItem { - missing := 0 - if record.MissingJSON != nil { - var values []string - _ = json.Unmarshal([]byte(*record.MissingJSON), &values) - missing = len(values) - } + missing := len(decodeMissingValues(record.MissingJSON)) return AgentCollectionItem{ TaskID: record.ID, AttemptNumber: record.AttemptNumber, Status: record.Status, Source: record.Source, GoodsID: record.GoodsIDSnapshot, Title: record.Title, MissingCount: missing, ErrorCode: record.ErrorCode, ErrorMessage: record.ErrorMessage, diff --git a/server/app/goauto/task/agent_history_test.go b/server/app/goauto/task/agent_history_test.go index 5e3c6da..cc82556 100644 --- a/server/app/goauto/task/agent_history_test.go +++ b/server/app/goauto/task/agent_history_test.go @@ -73,3 +73,25 @@ func TestAgentHistoryRespectsRequestedDays(t *testing.T) { t.Fatalf("invalid days was accepted: %v", err) } } + +func TestAgentHistoryDetailReturnsArrayForLegacyNullMissing(t *testing.T) { + db := openTaskDatabase(t) + deviceRecord, token := registerTaskDevice(t, db, "history-null-missing") + record := createTask(t, db, &deviceRecord.ID) + legacyNull := "null" + if err := db.Model(&record).Update("missing_json", legacyNull).Error; err != nil { + t.Fatal(err) + } + + detail, err := newTaskService(db).AgentHistoryDetail(context.Background(), record.ID, token) + if err != nil { + t.Fatal(err) + } + if detail.Missing == nil || len(detail.Missing) != 0 { + t.Fatalf("legacy null missing was not normalized: %#v", detail.Missing) + } + raw, err := json.Marshal(detail) + if err != nil || !strings.Contains(string(raw), `"missing":[]`) { + t.Fatalf("history detail did not preserve array contract: %s %v", raw, err) + } +} diff --git a/server/app/goauto/task/result_service.go b/server/app/goauto/task/result_service.go index 8371c36..57d8779 100644 --- a/server/app/goauto/task/result_service.go +++ b/server/app/goauto/task/result_service.go @@ -208,7 +208,7 @@ func normalizeResultRequest(request ResultRequest) normalizedCollectionResult { result.Dimensions = make([]ResultDimension, 0, len(request.Dimensions)) result.ColorPrices = append([]ResultColorPrice(nil), request.ColorPrices...) result.SKUs = make([]ResultSKU, 0, len(request.SKUs)) - result.Missing = append([]string(nil), request.Missing...) + result.Missing = append(make([]string, 0, len(request.Missing)), request.Missing...) keyCounts := map[string]int{} for _, dimension := range request.Dimensions { @@ -543,12 +543,22 @@ func (service *Service) Detail(ctx context.Context, taskID uint64) (DetailRespon } result.SKUs = append(result.SKUs, DetailSKU{ID: sku.ID, Specs: specs, PriceCent: sku.PriceCent, Available: sku.Available, Complete: sku.Complete}) } - if item.MissingJSON != nil { - _ = json.Unmarshal([]byte(*item.MissingJSON), &result.Missing) - } + result.Missing = decodeMissingValues(item.MissingJSON) return result, nil } +func decodeMissingValues(raw *string) []string { + values := make([]string, 0) + if raw == nil { + return values + } + var decoded []string + if json.Unmarshal([]byte(*raw), &decoded) != nil || decoded == nil { + return values + } + return decoded +} + func validateResultRequest(taskID uint64, request ResultRequest) error { if taskID == 0 || uuid.Validate(request.RequestID) != nil || request.Product.PDDGoodsID == "" { return serviceError("INVALID_REQUEST", "结果 requestId、taskId 或 goods_id 无效")