feat(android): import private Shopee probe tasks

This commit is contained in:
QiuSW
2026-07-25 19:06:28 +08:00
parent d57c4ae2d4
commit 1c1c158149
25 changed files with 1034 additions and 34 deletions
@@ -0,0 +1,16 @@
plugins {
id("org.jetbrains.kotlin.jvm")
}
kotlin {
jvmToolchain(17)
}
dependencies {
implementation("org.json:json:20231013")
testImplementation("junit:junit:4.13.2")
}
tasks.test {
useJUnit()
}
@@ -0,0 +1,56 @@
package com.roubao.task
private val SHA256_PATTERN = Regex("[0-9a-f]{64}")
data class ProbeReferenceImage(
val relativePath: String,
val mediaType: String,
val sizeBytes: Long,
val sha256: String
) {
init {
require(isSafeRelativePath(relativePath)) {
"Reference image path must be a safe relative path"
}
require(mediaType == "image/jpeg") { "Only JPEG reference images are supported" }
require(sizeBytes > 0) { "Reference image size must be positive" }
require(SHA256_PATTERN.matches(sha256)) { "Reference image SHA-256 is invalid" }
}
}
data class ProbeTask(
val schemaVersion: Int = CURRENT_SCHEMA_VERSION,
val probeId: String,
val sourceOrderNo: String,
val sourceStoreName: String,
val title: String,
val sku: String,
val quantity: Int,
val referenceImage: ProbeReferenceImage
) {
init {
require(schemaVersion == CURRENT_SCHEMA_VERSION) {
"Unsupported ProbeTask schema version"
}
require(probeId.isNotBlank()) { "Probe task id must not be blank" }
require(sourceOrderNo.isNotBlank()) { "Source order number must not be blank" }
require(sourceStoreName.isNotBlank()) { "Source store name must not be blank" }
require(title.isNotBlank()) { "Title must not be blank" }
require(sku.isNotBlank()) { "SKU must not be blank" }
require(quantity > 0) { "Quantity must be positive" }
}
companion object {
const val CURRENT_SCHEMA_VERSION = 1
}
}
private fun isSafeRelativePath(path: String): Boolean {
if (path.isBlank() || path.startsWith("/") || path.startsWith("\\")) {
return false
}
if ('\\' in path) {
return false
}
return path.split('/').none { it.isBlank() || it == "." || it == ".." }
}
@@ -0,0 +1,62 @@
package com.roubao.task
import org.json.JSONArray
import org.json.JSONObject
object ProbeTaskJson {
fun encode(tasks: Collection<ProbeTask>): String {
val root = JSONObject()
root.put("schema_version", ProbeTask.CURRENT_SCHEMA_VERSION)
root.put("tasks", JSONArray().apply {
tasks.forEach { put(it.toJson()) }
})
return root.toString(2) + "\n"
}
fun decode(payload: String): List<ProbeTask> {
val root = JSONObject(payload)
require(root.getInt("schema_version") == ProbeTask.CURRENT_SCHEMA_VERSION) {
"Unsupported ProbeTask document schema version"
}
val tasks = root.getJSONArray("tasks")
return buildList(tasks.length()) {
for (index in 0 until tasks.length()) {
add(tasks.getJSONObject(index).toProbeTask())
}
}
}
private fun ProbeTask.toJson(): JSONObject = JSONObject()
.put("probe_id", probeId)
.put("source_order_no", sourceOrderNo)
.put("source_store_name", sourceStoreName)
.put("title", title)
.put("sku", sku)
.put("quantity", quantity)
.put(
"reference_image",
JSONObject()
.put("relative_path", referenceImage.relativePath)
.put("media_type", referenceImage.mediaType)
.put("size_bytes", referenceImage.sizeBytes)
.put("sha256", referenceImage.sha256)
)
private fun JSONObject.toProbeTask(): ProbeTask {
val image = getJSONObject("reference_image")
return ProbeTask(
probeId = getString("probe_id"),
sourceOrderNo = getString("source_order_no"),
sourceStoreName = getString("source_store_name"),
title = getString("title"),
sku = getString("sku"),
quantity = getInt("quantity"),
referenceImage = ProbeReferenceImage(
relativePath = image.getString("relative_path"),
mediaType = image.getString("media_type"),
sizeBytes = image.getLong("size_bytes"),
sha256 = image.getString("sha256")
)
)
}
}
@@ -0,0 +1,5 @@
package com.roubao.task
fun interface TaskSource {
suspend fun nextTask(): ProbeTask?
}
@@ -0,0 +1,26 @@
package com.roubao.task
import org.junit.Assert.assertEquals
import org.junit.Test
class ProbeTaskJsonTest {
@Test
fun `task document round trips without changing authoritative fields`() {
val task = ProbeTask(
probeId = "probe_0123456789abcdef",
sourceOrderNo = "ORDER_000001",
sourceStoreName = "Test Store",
title = "Test Product",
sku = "SKU-BLACK",
quantity = 2,
referenceImage = ProbeReferenceImage(
relativePath = "assets/0123456789abcdef.jpg",
mediaType = "image/jpeg",
sizeBytes = 123,
sha256 = "a".repeat(64)
)
)
assertEquals(listOf(task), ProbeTaskJson.decode(ProbeTaskJson.encode(listOf(task))))
}
}