修复溯源模块大量问题

This commit is contained in:
BBIT-Kai
2026-04-14 10:10:52 +08:00
parent 0a43f5e4b9
commit 1c68762421
26 changed files with 3413 additions and 463 deletions
+32 -2
View File
@@ -38,7 +38,7 @@ fun Application.configureRouting() {
call.respond(mapOf("status" to "ok"))
}
get("/p/{code}") {
get("/f10/{code}") {
val code = call.parameters["code"]?.trim().orEmpty()
if (code.isBlank()) {
call.respondText("批次编码不能为空", status = HttpStatusCode.BadRequest)
@@ -75,6 +75,36 @@ fun Application.configureRouting() {
)
}
get("/preview/{code}") {
val code = call.parameters["code"]?.trim().orEmpty()
if (code.isBlank()) {
call.respondText("预演页编码不能为空", status = HttpStatusCode.BadRequest)
return@get
}
val page = call.traceabilityService().loadPreviewPage(code)
if (page == null) {
call.respond(
HttpStatusCode.NotFound,
FreeMarkerContent(
"error.ftl",
mapOf("message" to "未找到对应的预演页,请确认链接是否正确。"),
),
)
return@get
}
call.respond(
FreeMarkerContent(
"traceability.ftl",
mapOf(
"page" to page,
"feedbackMessage" to "",
),
),
)
}
post("/feedback") {
val params = call.receiveParameters()
val code = params["batchCode"]?.trim().orEmpty()
@@ -92,7 +122,7 @@ fun Application.configureRouting() {
rating = params["rating"]?.toIntOrNull() ?: 5,
)
val result = if (response.status) "success" else "failed"
call.respondRedirect("/p/$code?result=$result")
call.respondRedirect("/f10/$code?result=$result")
}
staticResources("/static", "static")
+12
View File
@@ -52,6 +52,18 @@ class TraceabilityClient(
return payload.data
}
suspend fun fetchPreviewDetail(code: String): TraceabilityPublicDetailResponse? {
val response = client.get {
url("$coreBaseUrl/traceability/public/preview-page/by-code/$code")
accept(ContentType.Application.Json)
}
if (!response.status.isSuccess()) {
return null
}
val payload = response.body<ApiResponse<TraceabilityPublicDetailResponse>>()
return payload.data
}
suspend fun submitFeedback(request: SubmitTraceabilityFeedbackRequest): ApiResponse<TraceabilityFeedbackResponse> {
val response = client.post {
url("$coreBaseUrl/traceability/public/feedback")
+12 -1
View File
@@ -11,6 +11,12 @@ data class ApiResponse<T>(
val data: T? = null,
)
@Serializable
data class TraceFieldStyleResponse(
val bold: Boolean = false,
val color: String = "",
)
@Serializable
data class TraceFieldDefinitionResponse(
val key: String,
@@ -20,7 +26,9 @@ data class TraceFieldDefinitionResponse(
val visible: Boolean = true,
val placeholder: String = "",
val defaultValue: JsonElement? = null,
val defaultPreviewUrl: String? = null,
val options: List<String> = emptyList(),
val fieldStyle: TraceFieldStyleResponse = TraceFieldStyleResponse(),
)
@Serializable
@@ -35,6 +43,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(),
)
@@ -49,6 +58,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,
@@ -96,6 +106,8 @@ data class DisplayEntry(
val label: String,
val value: String,
val type: String = "string",
val bold: Boolean = false,
val color: String = "",
)
data class PublicSectionView(
@@ -116,7 +128,6 @@ data class TimelineSectionView(
data class PageViewModel(
val code: String,
val pageUrl: String,
val batchName: String,
val productName: String,
val templateName: String,
+24 -6
View File
@@ -1,4 +1,4 @@
package com.bbitcn
package com.bbitcn
import io.ktor.server.config.ApplicationConfig
import kotlinx.serialization.json.JsonArray
@@ -7,7 +7,6 @@ import kotlinx.serialization.json.JsonObject
data class TraceabilityPublicConfig(
val coreBaseUrl: String,
val publicBaseUrl: String,
)
class TraceabilityService(
@@ -20,12 +19,11 @@ class TraceabilityService(
return PageViewModel(
code = batch.batchCode,
pageUrl = "${config.publicBaseUrl.trimEnd('/')}/p/${batch.batchCode}",
batchName = batch.batchName,
productName = batch.productName.ifBlank { batch.templateName },
templateName = batch.templateName,
summary = batch.summary.ifBlank { "该批次已完成关键环节留痕,可查看公开资料与业务流程。" },
coverImage = batch.coverImage,
coverImage = batch.coverImagePreviewUrl.ifBlank { batch.coverImage },
scanCount = batch.scanCount,
publishedAt = formatDateOnly(batch.publishedAt),
tagsText = batch.tags.joinToString("").ifBlank { "暂无标签" },
@@ -34,6 +32,24 @@ class TraceabilityService(
)
}
suspend fun loadPreviewPage(code: String): PageViewModel? {
val detail = client.fetchPreviewDetail(code) ?: return null
val batch = detail.batch
return PageViewModel(
code = batch.batchCode,
batchName = batch.batchName,
productName = batch.productName.ifBlank { batch.templateName },
templateName = "预演页",
summary = batch.summary.ifBlank { "当前为预演页内容,可持续调整字段和值供客户确认。" },
coverImage = batch.coverImagePreviewUrl.ifBlank { batch.coverImage },
scanCount = batch.scanCount,
publishedAt = formatDateOnly(batch.updatedAt),
tagsText = batch.tags.joinToString("").ifBlank { "暂无标签" },
publicSections = detail.publicSections.map(::toPublicSectionView),
businessSections = detail.businessSections.map(::toTimelineSectionView),
)
}
suspend fun submitFeedback(
code: String,
type: String,
@@ -80,10 +96,13 @@ class TraceabilityService(
private fun toDisplayEntries(step: TraceBatchStepResponse): List<DisplayEntry> {
return step.values.entries.map { (key, value) ->
val field = step.fields.find { it.key == key }
val imageUrl = step.valuePreviewUrls[key].orEmpty()
DisplayEntry(
label = field?.label ?: key,
value = formatJsonValue(value),
value = if ((field?.type ?: "string") == "image" && imageUrl.isNotBlank()) imageUrl else formatJsonValue(value),
type = field?.type ?: "string",
bold = field?.fieldStyle?.bold ?: false,
color = field?.fieldStyle?.color.orEmpty(),
)
}
}
@@ -106,6 +125,5 @@ class TraceabilityService(
fun ApplicationConfig.toTraceabilityPublicConfig(): TraceabilityPublicConfig {
return TraceabilityPublicConfig(
coreBaseUrl = property("traceability.core-base-url").getString().trimEnd('/'),
publicBaseUrl = property("traceability.public-base-url").getString().trimEnd('/'),
)
}
+3 -2
View File
@@ -6,5 +6,6 @@ ktor:
port: 8081
traceability:
core-base-url: "http://127.0.0.1:8089"
public-base-url: "http://127.0.0.1:8081"
# 访问主服务的地址
# core-base-url: "http://127.0.0.1:8089" # 开发
core-base-url: "https://ai.ronsunny.cn:8090/api" # 生产
+51 -31
View File
@@ -17,9 +17,9 @@ a {
}
.page-shell {
max-width: 1240px;
max-width: 1440px;
margin: 0 auto;
padding: 28px 16px 48px;
padding: 36px 40px 56px;
}
.hero,
@@ -30,6 +30,25 @@ a {
box-shadow: 0 16px 48px rgba(16, 24, 40, 0.08);
}
.cover-panel {
margin-bottom: 18px;
}
.cover-card {
width: 100%;
overflow: hidden;
border: 1px solid rgba(228, 234, 245, 0.9);
border-radius: 28px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 16px 48px rgba(16, 24, 40, 0.08);
}
.cover-card img {
display: block;
width: 100%;
height: auto;
}
.hero {
display: grid;
grid-template-columns: 1fr;
@@ -37,11 +56,6 @@ a {
padding: 26px;
}
.hero--with-cover {
grid-template-columns: minmax(0, 1.2fr) 320px;
align-items: stretch;
}
.hero h1,
.panel h2,
.info-card h3,
@@ -69,26 +83,11 @@ a {
.hero__stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.hero__cover {
overflow: hidden;
border: 1px solid #e8eef7;
border-radius: 22px;
background: #fff;
min-height: 240px;
}
.hero__cover img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.stat-card,
.summary-card,
.kv-card,
@@ -134,6 +133,7 @@ a {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
align-content: start;
}
.panel {
@@ -146,9 +146,9 @@ a {
}
.tabs-nav {
display: inline-flex;
flex-wrap: wrap;
gap: 10px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
padding: 8px;
border: 1px solid #e8eef7;
border-radius: 999px;
@@ -157,7 +157,10 @@ a {
}
.tab-btn {
min-width: 112px;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 42px;
padding: 0 18px;
border: none;
@@ -168,6 +171,8 @@ a {
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
white-space: nowrap;
}
.tab-btn.active {
@@ -348,6 +353,14 @@ a {
margin-top: 64px;
}
.page-footer {
padding: 24px 4px 8px;
text-align: center;
color: #7d8899;
font-size: 13px;
line-height: 1.8;
}
@media (max-width: 992px) {
.hero,
.form-grid,
@@ -363,14 +376,21 @@ a {
.timeline-item__head {
flex-direction: column;
}
}
@media (max-width: 768px) {
.page-shell {
padding: 24px 18px 44px;
}
.tabs-nav {
display: grid;
grid-template-columns: 1fr;
border-radius: 20px;
gap: 4px;
padding: 6px;
}
.tab-btn {
width: 100%;
min-height: 40px;
padding: 0 10px;
font-size: 13px;
}
}
@@ -8,7 +8,15 @@
</head>
<body>
<div class="page-shell">
<section class="hero<#if page.coverImage?has_content> hero--with-cover</#if>">
<#if page.coverImage?has_content>
<section class="cover-panel">
<div class="cover-card">
<img src="${page.coverImage}" alt="${page.batchName}" />
</div>
</section>
</#if>
<section class="hero">
<div class="hero__content">
<h1>${page.batchName}</h1>
<p>${page.summary}</p>
@@ -21,51 +29,35 @@
<span>产品名称</span>
<strong>${page.productName}</strong>
</div>
<div class="stat-card">
<span>所属模板</span>
<strong>${page.templateName}</strong>
</div>
<div class="stat-card">
<span>累计访问</span>
<strong>${page.scanCount}</strong>
</div>
</div>
</div>
<#if page.coverImage?has_content>
<div class="hero__cover">
<img src="${page.coverImage}" alt="${page.productName}" />
</div>
</#if>
<div class="hero__aside">
<div class="summary-card">
<span>发布时间</span>
<strong>${page.publishedAt}</strong>
</div>
<div class="summary-card">
<span>标签</span>
<strong>${page.tagsText}</strong>
</div>
<#if page.tagsText != "暂无标签">
<div class="summary-card">
<span>标签</span>
<strong>${page.tagsText}</strong>
</div>
</#if>
</div>
</section>
<#if feedbackMessage?has_content>
<section class="notice">${feedbackMessage}</section>
</#if>
<section class="panel tabs-panel">
<div class="tabs-nav" role="tablist" aria-label="溯源页面内容切换">
<button class="tab-btn active" data-tab-target="timeline-panel" type="button">溯源链</button>
<button class="tab-btn" data-tab-target="public-panel" type="button">公开资料</button>
<button class="tab-btn" data-tab-target="feedback-panel" type="button">反馈投诉</button>
<button class="tab-btn" data-tab-target="feedback-panel" type="button">反馈投诉</button>
</div>
<div id="timeline-panel" class="tab-panel active">
<div class="panel__head">
<div>
<h2>溯源链</h2>
<p>按业务流程顺序查看本批次的处理过程与留痕信息。</p>
</div>
</div>
<#if page.businessSections?size gt 0>
<div class="timeline">
<#list page.businessSections as section>
@@ -85,10 +77,10 @@
<#list section.entries as entry>
<div class="kv-card">
<span>${entry.label}</span>
<#if entry.type == "image" && entry.value != "未填写">
<#if entry.type == "image" && entry.value?has_content && entry.value != "未填写">
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
<#else>
<strong>${entry.value}</strong>
<strong<#if entry.bold || entry.color?has_content> style="<#if entry.bold>font-weight:700;</#if><#if entry.color?has_content>color:${entry.color};</#if>"</#if>>${entry.value}</strong>
</#if>
</div>
</#list>
@@ -103,12 +95,6 @@
</div>
<div id="public-panel" class="tab-panel">
<div class="panel__head">
<div>
<h2>公开资料</h2>
<p>面向消费者展示的企业资料、资质证明及其他公开信息。</p>
</div>
</div>
<#if page.publicSections?size gt 0>
<div class="public-grid">
<#list page.publicSections as section>
@@ -121,10 +107,10 @@
<#list section.entries as entry>
<div class="kv-card">
<span>${entry.label}</span>
<#if entry.type == "image" && entry.value != "未填写">
<#if entry.type == "image" && entry.value?has_content && entry.value != "未填写">
<img class="kv-image" src="${entry.value}" alt="${entry.label}" />
<#else>
<strong>${entry.value}</strong>
<strong<#if entry.bold || entry.color?has_content> style="<#if entry.bold>font-weight:700;</#if><#if entry.color?has_content>color:${entry.color};</#if>"</#if>>${entry.value}</strong>
</#if>
</div>
</#list>
@@ -140,10 +126,15 @@
<div id="feedback-panel" class="tab-panel">
<div class="panel__head">
<div>
<h2>反馈投诉</h2>
<h2>反馈投诉</h2>
<p>如发现信息异常、商品质量问题,或有建议,可直接提交。</p>
</div>
</div>
<#if feedbackMessage?has_content>
<div class="notice">${feedbackMessage}</div>
</#if>
<form class="feedback-form" method="post" action="/feedback">
<input type="hidden" name="batchCode" value="${page.code}" />
<div class="form-grid">
@@ -155,6 +146,7 @@
<option value="consult">咨询</option>
</select>
</label>
<label class="form-item">
<span>满意度</span>
<select name="rating">
@@ -165,19 +157,26 @@
<option value="1">1 分</option>
</select>
</label>
<label class="form-item">
<span>联系方式</span>
<input name="contact" placeholder="手机号 / 邮箱 / 微信" />
</label>
</div>
<label class="form-item form-item--full">
<span>反馈内容</span>
<textarea name="content" placeholder="请填写你要反馈的问题或建议" required></textarea>
</label>
<button type="submit" class="submit-btn">提交反馈</button>
</form>
</div>
</section>
<footer class="page-footer">
技术支持:四川主干信息技术有限公司 BBITCN Co.,Ltd
</footer>
</div>
<script>