...

先分析一下现有代码的问题,然后给出重构方案。

## 分析现有问题

1. `acceptMap["cases"] as List<MutableMap<String, Any?>>` — 用 `Map` 直接当 DTO,类型不安全且可读性差
2. `it.remove("xxx")` — 用 map 的 remove 来"解构"字段,非常脆弱
3. `upsert(acceptMap, proId)` — 原来的 upsert 接收 `Map`,现在已改成接收 DTO,所以 `createCases` 里的调用也需要同步修改
4. batch 的写法冗长,null 占位符满天飞

---

## 重构方案

### 1. DTO 定义

`CreateOrUpdateTestJobRequest` **可以直接复用**,`CreateCasesRequest` 组合它即可:

```kotlin
package dto

import com.fasterxml.jackson.annotation.JsonAlias

data class TestCaseResultRequest(
    val name: String?,
    val result: Int = 0,
    val author: String?,
    val uri: String?,
    @field:JsonAlias("testcaseproject")
    val testcaseProject: String?,
    @field:JsonAlias("domain_name")
    val domainName: String?,
    @field:JsonAlias("subdomain_name")
    val subDomainName: String?,
    // 剩余未知字段作为附加数据
    val extra: Map<String, Any?> = emptyMap(),
) {
    // uri 最后一段,e.g. "/a/b/c" -> "c"
    val realUri: String?
        get() = uri?.trim('/')?.substringAfterLast('/')?.ifEmpty { null }
}

data class CreateCasesRequest(
    // 直接复用,Jackson 会自动映射同名字段
    val jobId: Long,
    @field:JsonAlias("platform")
    val jobType: Short = 0,
    // 复用 job 信息(可选字段按需填写)
    val job: CreateOrUpdateTestJobRequest? = null,
    val imageUrl: String?,
    val cases: List<TestCaseResultRequest>,
)
```

> **关于复用 `CreateOrUpdateTestJobRequest`**:请求体里 job 相关字段和 cases 混在一起,建议在 `CreateCasesRequest` 里直接平铺 job 字段(或嵌套一个 `job` 对象),取决于调用方的报文结构。这里按"平铺"方式,让 Jackson 直接反序列化整个请求体到 `CreateCasesRequest`,`job` 内嵌一个 `CreateOrUpdateTestJobRequest`。

---

### 2. 重写 `createCases`

```kotlin
@Post("/cases")
fun createCases(body: Json<CreateCasesRequest>): String {
    val req = body.value

    // 1. 查或创建 TestJob
    val testJob = jobRepo.findByJobIdAndJobType(req.jobId, req.jobType)
        ?: run {
            val proId = productVersionRepo.resolveProductVersionId(req.imageUrl)
            // CreateOrUpdateTestJobRequest 直接从 req 里构造,字段对齐
            val jobReq = requireNotNull(req.job) { "新 job 必须提供 job 信息" }
            jobRepo.upsert(jobReq, proId)
        }

    // 2. 批量插入 TestCaseResult
    val now = LocalDateTime.now()
    dsl.batch(
        req.cases.map { case ->
            dsl.insertInto(TESTCASERESULT)
                .set(TESTCASERESULT.CASE,            case.name)
                .set(TESTCASERESULT.RESULT,          case.result)
                .set(TESTCASERESULT.PLATFORM,        req.jobType.toInt())
                .set(TESTCASERESULT.CREATE_TIME,     now)
                .set(TESTCASERESULT.REAL_URI,        case.realUri)
                .set(TESTCASERESULT.AUTHOR,          case.author)
                .set(TESTCASERESULT.URI,             case.uri)
                .set(TESTCASERESULT.TESTCASEPROJECT, case.testcaseProject)
                .set(TESTCASERESULT.DOMAIN_NAME,     case.domainName)
                .set(TESTCASERESULT.SUB_DOMAIN_NAME, case.subDomainName)
                .set(TESTCASERESULT.TESTJOB_ID,      testJob.id)
                .set(TESTCASERESULT.JOB_ID,          req.jobId)
                .set(TESTCASERESULT.DATA,            JSONObject(case.extra).toString())
        }
    ).execute()

    return "ok"
}
```

---

## 关键改进点对比

| | 重构前 | 重构后 |
|---|---|---|
| 入参类型 | `Map<String, Any>` | 强类型 DTO |
| 字段提取 | `it.remove("xxx")` 破坏性解构 | 直接属性访问 |
| realUri 计算 | 散落在业务代码里 | 封装在 DTO 的计算属性里 |
| batch 写法 | 一个模板 + 多次 bind,null 占位 | 每行一个完整 insert,`.set()` 链式调用,直观 |
| extra 数据 | map 剩余字段混在一起 | 显式声明 `extra: Map<String, Any?>` |

---

## 一个小建议

如果 `cases` 里的 `extra` 字段是真正动态的(调用方会随意传字段),可以用 Jackson 的 `@JsonAnySetter` 来收集:

```kotlin
data class TestCaseResultRequest(
    val name: String?,
    // ... 其他已知字段
) {
    @JsonIgnore
    val extra: MutableMap<String, Any?> = mutableMapOf()

    @JsonAnySetter
    fun setExtra(key: String, value: Any?) {
        extra[key] = value
    }
}
```

这样已知字段走强类型,未知字段自动归入 `extra`,两全其美。