...
先分析一下现有代码的问题,然后给出重构方案。
## 分析现有问题
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`,两全其美。