修复溯源模块大量问题
This commit is contained in:
@@ -75,5 +75,5 @@ fun Application.module() {
|
||||
VideoAnalyticsJetson()
|
||||
// 业务-图片分析
|
||||
ImageAnalytics()
|
||||
Traceability()
|
||||
Traceability(appConfig)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package ink.snowflake.server.controller
|
||||
|
||||
import ink.snowflake.server.model.request.CreateTraceBatchRequest
|
||||
import ink.snowflake.server.model.request.SaveTraceTemplateRequest
|
||||
import ink.snowflake.server.model.request.SaveTraceNodeLibraryRequest
|
||||
import ink.snowflake.server.model.request.SaveTracePreviewPageRequest
|
||||
import ink.snowflake.server.model.request.SubmitTraceabilityFeedbackRequest
|
||||
import ink.snowflake.server.model.request.TraceabilityFileAssetDeleteRequest
|
||||
import ink.snowflake.server.model.request.TraceabilityOssDeleteRequest
|
||||
import ink.snowflake.server.model.request.TraceabilityOssMoveRequest
|
||||
import ink.snowflake.server.model.request.TraceabilityOssPresignRequest
|
||||
@@ -13,6 +16,7 @@ import ink.snowflake.server.model.response.BaseResponse
|
||||
import ink.snowflake.server.model.response.TraceBatchStepResponse
|
||||
import ink.snowflake.server.model.response.TraceabilityOssFileResponse
|
||||
import ink.snowflake.server.model.response.TraceabilityPublicDetailResponse
|
||||
import ink.snowflake.server.utils.AppConfig
|
||||
import ink.snowflake.server.utils.OSSUtils
|
||||
import ink.snowflake.server.utils.dao.TraceabilityDao
|
||||
import io.ktor.http.ContentType
|
||||
@@ -35,7 +39,58 @@ import kotlinx.serialization.json.JsonObject
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
fun Application.Traceability() {
|
||||
private fun parseUuidOrNull(raw: String?): UUID? = runCatching {
|
||||
raw?.let(UUID::fromString)
|
||||
}.getOrNull()
|
||||
|
||||
private suspend fun handleSaveNodeLibrary(call: io.ktor.server.application.ApplicationCall, id: UUID?) {
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "节点库ID无效", data = null))
|
||||
return
|
||||
}
|
||||
val request = call.receive<SaveTraceNodeLibraryRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.saveNodeLibrary(id, request)))
|
||||
}
|
||||
|
||||
private suspend fun handleSaveTemplate(call: io.ktor.server.application.ApplicationCall, id: UUID?) {
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "模板ID无效", data = null))
|
||||
return
|
||||
}
|
||||
val request = call.receive<SaveTraceTemplateRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.saveTemplate(id, request)))
|
||||
}
|
||||
|
||||
private suspend fun handleUpdateBatchBase(call: io.ktor.server.application.ApplicationCall, id: UUID?) {
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "批次ID无效", data = null))
|
||||
return
|
||||
}
|
||||
val request = call.receive<UpdateTraceBatchBaseRequest>()
|
||||
val data = TraceabilityDao.updateBatchBase(id, request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "批次不存在", data = null))
|
||||
return
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
|
||||
private suspend fun handleUpdateBatchStep(call: io.ktor.server.application.ApplicationCall, id: UUID?, stepId: UUID?) {
|
||||
if (id == null || stepId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "步骤ID无效", data = null))
|
||||
return
|
||||
}
|
||||
val request = call.receive<UpdateTraceBatchStepRequest>()
|
||||
val data = TraceabilityDao.updateBatchStep(id, stepId, request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "步骤不存在", data = null))
|
||||
return
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
|
||||
fun Application.Traceability(config: AppConfig) {
|
||||
TraceabilityDao.init(config)
|
||||
TraceabilityDao.initSchema()
|
||||
|
||||
routing {
|
||||
@@ -43,6 +98,30 @@ fun Application.Traceability() {
|
||||
get("/overview") {
|
||||
call.respond(BaseResponse(data = TraceabilityDao.getOverview()))
|
||||
}
|
||||
route("/node-library") {
|
||||
get {
|
||||
call.respond(BaseResponse(data = TraceabilityDao.listNodeLibrary()))
|
||||
}
|
||||
post {
|
||||
val request = call.receive<SaveTraceNodeLibraryRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.saveNodeLibrary(null, request)))
|
||||
}
|
||||
put("/{id}") {
|
||||
handleSaveNodeLibrary(call, parseUuidOrNull(call.parameters["id"]))
|
||||
}
|
||||
post("/{id}") {
|
||||
handleSaveNodeLibrary(call, parseUuidOrNull(call.parameters["id"]))
|
||||
}
|
||||
delete("/{id}") {
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "节点库ID无效", data = null))
|
||||
return@delete
|
||||
}
|
||||
val deleted = TraceabilityDao.deleteNodeLibrary(id)
|
||||
call.respond(BaseResponse(status = deleted, message = if (deleted) "节点库节点已删除" else "节点库节点不存在", data = deleted))
|
||||
}
|
||||
}
|
||||
route("/templates") {
|
||||
get {
|
||||
call.respond(BaseResponse(data = TraceabilityDao.listTemplates()))
|
||||
@@ -52,7 +131,7 @@ fun Application.Traceability() {
|
||||
call.respond(BaseResponse(data = TraceabilityDao.saveTemplate(null, request)))
|
||||
}
|
||||
get("/{id}") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
val data = id?.let(TraceabilityDao::getTemplate)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "模板不存在", data = null))
|
||||
@@ -61,16 +140,13 @@ fun Application.Traceability() {
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
put("/{id}") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "模板ID无效", data = null))
|
||||
return@put
|
||||
}
|
||||
val request = call.receive<SaveTraceTemplateRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.saveTemplate(id, request)))
|
||||
handleSaveTemplate(call, parseUuidOrNull(call.parameters["id"]))
|
||||
}
|
||||
post("/{id}") {
|
||||
handleSaveTemplate(call, parseUuidOrNull(call.parameters["id"]))
|
||||
}
|
||||
delete("/{id}") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "模板ID无效", data = null))
|
||||
return@delete
|
||||
@@ -80,16 +156,89 @@ fun Application.Traceability() {
|
||||
}
|
||||
}
|
||||
|
||||
route("/previews") {
|
||||
get {
|
||||
call.respond(BaseResponse(data = TraceabilityDao.listPreviewPages()))
|
||||
}
|
||||
post {
|
||||
val request = call.receive<SaveTracePreviewPageRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.savePreviewPage(null, request)))
|
||||
}
|
||||
get("/{id}") {
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
val data = id?.let(TraceabilityDao::getPreviewPage)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "预演页不存在", data = null))
|
||||
return@get
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
put("/{id}") {
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "预演页ID无效", data = null))
|
||||
return@put
|
||||
}
|
||||
val request = call.receive<SaveTracePreviewPageRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.savePreviewPage(id, request)))
|
||||
}
|
||||
post("/{id}") {
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "预演页ID无效", data = null))
|
||||
return@post
|
||||
}
|
||||
val request = call.receive<SaveTracePreviewPageRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.savePreviewPage(id, request)))
|
||||
}
|
||||
delete("/{id}") {
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "预演页ID无效", data = null))
|
||||
return@delete
|
||||
}
|
||||
val deleted = TraceabilityDao.deletePreviewPage(id)
|
||||
call.respond(BaseResponse(status = deleted, message = if (deleted) "预演页已删除" else "预演页不存在", data = deleted))
|
||||
}
|
||||
post("/{id}/sync-template") {
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "预演页ID无效", data = null))
|
||||
return@post
|
||||
}
|
||||
val data = TraceabilityDao.syncPreviewToTemplate(id)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "预演页不存在", data = null))
|
||||
return@post
|
||||
}
|
||||
call.respond(BaseResponse(message = "已同步为新模板", data = data))
|
||||
}
|
||||
}
|
||||
|
||||
route("/batches") {
|
||||
get {
|
||||
call.respond(BaseResponse(data = TraceabilityDao.listBatches()))
|
||||
}
|
||||
post {
|
||||
val request = call.receive<CreateTraceBatchRequest>()
|
||||
call.respond(BaseResponse(data = TraceabilityDao.createBatch(request)))
|
||||
val templateId = parseUuidOrNull(request.templateId)
|
||||
if (templateId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "模板ID无效", data = null))
|
||||
return@post
|
||||
}
|
||||
if (TraceabilityDao.getTemplate(templateId) == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "模板不存在", data = null))
|
||||
return@post
|
||||
}
|
||||
val data = TraceabilityDao.createBatch(request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "模板不存在", data = null))
|
||||
return@post
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
delete("/{id}") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "批次ID无效", data = null))
|
||||
return@delete
|
||||
@@ -98,7 +247,7 @@ fun Application.Traceability() {
|
||||
call.respond(BaseResponse(status = deleted, message = if (deleted) "批次已删除" else "批次不存在", data = deleted))
|
||||
}
|
||||
get("/{id}") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
val data = id?.let(TraceabilityDao::getBatch)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "批次不存在", data = null))
|
||||
@@ -107,36 +256,27 @@ fun Application.Traceability() {
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
put("/{id}/base") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "批次ID无效", data = null))
|
||||
return@put
|
||||
}
|
||||
val request = call.receive<UpdateTraceBatchBaseRequest>()
|
||||
val data = TraceabilityDao.updateBatchBase(id, request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "批次不存在", data = null))
|
||||
return@put
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
handleUpdateBatchBase(call, parseUuidOrNull(call.parameters["id"]))
|
||||
}
|
||||
post("/{id}/base") {
|
||||
handleUpdateBatchBase(call, parseUuidOrNull(call.parameters["id"]))
|
||||
}
|
||||
put("/{id}/steps/{stepId}") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
val stepId = call.parameters["stepId"]?.let(UUID::fromString)
|
||||
if (id == null || stepId == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "步骤ID无效", data = null))
|
||||
return@put
|
||||
}
|
||||
val request = call.receive<UpdateTraceBatchStepRequest>()
|
||||
val data = TraceabilityDao.updateBatchStep(id, stepId, request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.NotFound, BaseResponse(status = false, message = "步骤不存在", data = null))
|
||||
return@put
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
handleUpdateBatchStep(
|
||||
call,
|
||||
parseUuidOrNull(call.parameters["id"]),
|
||||
parseUuidOrNull(call.parameters["stepId"]),
|
||||
)
|
||||
}
|
||||
post("/{id}/steps/{stepId}") {
|
||||
handleUpdateBatchStep(
|
||||
call,
|
||||
parseUuidOrNull(call.parameters["id"]),
|
||||
parseUuidOrNull(call.parameters["stepId"]),
|
||||
)
|
||||
}
|
||||
post("/{id}/publish") {
|
||||
val id = call.parameters["id"]?.let(UUID::fromString)
|
||||
val id = parseUuidOrNull(call.parameters["id"])
|
||||
if (id == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "批次ID无效", data = null))
|
||||
return@post
|
||||
@@ -156,7 +296,12 @@ fun Application.Traceability() {
|
||||
}
|
||||
post {
|
||||
val request = call.receive<SubmitTraceabilityFeedbackRequest>()
|
||||
call.respond(BaseResponse(message = "反馈已提交", data = TraceabilityDao.submitFeedback(request)))
|
||||
val data = TraceabilityDao.submitFeedback(request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "批次不存在或批次参数无效", data = null))
|
||||
return@post
|
||||
}
|
||||
call.respond(BaseResponse(message = "反馈已提交", data = data))
|
||||
}
|
||||
}
|
||||
route("/public") {
|
||||
@@ -170,9 +315,42 @@ fun Application.Traceability() {
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
get("/preview/by-code/{code}") {
|
||||
val code = call.parameters["code"] ?: ""
|
||||
val data = TraceabilityDao.getPublicDetailByCode(
|
||||
batchCode = code,
|
||||
increaseScan = false,
|
||||
onlyPublished = false,
|
||||
)
|
||||
if (data == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
BaseResponse(status = false, message = "未找到对应批次", data = null),
|
||||
)
|
||||
return@get
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
get("/preview-page/by-code/{code}") {
|
||||
val code = call.parameters["code"] ?: ""
|
||||
val data = TraceabilityDao.getPreviewPublicDetailByCode(code)
|
||||
if (data == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
BaseResponse(status = false, message = "未找到对应预演页", data = null),
|
||||
)
|
||||
return@get
|
||||
}
|
||||
call.respond(BaseResponse(data = data))
|
||||
}
|
||||
post("/feedback") {
|
||||
val request = call.receive<SubmitTraceabilityFeedbackRequest>()
|
||||
call.respond(BaseResponse(message = "感谢反馈,我们会尽快处理", data = TraceabilityDao.submitFeedback(request)))
|
||||
val data = TraceabilityDao.submitFeedback(request)
|
||||
if (data == null) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseResponse(status = false, message = "未找到对应批次,无法提交反馈", data = null))
|
||||
return@post
|
||||
}
|
||||
call.respond(BaseResponse(message = "感谢反馈,我们会尽快处理", data = data))
|
||||
}
|
||||
get("/page/{code}") {
|
||||
val code = call.parameters["code"] ?: ""
|
||||
@@ -191,6 +369,7 @@ fun Application.Traceability() {
|
||||
var bucketName = OSSUtils.defaultBucket()
|
||||
var objectDir = "traceability/images"
|
||||
var objectName = ""
|
||||
var assetType = ""
|
||||
var response: TraceabilityOssFileResponse? = null
|
||||
|
||||
multipart.forEachPart { part ->
|
||||
@@ -200,6 +379,7 @@ fun Application.Traceability() {
|
||||
"bucketName" -> bucketName = part.value.ifBlank { OSSUtils.defaultBucket() }
|
||||
"objectDir" -> objectDir = part.value.ifBlank { "traceability/images" }
|
||||
"objectName" -> objectName = part.value
|
||||
"assetType" -> assetType = part.value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +403,16 @@ fun Application.Traceability() {
|
||||
fileName = fileName,
|
||||
size = bytes.size.toLong(),
|
||||
)
|
||||
if (assetType.isNotBlank()) {
|
||||
TraceabilityDao.recordFileAsset(
|
||||
assetType = assetType,
|
||||
bucketName = bucketName,
|
||||
objectName = finalObjectName,
|
||||
fileName = fileName,
|
||||
contentType = contentType,
|
||||
size = bytes.size.toLong(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
@@ -240,6 +430,39 @@ fun Application.Traceability() {
|
||||
call.respond(BaseResponse(message = "图片上传成功", data = response))
|
||||
}
|
||||
|
||||
get("/history") {
|
||||
val assetType = call.request.queryParameters["assetType"]?.trim().orEmpty()
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull()?.coerceIn(1, 100) ?: 24
|
||||
if (assetType.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
BaseResponse(status = false, message = "assetType 不能为空", data = null),
|
||||
)
|
||||
return@get
|
||||
}
|
||||
call.respond(BaseResponse(data = TraceabilityDao.listFileAssets(assetType, limit)))
|
||||
}
|
||||
|
||||
post("/history/delete") {
|
||||
val request = call.receive<TraceabilityFileAssetDeleteRequest>()
|
||||
val assetId = parseUuidOrNull(request.id)
|
||||
if (assetId == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
BaseResponse(status = false, message = "历史文件ID无效", data = false),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
val (status, messageText) = TraceabilityDao.deleteFileAsset(assetId)
|
||||
call.respond(
|
||||
if (status) {
|
||||
BaseResponse(message = messageText, data = true)
|
||||
} else {
|
||||
BaseResponse(status = false, message = messageText, data = false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
post("/presigned-put") {
|
||||
val request = call.receive<TraceabilityOssPresignRequest>()
|
||||
val bucketName = request.bucketName?.ifBlank { OSSUtils.defaultBucket() } ?: OSSUtils.defaultBucket()
|
||||
@@ -324,7 +547,8 @@ private fun renderTraceabilityPage(detail: TraceabilityPublicDetailResponse): St
|
||||
val batch = detail.batch
|
||||
val publicCards = detail.publicSections.joinToString("") { renderSectionCard(it) }
|
||||
val timelineCards = detail.businessSections.joinToString("") { renderTimelineCard(it) }
|
||||
val cover = batch.coverImage.takeIf { it.isNotBlank() }
|
||||
val cover = batch.coverImagePreviewUrl.takeIf { it.isNotBlank() }
|
||||
?: batch.coverImage.takeIf { it.isNotBlank() }
|
||||
?: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1400&q=80"
|
||||
|
||||
return """
|
||||
@@ -405,18 +629,18 @@ private fun renderTraceabilityPage(detail: TraceabilityPublicDetailResponse): St
|
||||
<div class="timeline">$timelineCards</div>
|
||||
</section>
|
||||
<section class="section">
|
||||
<div class="section-head"><div><h2>投诉与建议</h2><p>如果你发现信息异常、质量问题,或有优化建议,可以直接提交。</p></div></div>
|
||||
<div class="section-head"><div><h2>反馈与投诉</h2><p>如果你发现信息异常、质量问题,或有优化建议,可以直接提交。</p></div></div>
|
||||
<div class="feedback-grid">
|
||||
<div class="feedback">
|
||||
<form id="feedback-form">
|
||||
<label>反馈类型</label>
|
||||
<select name="type"><option value="complaint">投诉</option><option value="suggestion">建议</option><option value="consult">咨询</option></select>
|
||||
<label>满意度</label>
|
||||
<select name="rating"><option value="5">5 分</option><option value="4">4 分</option><option value="3">3 分</option><option value="2">2 分</option><option value="1">1 分</option></select>
|
||||
<label>联系方式</label>
|
||||
<input name="contact" placeholder="手机号、邮箱或微信(选填)" />
|
||||
<label>反馈内容</label>
|
||||
<textarea name="content" placeholder="请描述你的问题或建议"></textarea>
|
||||
<label>满意度</label>
|
||||
<select name="rating"><option value="5">5 分</option><option value="4">4 分</option><option value="3">3 分</option><option value="2">2 分</option><option value="1">1 分</option></select>
|
||||
<button type="submit">提交反馈</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -450,15 +674,21 @@ private fun renderTraceabilityPage(detail: TraceabilityPublicDetailResponse): St
|
||||
}
|
||||
|
||||
private fun renderSectionCard(step: TraceBatchStepResponse): String =
|
||||
"""<article class="section-card"><h3>${escapeHtml(step.name)}</h3><p class="muted">${escapeHtml(step.description)}</p><div class="kv-grid">${renderValueCards(step.values)}</div></article>"""
|
||||
"""<article class="section-card"><h3>${escapeHtml(step.name)}</h3><p class="muted">${escapeHtml(step.description)}</p><div class="kv-grid">${renderValueCards(step)}</div></article>"""
|
||||
|
||||
private fun renderTimelineCard(step: TraceBatchStepResponse): String {
|
||||
val time = step.completedAt.ifBlank { "待补充" }
|
||||
return """<div class="timeline-item"><div class="timeline-rail"><span class="dot"></span><span class="line"></span></div><div class="timeline-card"><div class="timeline-meta"><div><h3 style="margin:0;">${escapeHtml(step.name)}</h3><p class="muted">${escapeHtml(step.description)}</p></div><span class="tag">${escapeHtml(step.status)} · ${escapeHtml(time)}</span></div><div class="kv-grid">${renderValueCards(step.values)}</div></div></div>"""
|
||||
return """<div class="timeline-item"><div class="timeline-rail"><span class="dot"></span><span class="line"></span></div><div class="timeline-card"><div class="timeline-meta"><div><h3 style="margin:0;">${escapeHtml(step.name)}</h3><p class="muted">${escapeHtml(step.description)}</p></div></div><div class="kv-grid">${renderValueCards(step)}</div></div></div>"""
|
||||
}
|
||||
|
||||
private fun renderValueCards(values: JsonObject): String = values.entries.joinToString("") { (key, value) ->
|
||||
"""<div class="kv"><span>${escapeHtml(key)}</span><strong>${escapeHtml(formatJsonValue(value))}</strong></div>"""
|
||||
private fun renderValueCards(step: TraceBatchStepResponse): String = step.values.entries.joinToString("") { (key, value) ->
|
||||
val field = step.fields.find { it.key == key }
|
||||
val label = field?.label ?: key
|
||||
val imageUrl = step.valuePreviewUrls[key].orEmpty()
|
||||
if ((field?.type ?: "string") == "image" && imageUrl.isNotBlank()) {
|
||||
"""<div class="kv"><span>${escapeHtml(label)}</span><img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(label)}" style="display:block;width:100%;max-height:220px;margin-top:8px;border:1px solid #dbe3f0;border-radius:14px;object-fit:cover;background:#fff;" /></div>"""
|
||||
} else {
|
||||
"""<div class="kv"><span>${escapeHtml(label)}</span><strong>${escapeHtml(formatJsonValue(value))}</strong></div>"""
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatJsonValue(value: JsonElement): String = when (value) {
|
||||
|
||||
@@ -28,6 +28,41 @@ object TraceabilityTemplateNodesTable : UUIDTable("traceability_template_nodes")
|
||||
val updatedAt = timestamp("updated_at").nullable()
|
||||
}
|
||||
|
||||
object TraceabilityPreviewPagesTable : UUIDTable("traceability_preview_pages") {
|
||||
val name = varchar("name", 120)
|
||||
val previewCode = varchar("preview_code", 120).uniqueIndex()
|
||||
val description = text("description").default("")
|
||||
val productName = varchar("product_name", 120).default("")
|
||||
val coverImage = text("cover_image").default("")
|
||||
val themeColor = varchar("theme_color", 20).default("#1f4fd6")
|
||||
val tagsJson = text("tags_json").default("[]")
|
||||
val createdAt = timestamp("created_at").nullable()
|
||||
val updatedAt = timestamp("updated_at").nullable()
|
||||
}
|
||||
|
||||
object TraceabilityPreviewNodesTable : UUIDTable("traceability_preview_nodes") {
|
||||
val previewPageId = reference("preview_page_id", TraceabilityPreviewPagesTable)
|
||||
val sort = integer("sort").default(0)
|
||||
val category = varchar("category", 32).default("business")
|
||||
val name = varchar("name", 120)
|
||||
val description = text("description").default("")
|
||||
val consumerVisible = bool("consumer_visible").default(true)
|
||||
val fieldsJson = text("fields_json").default("[]")
|
||||
val valuesJson = text("values_json").default("{}")
|
||||
val createdAt = timestamp("created_at").nullable()
|
||||
val updatedAt = timestamp("updated_at").nullable()
|
||||
}
|
||||
|
||||
object TraceabilityNodeLibraryTable : UUIDTable("traceability_node_library") {
|
||||
val category = varchar("category", 32).default("business")
|
||||
val name = varchar("name", 120)
|
||||
val description = text("description").default("")
|
||||
val consumerVisible = bool("consumer_visible").default(true)
|
||||
val fieldsJson = text("fields_json")
|
||||
val createdAt = timestamp("created_at").nullable()
|
||||
val updatedAt = timestamp("updated_at").nullable()
|
||||
}
|
||||
|
||||
object TraceabilityBatchesTable : UUIDTable("traceability_batches") {
|
||||
val templateId = reference("template_id", TraceabilityTemplatesTable)
|
||||
val batchName = varchar("batch_name", 150)
|
||||
@@ -55,6 +90,7 @@ object TraceabilityBatchStepsTable : UUIDTable("traceability_batch_steps") {
|
||||
val consumerVisible = bool("consumer_visible").default(true)
|
||||
val status = varchar("status", 32).default("pending")
|
||||
val operatorName = varchar("operator_name", 80).default("")
|
||||
val fieldsJson = text("fields_json").default("[]")
|
||||
val valuesJson = text("values_json").default("{}")
|
||||
val completedAt = timestamp("completed_at").nullable()
|
||||
val createdAt = timestamp("created_at").nullable()
|
||||
@@ -70,3 +106,13 @@ object TraceabilityFeedbackTable : UUIDTable("traceability_feedback") {
|
||||
val rating = integer("rating").default(5)
|
||||
val createdAt = timestamp("created_at").nullable()
|
||||
}
|
||||
|
||||
object TraceabilityFileAssetsTable : UUIDTable("traceability_file_assets") {
|
||||
val assetType = varchar("asset_type", 32).default("general")
|
||||
val bucketName = varchar("bucket_name", 120)
|
||||
val objectName = text("object_name")
|
||||
val fileName = varchar("file_name", 255).default("")
|
||||
val contentType = varchar("content_type", 120).default("")
|
||||
val size = long("size").default(0)
|
||||
val createdAt = timestamp("created_at").nullable()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class TraceFieldStyleRequest(
|
||||
val bold: Boolean = false,
|
||||
val color: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceFieldDefinitionRequest(
|
||||
val key: String,
|
||||
@@ -12,9 +18,11 @@ data class TraceFieldDefinitionRequest(
|
||||
val type: String = "string",
|
||||
val required: Boolean = false,
|
||||
val visible: Boolean = true,
|
||||
val fixedPreset: Boolean = false,
|
||||
val placeholder: String = "",
|
||||
val defaultValue: JsonElement? = null,
|
||||
val options: List<String> = emptyList(),
|
||||
val fieldStyle: TraceFieldStyleRequest = TraceFieldStyleRequest(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -28,6 +36,17 @@ data class TraceTemplateNodeRequest(
|
||||
val fields: List<TraceFieldDefinitionRequest> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TracePreviewNodeRequest(
|
||||
val id: String? = null,
|
||||
val category: String = "business",
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val consumerVisible: Boolean = true,
|
||||
val fields: List<TraceFieldDefinitionRequest> = emptyList(),
|
||||
val values: JsonObject = JsonObject(emptyMap()),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SaveTraceTemplateRequest(
|
||||
val name: String,
|
||||
@@ -40,6 +59,26 @@ data class SaveTraceTemplateRequest(
|
||||
val nodes: List<TraceTemplateNodeRequest> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SaveTracePreviewPageRequest(
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val productName: String = "",
|
||||
val coverImage: String = "",
|
||||
val themeColor: String = "#1f4fd6",
|
||||
val tags: List<String> = emptyList(),
|
||||
val nodes: List<TracePreviewNodeRequest> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SaveTraceNodeLibraryRequest(
|
||||
val category: String = "business",
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val consumerVisible: Boolean = true,
|
||||
val fields: List<TraceFieldDefinitionRequest> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateTraceBatchRequest(
|
||||
val templateId: String,
|
||||
@@ -109,4 +148,9 @@ data class TraceabilityOssDeleteRequest(
|
||||
val objectName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceabilityFileAssetDeleteRequest(
|
||||
val id: String,
|
||||
)
|
||||
|
||||
fun CreateTraceBatchRequest.templateUuid(): UUID = UUID.fromString(templateId)
|
||||
|
||||
@@ -13,6 +13,12 @@ data class TraceabilityOverviewResponse(
|
||||
val totalScans: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceFieldStyleResponse(
|
||||
val bold: Boolean = false,
|
||||
val color: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceFieldDefinitionResponse(
|
||||
val key: String,
|
||||
@@ -20,9 +26,12 @@ data class TraceFieldDefinitionResponse(
|
||||
val type: String = "string",
|
||||
val required: Boolean = false,
|
||||
val visible: Boolean = true,
|
||||
val fixedPreset: Boolean = false,
|
||||
val placeholder: String = "",
|
||||
val defaultValue: JsonElement? = null,
|
||||
val defaultPreviewUrl: String? = null,
|
||||
val options: List<String> = emptyList(),
|
||||
val fieldStyle: TraceFieldStyleResponse = TraceFieldStyleResponse(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -37,6 +46,17 @@ data class TraceTemplateNodeResponse(
|
||||
val fields: List<TraceFieldDefinitionResponse>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceNodeLibraryResponse(
|
||||
val id: String,
|
||||
val category: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val consumerVisible: Boolean,
|
||||
val fields: List<TraceFieldDefinitionResponse>,
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceTemplateSummaryResponse(
|
||||
val id: String,
|
||||
@@ -45,6 +65,7 @@ data class TraceTemplateSummaryResponse(
|
||||
val productName: String,
|
||||
val industryName: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val themeColor: String,
|
||||
val status: String,
|
||||
val nodeCount: Int,
|
||||
@@ -60,12 +81,57 @@ data class TraceTemplateDetailResponse(
|
||||
val productName: String,
|
||||
val industryName: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val themeColor: String,
|
||||
val status: String,
|
||||
val nodes: List<TraceTemplateNodeResponse>,
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TracePreviewNodeResponse(
|
||||
val id: String,
|
||||
val sort: Int,
|
||||
val category: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val consumerVisible: Boolean,
|
||||
val values: JsonObject,
|
||||
val valuePreviewUrls: Map<String, String> = emptyMap(),
|
||||
val fields: List<TraceFieldDefinitionResponse>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TracePreviewPageSummaryResponse(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val previewCode: String,
|
||||
val description: String,
|
||||
val productName: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val themeColor: String,
|
||||
val tags: List<String>,
|
||||
val publicUrl: String,
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TracePreviewPageDetailResponse(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val previewCode: String,
|
||||
val description: String,
|
||||
val productName: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val themeColor: String,
|
||||
val tags: List<String>,
|
||||
val publicUrl: String,
|
||||
val nodes: List<TracePreviewNodeResponse>,
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceBatchStepResponse(
|
||||
val id: String,
|
||||
@@ -79,6 +145,7 @@ data class TraceBatchStepResponse(
|
||||
val status: String,
|
||||
val operatorName: String,
|
||||
val values: JsonObject,
|
||||
val valuePreviewUrls: Map<String, String> = emptyMap(),
|
||||
val completedAt: String = "",
|
||||
val fields: List<TraceFieldDefinitionResponse> = emptyList(),
|
||||
)
|
||||
@@ -93,6 +160,7 @@ data class TraceBatchSummaryResponse(
|
||||
val productName: String,
|
||||
val summary: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val tags: List<String>,
|
||||
val status: String,
|
||||
val currentStep: Int,
|
||||
@@ -111,6 +179,7 @@ data class TraceBatchDetailResponse(
|
||||
val productName: String,
|
||||
val summary: String,
|
||||
val coverImage: String,
|
||||
val coverImagePreviewUrl: String = "",
|
||||
val tags: List<String>,
|
||||
val status: String,
|
||||
val currentStep: Int,
|
||||
@@ -153,3 +222,16 @@ data class TraceabilityOssFileResponse(
|
||||
val fileName: String? = null,
|
||||
val size: Long? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraceabilityFileAssetResponse(
|
||||
val id: String,
|
||||
val assetType: String,
|
||||
val bucketName: String,
|
||||
val objectName: String,
|
||||
val fileName: String,
|
||||
val contentType: String,
|
||||
val size: Long,
|
||||
val previewUrl: String,
|
||||
val createdAt: String,
|
||||
)
|
||||
|
||||
@@ -26,4 +26,11 @@ class AppConfig(config: ApplicationConfig) {
|
||||
val ossDefaultBucket: String = config.property("ktor.oss.default-bucket").getString()
|
||||
val ossFallbackBucket: String = config.property("ktor.oss.fallback-bucket").getString()
|
||||
val ossFallbackObject: String = config.property("ktor.oss.fallback-object").getString()
|
||||
val traceabilityPublicPreviewBaseUrl: String =
|
||||
config.propertyOrNull("ktor.traceability.public-preview-base-url")
|
||||
?.getString()
|
||||
?.trim()
|
||||
?.trimEnd('/')
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: "http://127.0.0.1:8081"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,19 @@ package ink.snowflake.server.utils.dao
|
||||
import ink.snowflake.server.model.database.TraceabilityBatchStepsTable
|
||||
import ink.snowflake.server.model.database.TraceabilityBatchesTable
|
||||
import ink.snowflake.server.model.database.TraceabilityFeedbackTable
|
||||
import ink.snowflake.server.model.database.TraceabilityFileAssetsTable
|
||||
import ink.snowflake.server.model.database.TraceabilityNodeLibraryTable
|
||||
import ink.snowflake.server.model.database.TraceabilityPreviewNodesTable
|
||||
import ink.snowflake.server.model.database.TraceabilityPreviewPagesTable
|
||||
import ink.snowflake.server.model.database.TraceabilityTemplateNodesTable
|
||||
import ink.snowflake.server.model.database.TraceabilityTemplatesTable
|
||||
import ink.snowflake.server.model.request.CreateTraceBatchRequest
|
||||
import ink.snowflake.server.model.request.SaveTraceTemplateRequest
|
||||
import ink.snowflake.server.model.request.SaveTraceNodeLibraryRequest
|
||||
import ink.snowflake.server.model.request.SaveTracePreviewPageRequest
|
||||
import ink.snowflake.server.model.request.SubmitTraceabilityFeedbackRequest
|
||||
import ink.snowflake.server.model.request.TraceFieldDefinitionRequest
|
||||
import ink.snowflake.server.model.request.TracePreviewNodeRequest
|
||||
import ink.snowflake.server.model.request.UpdateTraceBatchBaseRequest
|
||||
import ink.snowflake.server.model.request.UpdateTraceBatchStepRequest
|
||||
import ink.snowflake.server.model.request.templateUuid
|
||||
@@ -16,19 +23,30 @@ import ink.snowflake.server.model.response.TraceBatchDetailResponse
|
||||
import ink.snowflake.server.model.response.TraceBatchStepResponse
|
||||
import ink.snowflake.server.model.response.TraceBatchSummaryResponse
|
||||
import ink.snowflake.server.model.response.TraceFieldDefinitionResponse
|
||||
import ink.snowflake.server.model.response.TraceFieldStyleResponse
|
||||
import ink.snowflake.server.model.response.TraceNodeLibraryResponse
|
||||
import ink.snowflake.server.model.response.TracePreviewNodeResponse
|
||||
import ink.snowflake.server.model.response.TracePreviewPageDetailResponse
|
||||
import ink.snowflake.server.model.response.TracePreviewPageSummaryResponse
|
||||
import ink.snowflake.server.model.response.TraceTemplateDetailResponse
|
||||
import ink.snowflake.server.model.response.TraceTemplateNodeResponse
|
||||
import ink.snowflake.server.model.response.TraceTemplateSummaryResponse
|
||||
import ink.snowflake.server.model.response.TraceabilityFeedbackResponse
|
||||
import ink.snowflake.server.model.response.TraceabilityFileAssetResponse
|
||||
import ink.snowflake.server.model.response.TraceabilityOverviewResponse
|
||||
import ink.snowflake.server.model.response.TraceabilityPublicDetailResponse
|
||||
import ink.snowflake.server.utils.AppConfig
|
||||
import ink.snowflake.server.utils.OSSUtils
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
@@ -47,21 +65,24 @@ object TraceabilityDao {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
private val publicPreviewBaseUrl =
|
||||
System.getenv("TRACEABILITY_PUBLIC_PREVIEW_BASE_URL")
|
||||
?.trim()
|
||||
?.trimEnd('/')
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: "http://127.0.0.1:8081"
|
||||
private var publicPreviewBaseUrl: String = "http://127.0.0.1:8081"
|
||||
|
||||
fun init(config: AppConfig) {
|
||||
publicPreviewBaseUrl = config.traceabilityPublicPreviewBaseUrl
|
||||
}
|
||||
|
||||
fun initSchema() {
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
TraceabilityTemplatesTable,
|
||||
TraceabilityTemplateNodesTable,
|
||||
TraceabilityPreviewPagesTable,
|
||||
TraceabilityPreviewNodesTable,
|
||||
TraceabilityNodeLibraryTable,
|
||||
TraceabilityBatchesTable,
|
||||
TraceabilityBatchStepsTable,
|
||||
TraceabilityFeedbackTable,
|
||||
TraceabilityFileAssetsTable,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -79,8 +100,374 @@ object TraceabilityDao {
|
||||
)
|
||||
}
|
||||
|
||||
fun listPreviewPages(): List<TracePreviewPageSummaryResponse> = transaction {
|
||||
TraceabilityPreviewPagesTable.selectAll()
|
||||
.orderBy(TraceabilityPreviewPagesTable.updatedAt, SortOrder.DESC)
|
||||
.map {
|
||||
val code = it[TraceabilityPreviewPagesTable.previewCode]
|
||||
TracePreviewPageSummaryResponse(
|
||||
id = it[TraceabilityPreviewPagesTable.id].value.toString(),
|
||||
name = it[TraceabilityPreviewPagesTable.name],
|
||||
previewCode = code,
|
||||
description = it[TraceabilityPreviewPagesTable.description],
|
||||
productName = it[TraceabilityPreviewPagesTable.productName],
|
||||
coverImage = it[TraceabilityPreviewPagesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(it[TraceabilityPreviewPagesTable.coverImage]).orEmpty(),
|
||||
themeColor = it[TraceabilityPreviewPagesTable.themeColor],
|
||||
tags = decodeStringList(it[TraceabilityPreviewPagesTable.tagsJson]),
|
||||
publicUrl = previewPublicUrl(code),
|
||||
updatedAt = formatTimestamp(it[TraceabilityPreviewPagesTable.updatedAt]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreviewPage(previewId: UUID): TracePreviewPageDetailResponse? = transaction {
|
||||
val row = TraceabilityPreviewPagesTable.selectAll()
|
||||
.where { TraceabilityPreviewPagesTable.id eq previewId }
|
||||
.singleOrNull() ?: return@transaction null
|
||||
val code = row[TraceabilityPreviewPagesTable.previewCode]
|
||||
TracePreviewPageDetailResponse(
|
||||
id = row[TraceabilityPreviewPagesTable.id].value.toString(),
|
||||
name = row[TraceabilityPreviewPagesTable.name],
|
||||
previewCode = code,
|
||||
description = row[TraceabilityPreviewPagesTable.description],
|
||||
productName = row[TraceabilityPreviewPagesTable.productName],
|
||||
coverImage = row[TraceabilityPreviewPagesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(row[TraceabilityPreviewPagesTable.coverImage]).orEmpty(),
|
||||
themeColor = row[TraceabilityPreviewPagesTable.themeColor],
|
||||
tags = decodeStringList(row[TraceabilityPreviewPagesTable.tagsJson]),
|
||||
publicUrl = previewPublicUrl(code),
|
||||
nodes = loadPreviewNodes(previewId),
|
||||
updatedAt = formatTimestamp(row[TraceabilityPreviewPagesTable.updatedAt]),
|
||||
)
|
||||
}
|
||||
|
||||
fun savePreviewPage(previewId: UUID?, request: SaveTracePreviewPageRequest): TracePreviewPageDetailResponse = transaction {
|
||||
val now = timestampLiteral(nowInstant())
|
||||
val currentId = previewId ?: TraceabilityPreviewPagesTable.insertAndGetId {
|
||||
it[name] = request.name
|
||||
it[previewCode] = buildPreviewCode()
|
||||
it[description] = request.description
|
||||
it[productName] = request.productName
|
||||
it[coverImage] = request.coverImage
|
||||
it[themeColor] = request.themeColor
|
||||
it[tagsJson] = json.encodeToString(request.tags)
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}.value
|
||||
|
||||
if (previewId != null) {
|
||||
TraceabilityPreviewPagesTable.update({ TraceabilityPreviewPagesTable.id eq currentId }) {
|
||||
it[name] = request.name
|
||||
it[description] = request.description
|
||||
it[productName] = request.productName
|
||||
it[coverImage] = request.coverImage
|
||||
it[themeColor] = request.themeColor
|
||||
it[tagsJson] = json.encodeToString(request.tags)
|
||||
it[updatedAt] = now
|
||||
}
|
||||
TraceabilityPreviewNodesTable.deleteWhere { TraceabilityPreviewNodesTable.previewPageId eq currentId }
|
||||
}
|
||||
|
||||
request.nodes.forEachIndexed { index, node ->
|
||||
TraceabilityPreviewNodesTable.insertAndGetId {
|
||||
it[previewPageId] = currentId
|
||||
it[sort] = index
|
||||
it[category] = node.category
|
||||
it[name] = node.name
|
||||
it[description] = node.description
|
||||
it[consumerVisible] = node.consumerVisible
|
||||
it[fieldsJson] = json.encodeToString(node.fields)
|
||||
it[valuesJson] = json.encodeToString(node.values)
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
getPreviewPage(currentId)!!
|
||||
}
|
||||
|
||||
fun deletePreviewPage(previewId: UUID): Boolean = transaction {
|
||||
TraceabilityPreviewNodesTable.deleteWhere { TraceabilityPreviewNodesTable.previewPageId eq previewId }
|
||||
TraceabilityPreviewPagesTable.deleteWhere { TraceabilityPreviewPagesTable.id eq previewId } > 0
|
||||
}
|
||||
|
||||
fun syncPreviewToTemplate(previewId: UUID): TraceTemplateDetailResponse? = transaction {
|
||||
val detail = getPreviewPage(previewId) ?: return@transaction null
|
||||
val request = SaveTraceTemplateRequest(
|
||||
name = detail.name,
|
||||
description = detail.description,
|
||||
productName = detail.productName,
|
||||
coverImage = detail.coverImage,
|
||||
themeColor = detail.themeColor,
|
||||
status = "draft",
|
||||
nodes = detail.nodes.map { node ->
|
||||
ink.snowflake.server.model.request.TraceTemplateNodeRequest(
|
||||
category = node.category,
|
||||
name = node.name,
|
||||
description = node.description,
|
||||
locked = false,
|
||||
consumerVisible = node.consumerVisible,
|
||||
fields = node.fields.map { field ->
|
||||
val value = node.values[field.key] ?: field.defaultValue
|
||||
TraceFieldDefinitionRequest(
|
||||
key = field.key,
|
||||
label = field.label,
|
||||
type = field.type,
|
||||
required = field.required,
|
||||
visible = field.visible,
|
||||
fixedPreset = field.fixedPreset,
|
||||
placeholder = field.placeholder,
|
||||
defaultValue = value,
|
||||
options = field.options,
|
||||
fieldStyle = ink.snowflake.server.model.request.TraceFieldStyleRequest(
|
||||
bold = field.fieldStyle.bold,
|
||||
color = field.fieldStyle.color,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
saveTemplate(null, request)
|
||||
}
|
||||
|
||||
fun recordFileAsset(
|
||||
assetType: String,
|
||||
bucketName: String,
|
||||
objectName: String,
|
||||
fileName: String,
|
||||
contentType: String,
|
||||
size: Long,
|
||||
) = transaction {
|
||||
val now = timestampLiteral(nowInstant())
|
||||
TraceabilityFileAssetsTable.insertAndGetId {
|
||||
it[this.assetType] = assetType.ifBlank { "general" }
|
||||
it[this.bucketName] = bucketName
|
||||
it[this.objectName] = objectName
|
||||
it[this.fileName] = fileName
|
||||
it[this.contentType] = contentType
|
||||
it[this.size] = size
|
||||
it[createdAt] = now
|
||||
}
|
||||
}
|
||||
|
||||
fun listFileAssets(assetType: String, limit: Int = 24): List<TraceabilityFileAssetResponse> = transaction {
|
||||
val items = LinkedHashMap<String, TraceabilityFileAssetResponse>()
|
||||
|
||||
TraceabilityFileAssetsTable.selectAll()
|
||||
.where { TraceabilityFileAssetsTable.assetType eq assetType }
|
||||
.orderBy(TraceabilityFileAssetsTable.createdAt, SortOrder.DESC)
|
||||
.limit(limit)
|
||||
.forEach {
|
||||
val bucketName = it[TraceabilityFileAssetsTable.bucketName]
|
||||
val objectName = it[TraceabilityFileAssetsTable.objectName]
|
||||
items["$bucketName::$objectName"] = TraceabilityFileAssetResponse(
|
||||
id = it[TraceabilityFileAssetsTable.id].value.toString(),
|
||||
assetType = it[TraceabilityFileAssetsTable.assetType],
|
||||
bucketName = bucketName,
|
||||
objectName = objectName,
|
||||
fileName = it[TraceabilityFileAssetsTable.fileName],
|
||||
contentType = it[TraceabilityFileAssetsTable.contentType],
|
||||
size = it[TraceabilityFileAssetsTable.size],
|
||||
previewUrl = OSSUtils.getTempUrl(bucketName, objectName),
|
||||
createdAt = formatTimestamp(it[TraceabilityFileAssetsTable.createdAt]),
|
||||
)
|
||||
}
|
||||
|
||||
if (assetType == "cover" && items.size < limit) {
|
||||
val legacySources = buildList {
|
||||
addAll(
|
||||
TraceabilityTemplatesTable.selectAll()
|
||||
.where { TraceabilityTemplatesTable.coverImage neq "" }
|
||||
.map { it[TraceabilityTemplatesTable.coverImage] to formatTimestamp(it[TraceabilityTemplatesTable.updatedAt]) },
|
||||
)
|
||||
addAll(
|
||||
TraceabilityBatchesTable.selectAll()
|
||||
.where { TraceabilityBatchesTable.coverImage neq "" }
|
||||
.map { it[TraceabilityBatchesTable.coverImage] to formatTimestamp(it[TraceabilityBatchesTable.updatedAt]) },
|
||||
)
|
||||
}
|
||||
|
||||
legacySources.forEachIndexed { index, (raw, createdAt) ->
|
||||
if (items.size >= limit) return@forEachIndexed
|
||||
val previewUrl = resolveStoredImagePreviewUrl(raw) ?: return@forEachIndexed
|
||||
val (bucketName, objectName) = extractStoredImageReference(raw)
|
||||
val identity = if (objectName.isNotBlank()) "$bucketName::$objectName" else "legacy::$previewUrl"
|
||||
if (items.containsKey(identity)) return@forEachIndexed
|
||||
items[identity] = TraceabilityFileAssetResponse(
|
||||
id = "legacy-cover-$index",
|
||||
assetType = assetType,
|
||||
bucketName = bucketName,
|
||||
objectName = objectName.ifBlank { previewUrl },
|
||||
fileName = objectName.substringAfterLast('/').ifBlank { "历史封面图" },
|
||||
contentType = "image/*",
|
||||
size = 0,
|
||||
previewUrl = previewUrl,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items.values.take(limit)
|
||||
}
|
||||
|
||||
fun deleteFileAsset(assetId: UUID): Pair<Boolean, String> = transaction {
|
||||
val asset = TraceabilityFileAssetsTable.selectAll()
|
||||
.where { TraceabilityFileAssetsTable.id eq assetId }
|
||||
.singleOrNull()
|
||||
?: return@transaction false to "历史文件不存在"
|
||||
|
||||
val bucketName = asset[TraceabilityFileAssetsTable.bucketName]
|
||||
val objectName = asset[TraceabilityFileAssetsTable.objectName]
|
||||
|
||||
val usedByTemplate = TraceabilityTemplatesTable.selectAll().any {
|
||||
val (bucket, obj) = extractStoredImageReference(it[TraceabilityTemplatesTable.coverImage])
|
||||
bucket == bucketName && obj == objectName
|
||||
}
|
||||
if (usedByTemplate) {
|
||||
return@transaction false to "该封面图仍被模板使用,无法删除"
|
||||
}
|
||||
|
||||
val usedByBatch = TraceabilityBatchesTable.selectAll().any {
|
||||
val (bucket, obj) = extractStoredImageReference(it[TraceabilityBatchesTable.coverImage])
|
||||
bucket == bucketName && obj == objectName
|
||||
}
|
||||
if (usedByBatch) {
|
||||
return@transaction false to "该封面图仍被批次使用,无法删除"
|
||||
}
|
||||
|
||||
runCatching {
|
||||
OSSUtils.deleteFile(bucketName, objectName)
|
||||
}
|
||||
TraceabilityFileAssetsTable.deleteWhere { TraceabilityFileAssetsTable.id eq assetId }
|
||||
true to "历史封面图已删除"
|
||||
}
|
||||
|
||||
private fun nowInstant() = Clock.System.now()
|
||||
|
||||
fun listNodeLibrary(): List<TraceNodeLibraryResponse> = transaction {
|
||||
ensureDefaultNodeLibrarySeeded()
|
||||
TraceabilityNodeLibraryTable.selectAll()
|
||||
.orderBy(TraceabilityNodeLibraryTable.updatedAt, SortOrder.DESC)
|
||||
.map {
|
||||
TraceNodeLibraryResponse(
|
||||
id = it[TraceabilityNodeLibraryTable.id].value.toString(),
|
||||
category = it[TraceabilityNodeLibraryTable.category],
|
||||
name = it[TraceabilityNodeLibraryTable.name],
|
||||
description = it[TraceabilityNodeLibraryTable.description],
|
||||
consumerVisible = it[TraceabilityNodeLibraryTable.consumerVisible],
|
||||
fields = decodeFields(it[TraceabilityNodeLibraryTable.fieldsJson]),
|
||||
updatedAt = formatTimestamp(it[TraceabilityNodeLibraryTable.updatedAt]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureDefaultNodeLibrarySeeded() {
|
||||
if (TraceabilityNodeLibraryTable.selectAll().limit(1).any()) {
|
||||
return
|
||||
}
|
||||
val now = timestampLiteral(nowInstant())
|
||||
defaultNodeLibraryPresets().forEach { request ->
|
||||
TraceabilityNodeLibraryTable.insertAndGetId {
|
||||
it[category] = request.category
|
||||
it[name] = request.name
|
||||
it[description] = request.description
|
||||
it[consumerVisible] = request.consumerVisible
|
||||
it[fieldsJson] = json.encodeToString(request.fields)
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun defaultNodeLibraryPresets(): List<SaveTraceNodeLibraryRequest> {
|
||||
return listOf(
|
||||
SaveTraceNodeLibraryRequest(
|
||||
category = "business",
|
||||
name = "生产加工节点",
|
||||
description = "记录原料、工艺、加工批次等业务过程信息。",
|
||||
consumerVisible = true,
|
||||
fields = listOf(
|
||||
TraceFieldDefinitionRequest(key = "process_name", label = "工艺名称"),
|
||||
TraceFieldDefinitionRequest(key = "operator", label = "负责人"),
|
||||
TraceFieldDefinitionRequest(key = "production_date", label = "生产日期", type = "date"),
|
||||
TraceFieldDefinitionRequest(key = "remark", label = "备注"),
|
||||
),
|
||||
),
|
||||
SaveTraceNodeLibraryRequest(
|
||||
category = "business",
|
||||
name = "质检检验节点",
|
||||
description = "记录质检结果、检验员和检验时间。",
|
||||
consumerVisible = true,
|
||||
fields = listOf(
|
||||
TraceFieldDefinitionRequest(key = "inspector", label = "检验员"),
|
||||
TraceFieldDefinitionRequest(key = "inspection_date", label = "检验日期", type = "date"),
|
||||
TraceFieldDefinitionRequest(
|
||||
key = "inspection_result",
|
||||
label = "检验结果",
|
||||
type = "select",
|
||||
options = listOf("合格", "不合格", "复检中"),
|
||||
),
|
||||
TraceFieldDefinitionRequest(key = "inspection_note", label = "检验说明"),
|
||||
),
|
||||
),
|
||||
SaveTraceNodeLibraryRequest(
|
||||
category = "public",
|
||||
name = "企业信息节点",
|
||||
description = "面向消费者展示企业名称、产地和联系方式等信息。",
|
||||
consumerVisible = true,
|
||||
fields = listOf(
|
||||
TraceFieldDefinitionRequest(key = "company_name", label = "企业名称"),
|
||||
TraceFieldDefinitionRequest(key = "origin", label = "产地"),
|
||||
TraceFieldDefinitionRequest(key = "contact_phone", label = "联系电话"),
|
||||
TraceFieldDefinitionRequest(key = "company_intro", label = "企业简介"),
|
||||
),
|
||||
),
|
||||
SaveTraceNodeLibraryRequest(
|
||||
category = "public",
|
||||
name = "资质证书节点",
|
||||
description = "展示认证证书、证书编号和有效期。",
|
||||
consumerVisible = true,
|
||||
fields = listOf(
|
||||
TraceFieldDefinitionRequest(key = "certificate_name", label = "证书名称"),
|
||||
TraceFieldDefinitionRequest(key = "certificate_no", label = "证书编号"),
|
||||
TraceFieldDefinitionRequest(key = "valid_until", label = "有效期", type = "date"),
|
||||
TraceFieldDefinitionRequest(key = "certificate_image", label = "证书图片", type = "image"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun saveNodeLibrary(nodeId: UUID?, request: SaveTraceNodeLibraryRequest): TraceNodeLibraryResponse = transaction {
|
||||
val now = timestampLiteral(nowInstant())
|
||||
val currentId = nodeId ?: TraceabilityNodeLibraryTable.insertAndGetId {
|
||||
it[category] = request.category
|
||||
it[name] = request.name
|
||||
it[description] = request.description
|
||||
it[consumerVisible] = request.consumerVisible
|
||||
it[fieldsJson] = json.encodeToString(request.fields)
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}.value
|
||||
|
||||
if (nodeId != null) {
|
||||
TraceabilityNodeLibraryTable.update({ TraceabilityNodeLibraryTable.id eq currentId }) {
|
||||
it[category] = request.category
|
||||
it[name] = request.name
|
||||
it[description] = request.description
|
||||
it[consumerVisible] = request.consumerVisible
|
||||
it[fieldsJson] = json.encodeToString(request.fields)
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
|
||||
listNodeLibrary().first { it.id == currentId.toString() }
|
||||
}
|
||||
|
||||
fun deleteNodeLibrary(nodeId: UUID): Boolean = transaction {
|
||||
TraceabilityNodeLibraryTable.deleteWhere { TraceabilityNodeLibraryTable.id eq nodeId } > 0
|
||||
}
|
||||
|
||||
fun listTemplates(): List<TraceTemplateSummaryResponse> = transaction {
|
||||
val batchCountByTemplate = TraceabilityBatchesTable.selectAll()
|
||||
.groupBy { it[TraceabilityBatchesTable.templateId].value }
|
||||
@@ -99,6 +486,7 @@ object TraceabilityDao {
|
||||
productName = it[TraceabilityTemplatesTable.productName],
|
||||
industryName = it[TraceabilityTemplatesTable.industryName],
|
||||
coverImage = it[TraceabilityTemplatesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(it[TraceabilityTemplatesTable.coverImage]).orEmpty(),
|
||||
themeColor = it[TraceabilityTemplatesTable.themeColor],
|
||||
status = it[TraceabilityTemplatesTable.status],
|
||||
nodeCount = nodeCountByTemplate[it[TraceabilityTemplatesTable.id].value] ?: 0,
|
||||
@@ -120,6 +508,7 @@ object TraceabilityDao {
|
||||
productName = templateRow[TraceabilityTemplatesTable.productName],
|
||||
industryName = templateRow[TraceabilityTemplatesTable.industryName],
|
||||
coverImage = templateRow[TraceabilityTemplatesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(templateRow[TraceabilityTemplatesTable.coverImage]).orEmpty(),
|
||||
themeColor = templateRow[TraceabilityTemplatesTable.themeColor],
|
||||
status = templateRow[TraceabilityTemplatesTable.status],
|
||||
nodes = loadTemplateNodes(templateId),
|
||||
@@ -154,11 +543,17 @@ object TraceabilityDao {
|
||||
}
|
||||
val existingNodeIds = TraceabilityTemplateNodesTable.selectAll()
|
||||
.where { TraceabilityTemplateNodesTable.templateId eq currentId }
|
||||
.map { it[TraceabilityTemplateNodesTable.id].value }
|
||||
.associate {
|
||||
it[TraceabilityTemplateNodesTable.id].value to it[TraceabilityTemplateNodesTable.fieldsJson]
|
||||
}
|
||||
if (existingNodeIds.isNotEmpty()) {
|
||||
existingNodeIds.forEach { nodeId ->
|
||||
TraceabilityBatchStepsTable.deleteWhere {
|
||||
existingNodeIds.forEach { (nodeId, fieldsJson) ->
|
||||
TraceabilityBatchStepsTable.update({
|
||||
TraceabilityBatchStepsTable.templateNodeId eq nodeId
|
||||
}) {
|
||||
it[TraceabilityBatchStepsTable.fieldsJson] = fieldsJson
|
||||
it[templateNodeId] = null
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,6 +616,7 @@ object TraceabilityDao {
|
||||
productName = it[TraceabilityBatchesTable.productName],
|
||||
summary = it[TraceabilityBatchesTable.summary],
|
||||
coverImage = it[TraceabilityBatchesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(it[TraceabilityBatchesTable.coverImage]).orEmpty(),
|
||||
tags = decodeStringList(it[TraceabilityBatchesTable.tagsJson]),
|
||||
status = it[TraceabilityBatchesTable.status],
|
||||
currentStep = it[TraceabilityBatchesTable.currentStep],
|
||||
@@ -249,6 +645,7 @@ object TraceabilityDao {
|
||||
productName = batchRow[TraceabilityBatchesTable.productName],
|
||||
summary = batchRow[TraceabilityBatchesTable.summary],
|
||||
coverImage = batchRow[TraceabilityBatchesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(batchRow[TraceabilityBatchesTable.coverImage]).orEmpty(),
|
||||
tags = decodeStringList(batchRow[TraceabilityBatchesTable.tagsJson]),
|
||||
status = batchRow[TraceabilityBatchesTable.status],
|
||||
currentStep = batchRow[TraceabilityBatchesTable.currentStep],
|
||||
@@ -260,8 +657,8 @@ object TraceabilityDao {
|
||||
)
|
||||
}
|
||||
|
||||
fun createBatch(request: CreateTraceBatchRequest): TraceBatchDetailResponse = transaction {
|
||||
val template = getTemplate(request.templateUuid()) ?: error("template not found")
|
||||
fun createBatch(request: CreateTraceBatchRequest): TraceBatchDetailResponse? = transaction {
|
||||
val template = getTemplate(request.templateUuid()) ?: return@transaction null
|
||||
val now = timestampLiteral(nowInstant())
|
||||
val batchId = TraceabilityBatchesTable.insertAndGetId {
|
||||
it[this.templateId] = request.templateUuid()
|
||||
@@ -269,7 +666,7 @@ object TraceabilityDao {
|
||||
it[batchCode] = request.batchCode
|
||||
it[productName] = request.productName
|
||||
it[summary] = request.summary
|
||||
it[coverImage] = request.coverImage
|
||||
it[coverImage] = request.coverImage.ifBlank { template.coverImage }
|
||||
it[tagsJson] = json.encodeToString(request.tags)
|
||||
it[status] = "draft"
|
||||
it[currentStep] = 0
|
||||
@@ -287,6 +684,7 @@ object TraceabilityDao {
|
||||
it[description] = node.description
|
||||
it[locked] = node.locked
|
||||
it[consumerVisible] = node.consumerVisible
|
||||
it[fieldsJson] = json.encodeToString(node.fields)
|
||||
it[status] = "pending"
|
||||
it[operatorName] = ""
|
||||
it[valuesJson] = buildDefaultValues(node.fields)
|
||||
@@ -295,7 +693,7 @@ object TraceabilityDao {
|
||||
}
|
||||
}
|
||||
|
||||
getBatch(batchId)!!
|
||||
getBatch(batchId)
|
||||
}
|
||||
|
||||
fun updateBatchBase(batchId: UUID, request: UpdateTraceBatchBaseRequest): TraceBatchDetailResponse? = transaction {
|
||||
@@ -357,11 +755,19 @@ object TraceabilityDao {
|
||||
getBatch(batchId)
|
||||
}
|
||||
|
||||
fun getPublicDetailByCode(batchCode: String, increaseScan: Boolean = false): TraceabilityPublicDetailResponse? = transaction {
|
||||
fun getPublicDetailByCode(
|
||||
batchCode: String,
|
||||
increaseScan: Boolean = false,
|
||||
onlyPublished: Boolean = true,
|
||||
): TraceabilityPublicDetailResponse? = transaction {
|
||||
val batchRow = TraceabilityBatchesTable.selectAll()
|
||||
.where { TraceabilityBatchesTable.batchCode eq batchCode }
|
||||
.singleOrNull() ?: return@transaction null
|
||||
|
||||
if (onlyPublished && batchRow[TraceabilityBatchesTable.status] != "published") {
|
||||
return@transaction null
|
||||
}
|
||||
|
||||
if (increaseScan) {
|
||||
TraceabilityBatchesTable.update({ TraceabilityBatchesTable.id eq batchRow[TraceabilityBatchesTable.id].value }) {
|
||||
it[scanCount] = batchRow[TraceabilityBatchesTable.scanCount] + 1
|
||||
@@ -377,6 +783,52 @@ object TraceabilityDao {
|
||||
)
|
||||
}
|
||||
|
||||
fun getPreviewPublicDetailByCode(previewCode: String): TraceabilityPublicDetailResponse? = transaction {
|
||||
val pageRow = TraceabilityPreviewPagesTable.selectAll()
|
||||
.where { TraceabilityPreviewPagesTable.previewCode eq previewCode }
|
||||
.singleOrNull() ?: return@transaction null
|
||||
val pageId = pageRow[TraceabilityPreviewPagesTable.id].value
|
||||
val nodes = loadPreviewNodes(pageId)
|
||||
val batchLike = TraceBatchDetailResponse(
|
||||
id = pageId.toString(),
|
||||
templateId = "",
|
||||
templateName = "预演页",
|
||||
batchName = pageRow[TraceabilityPreviewPagesTable.name],
|
||||
batchCode = pageRow[TraceabilityPreviewPagesTable.previewCode],
|
||||
productName = pageRow[TraceabilityPreviewPagesTable.productName],
|
||||
summary = pageRow[TraceabilityPreviewPagesTable.description],
|
||||
coverImage = pageRow[TraceabilityPreviewPagesTable.coverImage],
|
||||
coverImagePreviewUrl = resolveStoredImagePreviewUrl(pageRow[TraceabilityPreviewPagesTable.coverImage]).orEmpty(),
|
||||
tags = decodeStringList(pageRow[TraceabilityPreviewPagesTable.tagsJson]),
|
||||
status = "preview",
|
||||
currentStep = 0,
|
||||
scanCount = 0,
|
||||
publicUrl = previewPublicUrl(pageRow[TraceabilityPreviewPagesTable.previewCode]),
|
||||
steps = nodes.map {
|
||||
TraceBatchStepResponse(
|
||||
id = it.id,
|
||||
sort = it.sort,
|
||||
category = it.category,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
consumerVisible = it.consumerVisible,
|
||||
status = "preview",
|
||||
operatorName = "",
|
||||
values = it.values,
|
||||
valuePreviewUrls = it.valuePreviewUrls,
|
||||
fields = it.fields,
|
||||
)
|
||||
},
|
||||
updatedAt = formatTimestamp(pageRow[TraceabilityPreviewPagesTable.updatedAt]),
|
||||
publishedAt = formatTimestamp(pageRow[TraceabilityPreviewPagesTable.updatedAt]),
|
||||
)
|
||||
TraceabilityPublicDetailResponse(
|
||||
batch = batchLike,
|
||||
publicSections = batchLike.steps.filter { it.category == "public" && it.consumerVisible },
|
||||
businessSections = batchLike.steps.filter { it.category != "public" && it.consumerVisible },
|
||||
)
|
||||
}
|
||||
|
||||
fun listFeedback(): List<TraceabilityFeedbackResponse> = transaction {
|
||||
val batchMap = TraceabilityBatchesTable.selectAll()
|
||||
.associateBy { it[TraceabilityBatchesTable.id].value }
|
||||
@@ -400,14 +852,14 @@ object TraceabilityDao {
|
||||
}
|
||||
}
|
||||
|
||||
fun submitFeedback(request: SubmitTraceabilityFeedbackRequest): TraceabilityFeedbackResponse = transaction {
|
||||
fun submitFeedback(request: SubmitTraceabilityFeedbackRequest): TraceabilityFeedbackResponse? = transaction {
|
||||
val batchId = when {
|
||||
!request.batchId.isNullOrBlank() -> UUID.fromString(request.batchId)
|
||||
!request.batchId.isNullOrBlank() -> runCatching { UUID.fromString(request.batchId) }.getOrNull()
|
||||
!request.batchCode.isNullOrBlank() -> TraceabilityBatchesTable.selectAll()
|
||||
.where { TraceabilityBatchesTable.batchCode eq request.batchCode }
|
||||
.single()[TraceabilityBatchesTable.id].value
|
||||
else -> error("batch not found")
|
||||
}
|
||||
.singleOrNull()?.get(TraceabilityBatchesTable.id)?.value
|
||||
else -> null
|
||||
} ?: return@transaction null
|
||||
|
||||
val now = timestampLiteral(nowInstant())
|
||||
val feedbackId = TraceabilityFeedbackTable.insertAndGetId {
|
||||
@@ -420,7 +872,7 @@ object TraceabilityDao {
|
||||
it[createdAt] = now
|
||||
}.value
|
||||
|
||||
listFeedback().first { it.id == feedbackId.toString() }
|
||||
listFeedback().firstOrNull { it.id == feedbackId.toString() }
|
||||
}
|
||||
|
||||
private fun loadTemplateNodes(templateId: UUID): List<TraceTemplateNodeResponse> {
|
||||
@@ -441,12 +893,34 @@ object TraceabilityDao {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPreviewNodes(previewId: UUID): List<TracePreviewNodeResponse> {
|
||||
return TraceabilityPreviewNodesTable.selectAll()
|
||||
.where { TraceabilityPreviewNodesTable.previewPageId eq previewId }
|
||||
.orderBy(TraceabilityPreviewNodesTable.sort, SortOrder.ASC)
|
||||
.map { row ->
|
||||
val fields = decodeFields(row[TraceabilityPreviewNodesTable.fieldsJson])
|
||||
val values = decodeValues(row[TraceabilityPreviewNodesTable.valuesJson])
|
||||
TracePreviewNodeResponse(
|
||||
id = row[TraceabilityPreviewNodesTable.id].value.toString(),
|
||||
sort = row[TraceabilityPreviewNodesTable.sort],
|
||||
category = row[TraceabilityPreviewNodesTable.category],
|
||||
name = row[TraceabilityPreviewNodesTable.name],
|
||||
description = row[TraceabilityPreviewNodesTable.description],
|
||||
consumerVisible = row[TraceabilityPreviewNodesTable.consumerVisible],
|
||||
values = values,
|
||||
valuePreviewUrls = buildValuePreviewUrls(fields, values),
|
||||
fields = fields,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadBatchSteps(batchId: UUID): List<TraceBatchStepResponse> {
|
||||
return TraceabilityBatchStepsTable.selectAll()
|
||||
.where { TraceabilityBatchStepsTable.batchId eq batchId }
|
||||
.orderBy(TraceabilityBatchStepsTable.sort, SortOrder.ASC)
|
||||
.map { row ->
|
||||
val fields = row[TraceabilityBatchStepsTable.templateNodeId]?.value?.let { nodeId ->
|
||||
val snapshotFields = decodeFields(row[TraceabilityBatchStepsTable.fieldsJson])
|
||||
val fields = snapshotFields.takeIf { it.isNotEmpty() } ?: row[TraceabilityBatchStepsTable.templateNodeId]?.value?.let { nodeId ->
|
||||
TraceabilityTemplateNodesTable.selectAll()
|
||||
.where { TraceabilityTemplateNodesTable.id eq nodeId }
|
||||
.singleOrNull()
|
||||
@@ -465,6 +939,10 @@ object TraceabilityDao {
|
||||
status = row[TraceabilityBatchStepsTable.status],
|
||||
operatorName = row[TraceabilityBatchStepsTable.operatorName],
|
||||
values = decodeValues(row[TraceabilityBatchStepsTable.valuesJson]),
|
||||
valuePreviewUrls = buildValuePreviewUrls(
|
||||
fields,
|
||||
decodeValues(row[TraceabilityBatchStepsTable.valuesJson]),
|
||||
),
|
||||
completedAt = formatTimestamp(row[TraceabilityBatchStepsTable.completedAt]),
|
||||
fields = fields,
|
||||
)
|
||||
@@ -479,9 +957,15 @@ object TraceabilityDao {
|
||||
type = it.type,
|
||||
required = it.required,
|
||||
visible = it.visible,
|
||||
fixedPreset = it.fixedPreset,
|
||||
placeholder = it.placeholder,
|
||||
defaultValue = it.defaultValue,
|
||||
defaultPreviewUrl = if (it.type == "image") resolveImagePreviewUrl(it.defaultValue) else null,
|
||||
options = it.options,
|
||||
fieldStyle = TraceFieldStyleResponse(
|
||||
bold = it.fieldStyle.bold,
|
||||
color = it.fieldStyle.color,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -498,6 +982,81 @@ object TraceabilityDao {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
private fun buildValuePreviewUrls(
|
||||
fields: List<TraceFieldDefinitionResponse>,
|
||||
values: JsonObject,
|
||||
): Map<String, String> {
|
||||
return fields.filter { it.type == "image" }
|
||||
.mapNotNull { field ->
|
||||
resolveImagePreviewUrl(values[field.key])?.let { field.key to it }
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun resolveStoredImagePreviewUrl(raw: String?): String? {
|
||||
val text = raw?.trim().orEmpty()
|
||||
if (text.isBlank()) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
if (text.startsWith("{")) {
|
||||
resolveImagePreviewUrl(json.parseToJsonElement(text))
|
||||
} else {
|
||||
resolveImagePreviewUrl(JsonPrimitive(text))
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun extractStoredImageReference(raw: String?): Pair<String, String> {
|
||||
val text = raw?.trim().orEmpty()
|
||||
if (text.isBlank()) {
|
||||
return "" to ""
|
||||
}
|
||||
return runCatching {
|
||||
if (text.startsWith("{")) {
|
||||
val element = json.parseToJsonElement(text)
|
||||
if (element is JsonObject) {
|
||||
val bucketName = element["bucketName"]?.jsonPrimitive?.content?.trim().orEmpty()
|
||||
val objectName = element["objectName"]?.jsonPrimitive?.content?.trim().orEmpty()
|
||||
bucketName to objectName
|
||||
} else {
|
||||
"" to text
|
||||
}
|
||||
} else if (text.startsWith("http://") || text.startsWith("https://")) {
|
||||
"" to text
|
||||
} else {
|
||||
OSSUtils.defaultBucket() to text
|
||||
}
|
||||
}.getOrDefault("" to text)
|
||||
}
|
||||
|
||||
private fun resolveImagePreviewUrl(value: JsonElement?): String? = runCatching {
|
||||
when (value) {
|
||||
null, JsonNull -> null
|
||||
is JsonPrimitive -> {
|
||||
val raw = value.content.trim()
|
||||
when {
|
||||
raw.isBlank() -> null
|
||||
raw.startsWith("http://") || raw.startsWith("https://") -> raw
|
||||
else -> OSSUtils.getTempUrl(OSSUtils.defaultBucket(), raw)
|
||||
}
|
||||
}
|
||||
|
||||
is JsonObject -> {
|
||||
val bucketName = value["bucketName"]?.jsonPrimitive?.content?.trim().orEmpty()
|
||||
.ifBlank { OSSUtils.defaultBucket() }
|
||||
val objectName = value["objectName"]?.jsonPrimitive?.content?.trim().orEmpty()
|
||||
if (objectName.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
OSSUtils.getTempUrl(bucketName, objectName)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
private fun buildDefaultValues(fields: List<TraceFieldDefinitionResponse>): String {
|
||||
val values = buildJsonObject {
|
||||
fields.forEach { field ->
|
||||
@@ -511,6 +1070,15 @@ object TraceabilityDao {
|
||||
value?.toString()?.replace('T', ' ')?.replace("Z", "") ?: ""
|
||||
|
||||
private fun publicUrl(code: String): String {
|
||||
return "$publicPreviewBaseUrl/p/$code"
|
||||
return "$publicPreviewBaseUrl/f10/$code"
|
||||
}
|
||||
|
||||
private fun previewPublicUrl(code: String): String {
|
||||
return "$publicPreviewBaseUrl/preview/$code"
|
||||
}
|
||||
|
||||
private fun buildPreviewCode(): String {
|
||||
return "PV-${Clock.System.now().epochSeconds.toString().takeLast(8)}-${UUID.randomUUID().toString().take(6)}"
|
||||
.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,3 +40,7 @@ ktor:
|
||||
fallback-bucket: "system"
|
||||
fallback-object: "favicon.ico"
|
||||
|
||||
traceability:
|
||||
# public-preview-base-url: "http://127.0.0.1:8081" # 开发测试用
|
||||
public-preview-base-url: "https://ats.f10.bbitcn.com" # 生产环境用
|
||||
|
||||
|
||||
Reference in New Issue
Block a user