From ac374e5100c8e600f1b74ceb228cde49ebb1272c Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期五, 18 九月 2026 09:43:39 +0800
Subject: [PATCH] 更新....

---
 talent-card-admin/src/views/MedicalReservationView.vue                                                            |    4 
 talent-card-admin/src/views/MedicalApplicationView.vue                                                            |   13 +
 talent-card-admin/src/views/AppointmentManagementView.vue                                                         |    6 
 talent-card-admin/src/views/MovieTicketRecordView.vue                                                             |    9 +
 talent-card-admin/src/views/StaffManagementView.vue                                                               |   18 +
 talent-card-server/src/main/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalService.java      |   33 +---
 talent-card-server/src/test/java/com/talentcard/server/business/talent/TalentServiceTests.java                    |   55 ++++++
 talent-card-admin/src/views/HotelVerificationView.vue                                                             |   11 +
 talent-card-server/src/test/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalServiceTests.java |   12 +
 talent-card-admin/src/views/MovieTicketView.vue                                                                   |   34 ----
 talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java                    |   29 +++
 rencaika/pages/policy/policy.uvue                                                                                 |   11 +
 rencaika/pages/news/news.uvue                                                                                     |   24 ++
 talent-card-admin/src/views/CarWashBenefitView.vue                                                                |    4 
 talent-card-admin/src/views/ProductView.vue                                                                       |    8 
 talent-card-server/src/main/java/com/talentcard/server/business/talent/TalentService.java                         |   42 +++-
 talent-card-admin/src/views/TalentListView.vue                                                                    |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java                   |    4 
 rencaika/pages/coupon-select/coupon-select.uvue                                                                   |   26 +++
 talent-card-server/src/main/java/com/talentcard/server/business/card/CardAdminService.java                        |   12 -
 talent-card-admin/src/views/SpouseEmploymentView.vue                                                              |    4 
 talent-card-admin/src/views/UpgradeApplicationDetailView.vue                                                      |   13 +
 talent-card-admin/src/views/MerchantCreateView.vue                                                                |   12 +
 talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java                         |   36 +++
 rencaika/services/business.uts                                                                                    |   19 ++
 talent-card-admin/src/style.css                                                                                   |    3 
 talent-card-admin/src/views/HotelReservationView.vue                                                              |    8 
 talent-card-admin/src/views/FuelServiceView.vue                                                                   |    4 
 28 files changed, 334 insertions(+), 122 deletions(-)

diff --git a/rencaika/pages/coupon-select/coupon-select.uvue b/rencaika/pages/coupon-select/coupon-select.uvue
index eea21e4..a53ab2e 100644
--- a/rencaika/pages/coupon-select/coupon-select.uvue
+++ b/rencaika/pages/coupon-select/coupon-select.uvue
@@ -25,7 +25,7 @@
 			<view v-else-if="coupons.length > 0" class="coupon-list">
 				<view class="coupon-card" v-for="item in coupons" :key="item.id" hover-class="coupon-card-active" @tap="selectCoupon(item)">
 					<view class="coupon-amount">
-						<view class="amount-line"><text class="amount-number">{{item.amount}}</text><text class="amount-unit">元</text></view>
+						<view class="amount-line"><text class="amount-number" :class="amountNumberClass(item.amount)">{{item.amount}}</text><text class="amount-unit" :class="amountUnitClass(item.amount)">元</text></view>
 					</view>
 					<view class="coupon-info">
 						<image class="ticket-mark" src="/static/product-assets/coupon-ticket.svg"></image>
@@ -65,6 +65,23 @@
 	const loading = ref(false)
 	const loadFailed = ref(false)
 	const selecting = ref(false)
+
+	// 左侧色块宽度固定 218rpx,金额位数多(如 178.84)时必须缩小字号,
+	// 否则文字会超出色块被裁掉(uni-app x 里 view 默认 overflow: hidden)。
+	const amountNumberClass = (value: any): string => {
+		const length = value == null ? 0 : ('' + value).length
+		if (length >= 9) return 'number-xxs'
+		if (length >= 7) return 'number-xs'
+		if (length >= 5) return 'number-sm'
+		return ''
+	}
+	const amountUnitClass = (value: any): string => {
+		const length = value == null ? 0 : ('' + value).length
+		if (length >= 9) return 'unit-xxs'
+		if (length >= 7) return 'unit-xs'
+		if (length >= 5) return 'unit-sm'
+		return ''
+	}
 
 	onLoad((options: any) => {
 		// #ifdef MP-WEIXIN
@@ -164,6 +181,13 @@
 	.amount-line { display: flex; margin-top: 5rpx; color: #FFFFFF; flex-direction: row; align-items: flex-end; }
 	.amount-number { font-size: 78rpx; font-weight: 700; line-height: 88rpx; }
 	.amount-unit { margin: 0 0 11rpx 11rpx; font-size: 27rpx; font-weight: 600; }
+	/* 金额位数多的档位:写在这两行之后,同优先级下后面的规则生效,字号按位数逐级缩小。 */
+	.number-sm { font-size: 52rpx; line-height: 62rpx; }
+	.unit-sm { margin: 0 0 8rpx 8rpx; font-size: 22rpx; }
+	.number-xs { font-size: 42rpx; line-height: 52rpx; }
+	.unit-xs { margin: 0 0 7rpx 7rpx; font-size: 19rpx; }
+	.number-xxs { font-size: 34rpx; line-height: 44rpx; }
+	.unit-xxs { margin: 0 0 6rpx 6rpx; font-size: 17rpx; }
 	.coupon-info { display: flex; min-width: 0; flex: 1; height: 248rpx; box-sizing: border-box; padding: 27rpx 32rpx 25rpx 45rpx; position: relative; flex-direction: column; justify-content: space-between; }
 	.ticket-mark { position: absolute; top: 39rpx; right: 29rpx; width: 48rpx; height: 48rpx; opacity: .15; }
 	.coupon-name { color: #1C2028; font-size: 36rpx; font-weight: 700; line-height: 50rpx; }
diff --git a/rencaika/pages/news/news.uvue b/rencaika/pages/news/news.uvue
index 5b7656b..6701588 100644
--- a/rencaika/pages/news/news.uvue
+++ b/rencaika/pages/news/news.uvue
@@ -92,7 +92,15 @@
 	const contentPreview = (value: any): string => {
 		if (value == null) return ''
 		const plain = (value as string).replace(/\s+/g, ' ').trim()
-		return plain.length > 64 ? plain.substring(0, 64) + '...' : plain
+		// 摘要区固定 3 行:按 3 行能放下的字数截断并补省略号,
+		// 避免文字被容器高度从中间切断(只露出半行、又没有省略号)。
+		return plain.length > 54 ? plain.substring(0, 54) + '...' : plain
+	}
+	// 标签只占一行:压掉换行/连续空格(text 组件会把 \n 渲染成换行),
+	// 放不下时由样式的单行省略号收尾。
+	const newsTagText = (value: any): string => {
+		if (value == null) return ''
+		return ('' + value).replace(/\s+/g, ' ').trim()
 	}
 	const mapNewsRecords = (records: any[]): any[] => {
 		const mapped: any[] = []
@@ -102,7 +110,7 @@
 				id: '' + item.id,
 				title: item.title != null ? item.title as string : '人才资讯',
 				summary: contentPreview(item.contentPreview),
-				tag: item.summary != null ? item.summary as string : '',
+				tag: newsTagText(item.summary),
 				time: formatNewsTime(item.publishTime)
 			})
 		}
@@ -209,10 +217,14 @@
 	.news-title { display: block; color: #252525; font-size: 30rpx; font-weight: 700; line-height: 41rpx; }
 	.news-body { display: flex; height: 174rpx; margin-top: 13rpx; flex-direction: row; }
 	.news-copy { display: flex; flex: 1; min-width: 0; padding-right: 18rpx; flex-direction: column; justify-content: space-between; }
-	.news-summary { display: block; height: 105rpx; overflow: hidden; color: #8E9299; font-size: 23rpx; line-height: 34rpx; }
-	.news-meta { display: flex; height: 42rpx; flex-direction: row; align-items: center; }
-	.news-tag { padding: 5rpx 9rpx; border-radius: 6rpx; background-color: #FFF8EA; color: #F1A32D; font-size: 19rpx; }
-	.news-time { margin-left: 13rpx; color: #A5A8AE; font-size: 20rpx; }
+	/* 摘要固定 3 行(3×34rpx=102rpx),超出用省略号,不再从文字中间截断。 */
+	.news-summary { display: -webkit-box; height: 102rpx; overflow: hidden; -webkit-line-clamp: 3; -webkit-box-orient: vertical; color: #8E9299; font-size: 23rpx; line-height: 34rpx; }
+	.news-meta { display: flex; height: 42rpx; min-width: 0; flex-direction: row; align-items: center; }
+	/* 来源/摘要这行可能很长:按内容宽度排,放不下时收缩并省略,不去挤右侧时间。
+	   flex-shrink 必须显式写 1:uni-app x 的全局样式给 view/text 默认 flex-shrink: 0,
+	   不覆盖的话标签不会收缩,只会顶出卡片、把时间挤没(省略号也不会出现)。 */
+	.news-tag { display: block; flex: 0 1 auto; flex-grow: 0; flex-shrink: 1; flex-basis: auto; min-width: 0; padding: 5rpx 9rpx; border-radius: 6rpx; background-color: #FFF8EA; color: #F1A32D; font-size: 19rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+	.news-time { margin-left: 13rpx; color: #A5A8AE; font-size: 20rpx; flex-shrink: 0; }
 	.news-thumb { width: 236rpx; height: 168rpx; border-radius: 7rpx; flex-shrink: 0; }
 	.bottom-space { height: calc(28rpx + env(safe-area-inset-bottom)); }
 </style>
diff --git a/rencaika/pages/policy/policy.uvue b/rencaika/pages/policy/policy.uvue
index 2febab8..6c4ebc9 100644
--- a/rencaika/pages/policy/policy.uvue
+++ b/rencaika/pages/policy/policy.uvue
@@ -67,6 +67,12 @@
 		// #endif
 		loadInitialPolicies()
 	})
+	// 政策内容只展示一行:先把换行/连续空格压成单个空格,再由样式的单行省略号收尾,
+	// 否则文本里的 \n 会被 text 组件渲染成换行,一行放不下。
+	const policyPreview = (value: any): string => {
+		if (value == null) return ''
+		return ('' + value).replace(/\s+/g, ' ').trim()
+	}
 	const mapPolicies = (records: any[]): any[] => {
 		const mapped: any[] = []
 		for (let index = 0; index < records.length; index++) {
@@ -74,7 +80,7 @@
 			mapped.push({
 				id: '' + item.id,
 				title: item.title != null ? item.title as string : '人才政策',
-				description: item.contentPreview != null ? item.contentPreview as string : '',
+				description: policyPreview(item.contentPreview),
 				tag: item.subtitle != null && (item.subtitle as string).trim() != '' ? item.subtitle as string : '人才政策'
 			})
 		}
@@ -152,7 +158,8 @@
 	.policy-card { min-height: 220rpx; margin-bottom: 25rpx; box-sizing: border-box; padding: 31rpx 34rpx 25rpx; border-radius: 17rpx; background-color: rgba(255,255,255,.97); }
 	.policy-card-active { opacity: .78; }
 	.policy-title { display: block; color: #222326; font-size: 34rpx; font-weight: 700; }
-	.policy-desc { display: block; margin-top: 19rpx; color: #84868A; font-size: 27rpx; line-height: 39rpx; }
+	/* 政策内容只显示一行,超出用省略号。 */
+	.policy-desc { display: block; margin-top: 19rpx; color: #84868A; font-size: 27rpx; line-height: 39rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
 	.policy-foot { display: flex; margin-top: 18rpx; flex-direction: row; align-items: center; justify-content: space-between; }
 	.policy-tag { padding: 7rpx 9rpx; border-radius: 5rpx; background-color: #FFF4E7; color: #FF761D; font-size: 21rpx; }
 	.policy-detail { color: #77797D; font-size: 25rpx; }
diff --git a/rencaika/services/business.uts b/rencaika/services/business.uts
index 03f6783..754dcbf 100644
--- a/rencaika/services/business.uts
+++ b/rencaika/services/business.uts
@@ -5,11 +5,26 @@
 export const createIdempotencyKey = (scope: string): string => scope + '-' + Date.now() + '-' + Math.floor(Math.random() * 1000000000)
 
 export const sanitizeRichText = (html: string): string => {
-	return html
+	if (html == null) return ''
+	const base = getApiBaseUrl().replace(/\/+$/, '')
+	const content = ('' + html)
 		.replace(/<script[\s\S]*?<\/script>/gi, '')
 		.replace(/<style[\s\S]*?<\/style>/gi, '')
 		.replace(/\son[a-z]+\s*=\s*(['"]).*?\1/gi, '')
-		.replace(/\s(href|src)\s*=\s*(['"])(?!https:\/\/).*?\2/gi, '')
+		// 危险协议(javascript: 等)直接去掉属性。
+		.replace(/\s(href|src)\s*=\s*(['"])\s*javascript:[^'"]*\2/gi, '')
+	if (base == '') return content
+	// 后台富文本里的图片/链接存的是相对路径(/uploads/xxx.png)。小程序加载不了相对路径,
+	// 而原来这里会把所有非 https 的 src 整条删掉,图片就变成破图(只显示 alt 文字)。
+	// 这里统一补成接口域名下的绝对地址,http(s) 绝对地址原样保留。
+	return content
+		.replace(/(src|href)=(["'])\/\//gi, '$1=$2https://')
+		.replace(/(\s(?:src|href)\s*=\s*(['"]))\/+/gi, '$1' + base + '/')
+		.replace(/(\s(?:src|href)\s*=\s*(['"]))(?!https?:|\/\/|\/|data:|javascript:)/gi, '$1' + base + '/')
+		// 小程序 rich-text 不继承页面样式:后台图片带的是原图尺寸,会撑出卡片。
+		// 在图片标签上补内联样式,按容器宽度等比缩放并居中。
+		.replace(/(<img\b[^>]*?\sstyle=(["']))([^"']*)(\2)/gi, '$1$3max-width:100%;height:auto;display:block;margin:10px auto;$4')
+		.replace(/<img\b(?![^>]*\sstyle=)/gi, '<img style="max-width:100%;height:auto;display:block;margin:10px auto;"')
 }
 
 export const requestDynamicCode = (codeType: string, businessId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void) => {
diff --git a/talent-card-admin/src/style.css b/talent-card-admin/src/style.css
index 9148aaf..22bb0d4 100644
--- a/talent-card-admin/src/style.css
+++ b/talent-card-admin/src/style.css
@@ -3812,7 +3812,8 @@
 
 .movie-ticket-record-tools {
   display: flex;
-  justify-content: space-between;
+  /* 只剩"下载"一个按钮,靠右对齐(原来左边是"补录发放",该功能已下线)。 */
+  justify-content: flex-end;
   padding: 18px 0 16px;
 }
 
diff --git a/talent-card-admin/src/views/AppointmentManagementView.vue b/talent-card-admin/src/views/AppointmentManagementView.vue
index 9796bf3..abc9bd6 100644
--- a/talent-card-admin/src/views/AppointmentManagementView.vue
+++ b/talent-card-admin/src/views/AppointmentManagementView.vue
@@ -29,6 +29,8 @@
 function search() { pagination.page = 1; loadData() }
 function resetFilters() { Object.assign(filters, { applicationType: '', keyword: '', serviceKeyword: '', cardLevel: '', status: '', submittedTime: [] }); search() }
 function text(value: unknown) { return value === undefined || value === null || value === '' ? '-' : String(value) }
+// 后端返回 ISO 时间(2026-09-01T11:29:35),列表统一显示成 "2026-09-01 11:29:35"。
+function formatDateTime(value: unknown) { return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : '-' }
 async function handle(row: Record<string, any>, status = '已处理') { processingId.value = row.id; try { await updateServiceApplicationStatus(row.id, status, '管理员', status === '已驳回' ? '后台驳回' : ''); ElMessage.success('状态已更新'); await loadData() } finally { processingId.value = null } }
 async function download() { const blob = await exportServiceApplications(queryParams() as any); saveBlob(blob, '预约申请列表.xlsx') }
 onMounted(loadData)
@@ -56,7 +58,9 @@
         <el-table-column label="服务机构" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ text(row.serviceName || row.merchantName || row.hospitalName) }}</template></el-table-column>
         <el-table-column label="预约内容" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ text(row.appointmentContent || row.productName || row.packageName) }}</template></el-table-column>
         <el-table-column label="预约时间" min-width="150"><template #default="{ row }">{{ text(row.appointmentTime || row.expectedTime) }}</template></el-table-column>
-        <el-table-column prop="submittedAt" label="提交时间" min-width="170" />
+        <el-table-column prop="submittedAt" label="提交时间" min-width="170">
+          <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" min-width="110" />
         <el-table-column label="操作" width="170" fixed="right"><template #default="{ row }"><el-button link type="primary" :loading="processingId === row.id" @click="handle(row)">处理</el-button><el-button link type="danger" :loading="processingId === row.id" @click="handle(row, '已驳回')">驳回</el-button></template></el-table-column>
       </el-table>
diff --git a/talent-card-admin/src/views/CarWashBenefitView.vue b/talent-card-admin/src/views/CarWashBenefitView.vue
index 71b1650..86001e6 100644
--- a/talent-card-admin/src/views/CarWashBenefitView.vue
+++ b/talent-card-admin/src/views/CarWashBenefitView.vue
@@ -12,6 +12,8 @@
 const configDialogVisible = ref(false)
 const configForm = reactive({ merchantId: '', externalLinkType: 'miniProgram', externalMiniProgramAppId: '', externalMiniProgramPath: '', externalWebUrl: '', status: 1 })
 const statusOptions = [{ label: '待审核', value: 0 }, { label: '已通过', value: 1 }, { label: '已驳回', value: 2 }, { label: '已回滚', value: 3 }]
+// 后端返回 ISO 时间(2026-09-01T11:29:35),列表统一显示成 "2026-09-01 11:29:35"。
+function formatDateTime(value?: string) { return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : '' }
 async function loadData() { loading.value = true; try { if (activeTab.value === 'configs') { const result = await getCarWashJumpConfigs({ page: pagination.page, size: pagination.size, keyword: filters.keyword || undefined }); rows.value = result.records || []; pagination.total = result.total || 0 } else { const result = await getCarWashCredentials({ page: pagination.page, size: pagination.size, keyword: filters.keyword || undefined, status: filters.status, startDate: filters.dateRange?.[0], endDate: filters.dateRange?.[1] }); rows.value = result.records || []; pagination.total = result.total || 0 } } finally { loading.value = false } }
 function search() { pagination.page = 1; loadData() }
 function resetFilters() { Object.assign(filters, { keyword: '', status: undefined, dateRange: [] }); search() }
@@ -29,7 +31,7 @@
       <el-tabs v-model="activeTab" @tab-change="switchTab"><el-tab-pane label="跳转配置" name="configs" /><el-tab-pane label="凭证审核" name="credentials" /></el-tabs>
       <el-form class="filter-form" :model="filters" label-width="84px"><el-form-item label="关键词"><el-input v-model="filters.keyword" clearable placeholder="商家、地址、手机号、凭证编号" /></el-form-item><el-form-item v-if="activeTab === 'credentials'" label="状态"><el-select v-model="filters.status" clearable placeholder="全部"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item v-if="activeTab === 'credentials'" label="日期"><el-date-picker v-model="filters.dateRange" type="daterange" value-format="YYYY-MM-DD" /></el-form-item><div class="toolbar-actions"><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="resetFilters">重置</el-button></div></el-form>
       <el-table v-if="activeTab === 'configs'" v-loading="loading" :data="rows" border empty-text="暂无跳转配置"><el-table-column prop="merchantId" label="商户ID" width="100" /><el-table-column prop="merchantName" label="商户名称" min-width="160" /><el-table-column prop="externalLinkType" label="跳转类型" min-width="110" /><el-table-column prop="externalMiniProgramAppId" label="小程序AppId" min-width="160" /><el-table-column prop="externalMiniProgramPath" label="小程序路径" min-width="200" show-overflow-tooltip /><el-table-column prop="externalWebUrl" label="网页地址" min-width="200" show-overflow-tooltip /><el-table-column label="状态" width="90"><template #default="{ row }">{{ Number(row.serviceStatus ?? row.status) === 1 ? '启用' : '停用' }}</template></el-table-column><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openConfig(row)">配置</el-button></template></el-table-column></el-table>
-      <el-table v-else v-loading="loading" :data="rows" border empty-text="暂无洗车凭证"><el-table-column prop="credentialNo" label="凭证编号" min-width="130" /><el-table-column prop="talentName" label="持卡人" min-width="110" /><el-table-column prop="talentMobile" label="手机号" min-width="120" /><el-table-column prop="merchantName" label="商户" min-width="150" /><el-table-column prop="submitTime" label="提交时间" min-width="170" /><el-table-column prop="statusText" label="状态" min-width="110" /><el-table-column label="操作" width="210" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="audit(row, 1)">通过</el-button><el-button link type="danger" @click="audit(row, 2)">驳回</el-button><el-button link type="warning" @click="rollback(row)">回滚</el-button></template></el-table-column></el-table>
+      <el-table v-else v-loading="loading" :data="rows" border empty-text="暂无洗车凭证"><el-table-column prop="credentialNo" label="凭证编号" min-width="130" /><el-table-column prop="talentName" label="持卡人" min-width="110" /><el-table-column prop="talentMobile" label="手机号" min-width="120" /><el-table-column prop="merchantName" label="商户" min-width="150" /><el-table-column prop="submitTime" label="提交时间" min-width="170"><template #default="{ row }">{{ formatDateTime(row.submitTime) }}</template></el-table-column><el-table-column prop="statusText" label="状态" min-width="110" /><el-table-column label="操作" width="210" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="audit(row, 1)">通过</el-button><el-button link type="danger" @click="audit(row, 2)">驳回</el-button><el-button link type="warning" @click="rollback(row)">回滚</el-button></template></el-table-column></el-table>
       <div class="pagination-row"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /></div>
     </section>
     <el-dialog v-model="configDialogVisible" title="洗车跳转配置" width="620px"><el-form :model="configForm" label-width="130px"><el-form-item label="商户ID"><el-input v-model="configForm.merchantId" disabled /></el-form-item><el-form-item label="跳转类型"><el-select v-model="configForm.externalLinkType"><el-option label="第三方小程序" value="miniProgram" /><el-option label="网页" value="web" /><el-option label="不跳转" value="none" /></el-select></el-form-item><el-form-item label="小程序AppId"><el-input v-model="configForm.externalMiniProgramAppId" /></el-form-item><el-form-item label="小程序路径"><el-input v-model="configForm.externalMiniProgramPath" /></el-form-item><el-form-item label="网页地址"><el-input v-model="configForm.externalWebUrl" /></el-form-item><el-form-item label="状态"><el-switch v-model="configForm.status" :active-value="1" :inactive-value="0" /></el-form-item></el-form><template #footer><el-button @click="configDialogVisible = false">取消</el-button><el-button type="primary" @click="saveConfig">保存</el-button></template></el-dialog>
diff --git a/talent-card-admin/src/views/FuelServiceView.vue b/talent-card-admin/src/views/FuelServiceView.vue
index fdbdade..9faaf42 100644
--- a/talent-card-admin/src/views/FuelServiceView.vue
+++ b/talent-card-admin/src/views/FuelServiceView.vue
@@ -155,7 +155,9 @@
         <el-table-column prop="talentMobile" label="持卡人电话" min-width="118" align="center" />
         <el-table-column prop="talentLevel" label="人才等级" min-width="100" align="center" />
         <el-table-column prop="alipayMobile" label="支付宝绑定手机号" min-width="132" align="center" />
-        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center" />
+        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template>
+        </el-table-column>
         <el-table-column prop="submissionType" label="提交类型" min-width="108" align="center" />
         <el-table-column prop="status" label="状态" min-width="108" align="center" />
         <el-table-column label="操作" width="86" fixed="right" align="center">
diff --git a/talent-card-admin/src/views/HotelReservationView.vue b/talent-card-admin/src/views/HotelReservationView.vue
index d2de2ab..36a22fa 100644
--- a/talent-card-admin/src/views/HotelReservationView.vue
+++ b/talent-card-admin/src/views/HotelReservationView.vue
@@ -159,8 +159,12 @@
         <el-table-column prop="merchantName" label="酒店等级" min-width="96" align="center" />
         <el-table-column prop="productName" label="商品名称" min-width="96" align="center" />
         <el-table-column prop="guestCount" label="入住人数" min-width="90" align="center" />
-        <el-table-column prop="expectedTime" label="预约入住时间" min-width="132" align="center" />
-        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center" />
+        <el-table-column prop="expectedTime" label="预约入住时间" min-width="132" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.expectedTime) }}</template>
+        </el-table-column>
+        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" min-width="108" align="center" />
         <el-table-column label="操作" width="86" fixed="right" align="center">
           <template #default="{ row }">
diff --git a/talent-card-admin/src/views/HotelVerificationView.vue b/talent-card-admin/src/views/HotelVerificationView.vue
index ecd5e17..6e3b454 100644
--- a/talent-card-admin/src/views/HotelVerificationView.vue
+++ b/talent-card-admin/src/views/HotelVerificationView.vue
@@ -26,6 +26,11 @@
   total: 0,
 })
 
+// 后端返回的是 ISO 时间(2026-09-01T11:29:35),列表统一显示成 "2026-09-01 11:29:35"。
+function formatDateTime(value?: string) {
+  return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : ''
+}
+
 async function loadData() {
   loading.value = true
   try {
@@ -75,7 +80,7 @@
     { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile },
     { title: '人才等级', value: row => row.talentLevel }, { title: '酒店', value: row => row.merchantName },
     { title: '权益', value: row => row.benefitName }, { title: '核销金额(元)', value: row => Number(row.verifyAmount || 0) / 100 },
-    { title: '核销时间', value: row => row.verifyTime }, { title: '状态', value: row => row.status },
+    { title: '核销时间', value: row => formatDateTime(row.verifyTime) }, { title: '状态', value: row => row.status },
   ], result.records)
   ElMessage.success(`已导出 ${result.records.length} 条核销记录`)
 }
@@ -142,7 +147,9 @@
         <el-table-column prop="talentMobile" label="持卡人电话" min-width="140" align="center" />
         <el-table-column prop="talentLevel" label="人才等级" min-width="120" align="center" />
         <el-table-column prop="merchantName" label="商家名称" min-width="130" align="center" />
-        <el-table-column prop="verifyTime" label="核销时间" min-width="150" align="center" />
+        <el-table-column prop="verifyTime" label="核销时间" min-width="150" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.verifyTime) }}</template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" min-width="120" align="center" />
       </el-table>
 
diff --git a/talent-card-admin/src/views/MedicalApplicationView.vue b/talent-card-admin/src/views/MedicalApplicationView.vue
index 9633cc2..9ae7025 100644
--- a/talent-card-admin/src/views/MedicalApplicationView.vue
+++ b/talent-card-admin/src/views/MedicalApplicationView.vue
@@ -58,6 +58,11 @@
   loadData()
 }
 
+// 后端返回 ISO 时间(2026-09-01T11:29:35),列表统一显示成 "2026-09-01 11:29:35"。
+function formatDateTime(value?: string) {
+  return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : ''
+}
+
 function resetFilters() {
   filters.talentKeyword = ''
   filters.hospitalName = ''
@@ -141,8 +146,12 @@
         <el-table-column prop="hospitalName" label="医院名称" min-width="96" align="center" />
         <el-table-column prop="applicantName" label="就医人姓名" min-width="96" align="center" />
         <el-table-column prop="applicantMobile" label="手机号" min-width="112" align="center" />
-        <el-table-column prop="expectedTime" label="就医期望时间" min-width="118" align="center" />
-        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center" />
+        <el-table-column prop="expectedTime" label="就医期望时间" min-width="118" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.expectedTime) }}</template>
+        </el-table-column>
+        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" min-width="82" align="center" />
         <el-table-column label="操作" width="86" fixed="right" align="center">
           <template #default="{ row }">
diff --git a/talent-card-admin/src/views/MedicalReservationView.vue b/talent-card-admin/src/views/MedicalReservationView.vue
index c4b40d4..70953d8 100644
--- a/talent-card-admin/src/views/MedicalReservationView.vue
+++ b/talent-card-admin/src/views/MedicalReservationView.vue
@@ -146,7 +146,9 @@
         <el-table-column prop="packageName" label="套餐名称" min-width="92" align="center" />
         <el-table-column prop="applicantName" label="体检人姓名" min-width="96" align="center" />
         <el-table-column prop="applicantMobile" label="手机号" min-width="112" align="center" />
-        <el-table-column prop="expectedTime" label="体检期望时间" min-width="118" align="center" />
+        <el-table-column prop="expectedTime" label="体检期望时间" min-width="118" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.expectedTime) }}</template>
+        </el-table-column>
         <el-table-column label="提交时间" min-width="126" align="center">
           <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template>
         </el-table-column>
diff --git a/talent-card-admin/src/views/MerchantCreateView.vue b/talent-card-admin/src/views/MerchantCreateView.vue
index e4d820c..49575bf 100644
--- a/talent-card-admin/src/views/MerchantCreateView.vue
+++ b/talent-card-admin/src/views/MerchantCreateView.vue
@@ -1811,6 +1811,12 @@
   return Number(amount).toFixed(2).replace(/\.00$/, '')
 }
 
+// 销量是计数:0 也要显示成 0,不能因为 0 变空白让人以为没统计出来。
+function salesCountText(row: Product) {
+  const count = row.salesCount
+  return count === undefined || count === null ? 0 : count
+}
+
 function parseJsonValue<T>(value: unknown, fallback: T): T {
   if (!value) return fallback
   if (typeof value !== 'string') return value as T
@@ -3103,7 +3109,7 @@
               <template #default="{ row }">{{ productAmountText(row) }}</template>
             </el-table-column>
             <el-table-column v-if="!isHotelMerchant && !isHealthCheckMerchant" prop="salesCount" label="销量" min-width="90">
-              <template #default="{ row }">{{ row.salesCount || '' }}</template>
+              <template #default="{ row }">{{ salesCountText(row) }}</template>
             </el-table-column>
             <el-table-column label="修改时间" min-width="90">
               <template #default="{ row }">{{ formatDisplayTime(row.submitTime || row.createdAt) }}</template>
@@ -3561,7 +3567,9 @@
                 <el-table-column label="商品金额" min-width="98">
                   <template #default="{ row }">{{ productAmountText(row) }}</template>
                 </el-table-column>
-                <el-table-column prop="salesCount" label="销量" min-width="98" />
+                <el-table-column prop="salesCount" label="销量" min-width="98">
+                  <template #default="{ row }">{{ salesCountText(row) }}</template>
+                </el-table-column>
                 <el-table-column label="修改时间" min-width="98">
                   <template #default="{ row }">{{ formatDisplayTime(row.submitTime || row.createdAt) }}</template>
                 </el-table-column>
diff --git a/talent-card-admin/src/views/MovieTicketRecordView.vue b/talent-card-admin/src/views/MovieTicketRecordView.vue
index 2e35bd3..2e655b3 100644
--- a/talent-card-admin/src/views/MovieTicketRecordView.vue
+++ b/talent-card-admin/src/views/MovieTicketRecordView.vue
@@ -24,6 +24,11 @@
   total: 0,
 })
 
+// 后端返回 ISO 时间(2026-09-01T11:29:35),列表统一显示成 "2026-09-01 11:29:35"。
+function formatDateTime(value?: string) {
+  return typeof value === 'string' && value ? value.replace('T', ' ').slice(0, 19) : ''
+}
+
 async function loadData() {
   loading.value = true
   try {
@@ -118,7 +123,9 @@
         <el-table-column prop="talentName" label="持卡人名称" min-width="130" align="center" />
         <el-table-column prop="talentMobile" label="持卡人电话" min-width="130" align="center" />
         <el-table-column prop="talentLevel" label="人才等级" min-width="120" align="center" />
-        <el-table-column prop="receivedAt" label="领取时间" min-width="150" align="center" />
+        <el-table-column prop="receivedAt" label="领取时间" min-width="150" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.receivedAt) }}</template>
+        </el-table-column>
       </el-table>
 
       <div class="pagination-row">
diff --git a/talent-card-admin/src/views/MovieTicketView.vue b/talent-card-admin/src/views/MovieTicketView.vue
index 4053511..80abc9a 100644
--- a/talent-card-admin/src/views/MovieTicketView.vue
+++ b/talent-card-admin/src/views/MovieTicketView.vue
@@ -11,7 +11,6 @@
   getTicketPackageCodes,
   getTicketPackages,
   importTicketPackageCodes,
-  supplementTicketPackageClaim,
   updateTicketPackage,
   updateTicketPackageStatus,
   uploadAdminImage,
@@ -29,7 +28,6 @@
 const merchants = ref<Record<string, any>[]>([])
 const codeDialogVisible = ref(false)
 const generateDialogVisible = ref(false)
-const supplementVisible = ref(false)
 const merchantDialogVisible = ref(false)
 const editingId = ref<number | string | null>(null)
 const activePackage = ref<Record<string, any> | null>(null)
@@ -90,7 +88,6 @@
 
 const codeForm = reactive({ batchNo: '', codesText: '', remark: '' })
 const generateForm = reactive({ batchNo: '', codePrefix: 'EX', codeCount: 100, remark: '' })
-const supplementForm = reactive({ packageId: '', codeId: '', exchangeCode: '', talentId: '', cardId: '', remark: '' })
 
 const title = computed(() => {
   if (mode.value === 'claims') return '票包领取记录'
@@ -388,25 +385,6 @@
   await loadData()
 }
 
-function openSupplement() {
-  Object.assign(supplementForm, { packageId: rowId(selectedPackage.value || {}) || '', codeId: '', exchangeCode: '', talentId: '', cardId: '', remark: '' })
-  supplementVisible.value = true
-}
-
-async function supplement() {
-  await supplementTicketPackageClaim({
-    packageId: Number(supplementForm.packageId),
-    codeId: supplementForm.codeId ? Number(supplementForm.codeId) : undefined,
-    exchangeCode: supplementForm.exchangeCode || undefined,
-    talentId: Number(supplementForm.talentId),
-    cardId: supplementForm.cardId ? Number(supplementForm.cardId) : undefined,
-    remark: supplementForm.remark,
-  })
-  ElMessage.success('补录成功')
-  supplementVisible.value = false
-  await loadData()
-}
-
 async function download() {
   if (mode.value === 'claims') {
     const result = await getTicketPackageClaims({ ...claimParams(), page: 1, size: 1000 })
@@ -517,7 +495,6 @@
         </el-form>
 
         <div class="movie-ticket-record-tools">
-          <el-button type="primary" @click="openSupplement">补录发放</el-button>
           <el-button v-permission="'admin.movie-tickets.export'" :icon="Download" @click="download">下载</el-button>
         </div>
 
@@ -665,16 +642,5 @@
       </template>
     </el-dialog>
 
-    <el-dialog v-model="supplementVisible" title="补录发放记录" width="560px">
-      <el-form :model="supplementForm" label-width="100px">
-        <el-form-item label="票包ID"><el-input v-model="supplementForm.packageId" disabled /></el-form-item>
-        <el-form-item label="兑换码ID"><el-input v-model="supplementForm.codeId" /></el-form-item>
-        <el-form-item label="兑换码值"><el-input v-model="supplementForm.exchangeCode" /></el-form-item>
-        <el-form-item label="持卡人ID"><el-input v-model="supplementForm.talentId" /></el-form-item>
-        <el-form-item label="人才卡ID"><el-input v-model="supplementForm.cardId" /></el-form-item>
-        <el-form-item label="备注"><el-input v-model="supplementForm.remark" /></el-form-item>
-      </el-form>
-      <template #footer><el-button @click="supplementVisible = false">取消</el-button><el-button type="primary" @click="supplement">补录</el-button></template>
-    </el-dialog>
   </div>
 </template>
diff --git a/talent-card-admin/src/views/ProductView.vue b/talent-card-admin/src/views/ProductView.vue
index 747590a..4c23a90 100644
--- a/talent-card-admin/src/views/ProductView.vue
+++ b/talent-card-admin/src/views/ProductView.vue
@@ -62,6 +62,12 @@
   return Number(amount).toFixed(2).replace(/\.00$/, '')
 }
 
+// 销量是计数:0 也要显示成 0,不能因为 0 变空白让人以为没统计出来。
+function salesText(row: Product) {
+  const count = row.salesCount
+  return count === undefined || count === null ? 0 : count
+}
+
 function formatDateTime(value?: string) {
   return value ? value.replace('T', ' ') : ''
 }
@@ -200,7 +206,7 @@
           <template #default="{ row }">{{ amountText(row) }}</template>
         </el-table-column>
         <el-table-column prop="salesCount" label="销量" width="110" align="center">
-          <template #default="{ row }">{{ row.salesCount || '' }}</template>
+          <template #default="{ row }">{{ salesText(row) }}</template>
         </el-table-column>
         <el-table-column label="提交修改时间" width="145" align="center">
           <template #default="{ row }">{{ formatDateTime(row.submitTime || row.createdAt) }}</template>
diff --git a/talent-card-admin/src/views/SpouseEmploymentView.vue b/talent-card-admin/src/views/SpouseEmploymentView.vue
index 8bf4d62..418fe6a 100644
--- a/talent-card-admin/src/views/SpouseEmploymentView.vue
+++ b/talent-card-admin/src/views/SpouseEmploymentView.vue
@@ -142,7 +142,9 @@
         <el-table-column prop="talentLevel" label="人才等级" min-width="96" align="center" />
         <el-table-column prop="spouseName" label="配偶姓名" min-width="96" align="center" />
         <el-table-column prop="spouseMobile" label="配偶手机号" min-width="116" align="center" />
-        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center" />
+        <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center">
+          <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" min-width="120" align="center" />
         <el-table-column label="操作" width="86" fixed="right" align="center">
           <template #default="{ row }">
diff --git a/talent-card-admin/src/views/StaffManagementView.vue b/talent-card-admin/src/views/StaffManagementView.vue
index 737f801..9384107 100644
--- a/talent-card-admin/src/views/StaffManagementView.vue
+++ b/talent-card-admin/src/views/StaffManagementView.vue
@@ -105,6 +105,18 @@
 );
 const roleName = (code: string) =>
   roles.value.find((r) => r.roleCode === code)?.roleName || code;
+// 停用角色不能分配给员工,但员工列表仍要能显示出它的名称;
+// 之前只保留启用角色,导致停用角色的员工显示成 role_1789541215146 这种代码。
+const enabledRoles = computed(() => roles.value.filter((r) => r.status === 1));
+const formRoleOptions = computed(() => {
+  const options = enabledRoles.value.slice();
+  const current = form.roleCode;
+  if (current && !options.some((r) => r.roleCode === current)) {
+    const matched = roles.value.find((r) => r.roleCode === current);
+    if (matched) options.push(matched); // 编辑时保留原角色,避免下拉框显示成代码
+  }
+  return options;
+});
 const formatDateTime = (value?: string) =>
   value ? value.replace("T", " ").slice(0, 19) : "";
 const isDefaultAdmin = (row: AdminStaff) => row.account === "admin";
@@ -130,7 +142,7 @@
   try {
     const result = await Promise.all([getAdminStaff(), getAdminRoles()]);
     rows.value = result[0];
-    roles.value = result[1].filter((r) => r.status === 1);
+    roles.value = result[1];
   } finally {
     loading.value = false;
   }
@@ -148,7 +160,7 @@
     account: "",
     password: "",
     confirmPassword: "",
-    roleCode: roles.value[0]?.roleCode || "",
+    roleCode: enabledRoles.value[0]?.roleCode || "",
     positionName: "",
     customerWechat: "",
     status: 1,
@@ -367,7 +379,7 @@
         ><el-form-item label="角色" prop="roleCode" required
           ><el-select v-model="form.roleCode" class="full"
             ><el-option
-              v-for="role in roles"
+              v-for="role in formRoleOptions"
               :key="role.id"
               :label="role.roleName"
               :value="role.roleCode" /></el-select></el-form-item
diff --git a/talent-card-admin/src/views/TalentListView.vue b/talent-card-admin/src/views/TalentListView.vue
index 5529d60..fce08b8 100644
--- a/talent-card-admin/src/views/TalentListView.vue
+++ b/talent-card-admin/src/views/TalentListView.vue
@@ -290,7 +290,7 @@
       <div class="talent-import-dialog">
         <div class="talent-import-guide">
           <strong>导入说明</strong>
-          <span>模板中的 11 项均为必填;服务专员须填写已启用的服务专员姓名。单次最多导入 1000 条,仅支持 .xlsx 文件。</span>
+          <span>模板中的 11 项均为必填;服务专员须填写「系统管理 - 员工管理」里已启用员工的姓名。单次最多导入 1000 条,仅支持 .xlsx 文件。</span>
           <el-button link type="primary" :icon="Download" @click="downloadImportTemplate">下载导入模板</el-button>
         </div>
 
diff --git a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
index 90148ed..cdcb287 100644
--- a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
+++ b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
@@ -42,7 +42,12 @@
 
 async function approve() {
   if (!detail.value) return
-  await ElMessageBox.confirm('确认通过该升级申请?通过后将更新持卡人等级。', '审核通过', { type: 'warning' })
+  const target = detail.value.targetLevel ? `「${detail.value.targetLevel}」` : '申请等级'
+  await ElMessageBox.confirm(
+    `确认通过该升级申请?系统不会自动修改持卡人等级,通过后请到「持卡人管理」手动把该持卡人等级改为${target}。`,
+    '审核通过',
+    { type: 'warning' },
+  )
   await process('已通过')
 }
 
@@ -57,7 +62,11 @@
   processing.value = true
   try {
     detail.value = await auditUpgradeApplication(detail.value.id, status, remark)
-    ElMessage.success(status === '已通过' ? '升级申请已通过' : '升级申请已驳回')
+    ElMessage.success(
+      status === '已通过'
+        ? '升级申请已通过,请到「持卡人管理」手动修改该持卡人的等级'
+        : '升级申请已驳回',
+    )
   } finally {
     processing.value = false
   }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
index f806203..db80901 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -180,8 +180,8 @@
 		if (status != null) { sql.append(" and status = ?"); countSql.append(" and status = ?"); args.add(status); }
 		if (publishStart != null && !publishStart.isBlank()) { sql.append(" and date(publish_time) >= ?"); countSql.append(" and date(publish_time) >= ?"); args.add(publishStart); }
 		if (publishEnd != null && !publishEnd.isBlank()) { sql.append(" and date(publish_time) <= ?"); countSql.append(" and date(publish_time) <= ?"); args.add(publishEnd); }
-		// 置顶优先,置顶组内按置顶时间倒序(越后置顶越靠前);非置顶按排序值倒序。
-		sql.append(" order by is_top desc, case when is_top = 1 then top_time end desc, sort_no desc, publish_time desc, id desc");
+		// 置顶优先;置顶组内按“修改时间”倒序(最新修改的排前面,和人才端人才资讯一致),非置顶按排序值倒序。
+		sql.append(" order by is_top desc, case when is_top = 1 then updated_at end desc, sort_no desc, publish_time desc, id desc");
 		return page(sql.toString(), countSql.toString(), page, size, args.toArray());
 	}
 
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/card/CardAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/business/card/CardAdminService.java
index f742691..2955c6c 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/card/CardAdminService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/card/CardAdminService.java
@@ -206,16 +206,8 @@
 		if (!"待审核".equals(application.get("status"))) {
 			throw new BusinessException(ErrorCode.CONFLICT, "该申请已审核");
 		}
-		if ("已通过".equals(status) && application.get("talentId") != null) {
-			jdbcTemplate.update("update talent_user set talent_level = ? where deleted = 0 and id = ?",
-					application.get("targetLevel"),
-					application.get("talentId"));
-			jdbcTemplate.update("""
-					update talent_card
-					set card_level = ?
-					where deleted = 0 and talent_id = ? and card_type = 'PERSONAL'
-					""", application.get("targetLevel"), application.get("talentId"));
-		}
+		// 通过升级申请只改申请单状态:持卡人等级由运营在「持卡人管理」里手动修改
+		//(申请等级只作参考,实际等级以后台操作为准),所以这里不再自动改 talent_level / card_level。
 		jdbcTemplate.update("""
 				update upgrade_application
 				set status = ?, audit_time = now(), auditor_name = ?, audit_remark = ?
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalService.java
index 8b83889..416dfad 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalService.java
@@ -36,7 +36,9 @@
         result.put("heroImage", merchantHeroImage(serviceId,
                 text(merchant.get("coverUrl"), text(benefit.get("heroImage"), ""))));
         result.put("heroImages", merchantHeroImages(serviceId));
-        result.put("rightsText", text(matchedRight.get("detail"), text(benefit.get("rightsText"), "暂无使用说明")));
+        // 使用说明只认商家权益里配置的内容:商家没配(或该人才等级没配)就返回空,
+        // 小程序显示"暂无使用说明",不再回落到服务权益里的历史默认文案。
+        result.put("rightsText", text(matchedRight.get("detail"), ""));
         result.put("available", available);
         result.put("unavailableReason", available ? "" : text(benefit.get("unavailableReason"), "该权益暂未开放"));
         return result;
@@ -45,25 +47,9 @@
     @Transactional(readOnly = true)
     public List<Map<String, Object>> schools(Long serviceId) {
         Map<String, Object> merchant = approvalService(serviceId);
-        List<Map<String, Object>> configuredSchools = configuredSchools(merchant.get("usageInstructions"));
-        if (!configuredSchools.isEmpty()) return configuredSchools;
-        Map<String, Object> benefit = benefit();
-        return query("""
-                select id, school_name, address, latitude, longitude, contact_mobile, detail_available
-                from children_entrustment_school
-                where benefit_id = ? and enabled = 1 and deleted = 0
-                order by sort_no asc, id asc
-                """, benefit.get("id")).stream().map(row -> {
-            Map<String, Object> school = new LinkedHashMap<>();
-            school.put("id", row.get("id"));
-            school.put("name", row.get("schoolName"));
-            school.put("address", text(row.get("address"), "暂未配置学校地址"));
-            school.put("latitude", row.get("latitude"));
-            school.put("longitude", row.get("longitude"));
-            school.put("phone", text(row.get("contactMobile"), ""));
-            school.put("detailAvailable", number(row.get("detailAvailable")) == 1);
-            return school;
-                }).toList();
+        // 只返回商家自己配置的适用学校;没配就是空列表(小程序显示"暂无适用学校"),
+        // 不再回落到历史全局学校表,避免新添加的商家显示别人的学校。
+        return configuredSchools(merchant.get("usageInstructions"));
     }
 
     private List<String> merchantHeroImages(Long merchantId) {
@@ -140,11 +126,8 @@
             result.put("content", matched.get("content"));
             return result;
         }
-        try {
-            return school(Long.parseLong(schoolId));
-        } catch (NumberFormatException exception) {
-            throw new BusinessException(ErrorCode.NOT_FOUND, "学校不存在或暂未开放");
-        }
+        // 商家没配学校时不去查历史全局学校表,直接按"学校不存在"返回。
+        throw new BusinessException(ErrorCode.NOT_FOUND, "学校不存在或暂未开放");
     }
 
     @Transactional(readOnly = true)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
index 8f48e79..2adc926 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -118,7 +118,12 @@
 
 	@Transactional
 	public Map<String, Object> newsDetail(Long id) {
-		int updated = jdbcTemplate.update("update news_article set view_count = view_count + 1 where id = ? and deleted = 0 and status = 1", id);
+		// 浏览计数不能顺手刷新 updated_at:置顶组是按“修改时间”倒序排的,
+		// 否则哪篇被点得多,哪篇就被顶到置顶组最前面,看起来像没按修改时间排。
+		int updated = jdbcTemplate.update("""
+				update news_article set view_count = view_count + 1, updated_at = updated_at
+				where id = ? and deleted = 0 and status = 1
+				""", id);
 		if (updated == 0) {
 			throw new BusinessException(ErrorCode.NOT_FOUND, "资讯不存在");
 		}
@@ -148,7 +153,11 @@
 
 	@Transactional
 	public Map<String, Object> policy(Long id) {
-		int updated = jdbcTemplate.update("update policy_article set view_count = view_count + 1 where id = ? and deleted = 0 and status = 1", id);
+		// 同上:浏览政策详情不应该改“修改时间”(后台详情页会展示修改人/修改时间)。
+		int updated = jdbcTemplate.update("""
+				update policy_article set view_count = view_count + 1, updated_at = updated_at
+				where id = ? and deleted = 0 and status = 1
+				""", id);
 		if (updated == 0) {
 			throw new BusinessException(ErrorCode.NOT_FOUND, "政策已下架或不存在");
 		}
@@ -902,7 +911,7 @@
 		String serviceWhere = " where m.deleted = 0 and m.merchant_type_code = ?";
 		PageResult<Map<String, Object>> result = page("""
 				select m.id, m.merchant_name, m.merchant_type_code as merchant_type, m.contact_mobile, m.cover_url, m.address,
-				       m.service_label, m.status, m.sort_no, m.longitude, m.latitude,
+				       m.service_label, m.merchant_rights_config, m.status, m.sort_no, m.longitude, m.latitude,
 				       mapping.sort_no as topic_sort_no
 				from merchant m
 				left join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ?
@@ -914,16 +923,31 @@
 			item.put("businessType", merchantType.premiumBusinessType());
 			item.put("targetAction", merchantType.premiumBusinessType());
 			boolean tagVisible = !TAGLESS_MERCHANT_TYPES.contains(merchantType.code());
-			String benefitText = tagVisible ? textOrDefault(row.get("serviceLabel"), "人才专享") : "";
+			// 标签只用后台配的:先取商家权益里第一个启用等级的标签,再退回商家服务标签;
+			// 都没配就留空(后台标签是必填项,不再兜底"人才专享")。
+			String benefitText = tagVisible ? configuredMerchantTag(row.get("merchantRightsConfig"), row.get("serviceLabel")) : "";
+			// 没有标签文本时把 visible 置 false,小程序就不会渲染空标签块。
+			boolean tagPresent = tagVisible && !benefitText.isBlank();
 			item.put("benefitText", benefitText);
 			item.put("discountText", benefitText);
-			item.put("benefitTags", tagVisible ? List.of(benefitText) : List.of());
-			item.put("benefitTagVisible", tagVisible);
+			item.put("benefitTags", tagPresent ? List.of(benefitText) : List.of());
+			item.put("benefitTagVisible", tagPresent);
+			item.remove("merchantRightsConfig");
 			return item;
 		}).toList();
 		return new PageResult<>(records, result.page(), result.size(), result.total());
 	}
 
+	/** 列表标签只用后台配置:商家权益里第一个启用等级的标签 → 商家服务标签 → 空(不兜底默认文案)。 */
+	String configuredMerchantTag(Object rightsConfig, Object serviceLabel) {
+		for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) {
+			if (!booleanValue(right.get("enabled"))) continue;
+			String tag = textOrDefault(right.get("tag"), "");
+			if (!tag.isBlank()) return tag;
+		}
+		return textOrDefault(serviceLabel, "");
+	}
+
 	private Map<String, Object> enrichMerchantSearchRow(Map<String, Object> row) {
 		Map<String, Object> result = new LinkedHashMap<>(row);
 		String merchantType = String.valueOf(result.getOrDefault("merchantType", ""));
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/talent/TalentService.java b/talent-card-server/src/main/java/com/talentcard/server/business/talent/TalentService.java
index 586646e..ba96b94 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/talent/TalentService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/talent/TalentService.java
@@ -53,6 +53,15 @@
 		if (cardCount != null && cardCount > 0) {
 			throw new BusinessException(ErrorCode.CONFLICT, "人才卡号已存在");
 		}
+		String mobile = blankToNull(request.mobile());
+		// 一个手机号只能存在一个账号:新建持卡人前先查重(编辑手机号走 updateMobile,同样有查重)。
+		if (mobile != null) {
+			Integer mobileCount = jdbcTemplate.queryForObject(
+					"select count(*) from talent_user where deleted = 0 and mobile = ?", Integer.class, mobile);
+			if (mobileCount != null && mobileCount > 0) {
+				throw new BusinessException(ErrorCode.CONFLICT, "该手机号已存在账号,一个手机号只能存在一个账号");
+			}
+		}
 		jdbcTemplate.update("""
 				insert into talent_user(openid, name, mobile, id_card_no, gender, work_unit, talent_level,
 				                        source_area_type, work_address, contact_address)
@@ -60,7 +69,7 @@
 				""",
 				blankToNull(request.openid()),
 				request.name(),
-				blankToNull(request.mobile()),
+				mobile,
 				blankToNull(request.idCardNo()),
 				request.gender(),
 				blankToNull(request.workUnit()),
@@ -386,7 +395,7 @@
 	public Map<String, Object> importExcel(InputStream inputStream) {
 		List<Map<String, Object>> failures = new ArrayList<>();
 		List<TalentCreateRequest> validRows = new ArrayList<>();
-		List<Long> advisorIds = new ArrayList<>();
+		List<Long> staffIds = new ArrayList<>();
 		Set<String> fileMobiles = new HashSet<>();
 		Set<String> fileIdCards = new HashSet<>();
 		Set<String> fileCardNos = new HashSet<>();
@@ -424,15 +433,16 @@
 					failures.add(importFailure(rowIndex + 1, name, error));
 					continue;
 				}
-				Long advisorId = advisorIdByName(advisorName);
-				if (advisorId == null) {
-					failures.add(importFailure(rowIndex + 1, name, advisorName.isBlank() ? "服务专员不能为空" : "服务专员不存在、已停用或名称不唯一"));
+				Long staffId = serviceStaffIdByName(advisorName);
+				if (staffId == null) {
+					failures.add(importFailure(rowIndex + 1, name,
+							advisorName.isBlank() ? "服务专员不能为空" : "服务专员不存在、已停用或在员工管理中姓名不唯一"));
 					continue;
 				}
 				fileMobiles.add(mobile);
 				fileIdCards.add(idCardNo);
 				fileCardNos.add(talentCardNo);
-				advisorIds.add(advisorId);
+				staffIds.add(staffId);
 				validRows.add(new TalentCreateRequest(null, name, mobile, idCardNo, genderValue(gender), workUnit, level, talentCardNo,
 						"外地".equals(area) ? "OUTSIDE" : "LOCAL", workAddress, contactAddress));
 			}
@@ -447,7 +457,8 @@
 
 		for (int index = 0; index < validRows.size(); index++) {
 			Map<String, Object> created = create(validRows.get(index));
-			updateAdvisor(((Number) created.get("id")).longValue(), advisorIds.get(index));
+			// 服务专员写进专属服务人员字段(和「持卡人管理」里选服务人员是同一个字段)。
+			updateServiceStaff(((Number) created.get("id")).longValue(), staffIds.get(index));
 		}
 		Map<String, Object> result = new LinkedHashMap<>();
 		result.put("successCount", validRows.size());
@@ -557,11 +568,18 @@
 		return null;
 	}
 
-	private Long advisorIdByName(String advisorName) {
-		if (advisorName == null || advisorName.isBlank()) return null;
-		List<Long> ids = jdbcTemplate.queryForList(
-				"select id from talent_card_advisor where status = 1 and advisor_name = ?",
-				Long.class, advisorName.trim());
+	/**
+	 * 导入时"服务专员"填的是员工姓名,对应「系统管理 → 员工管理」里的员工(admin_operator)。
+	 *
+	 * 以前查的是老的 talent_card_advisor 顾问表,员工管理里新建的人根本查不到,
+	 * 于是导入一直报"服务专员不存在"。这里改成按员工姓名查(未删除、已启用、且姓名唯一)。
+	 */
+	Long serviceStaffIdByName(String staffName) {
+		if (staffName == null || staffName.isBlank()) return null;
+		List<Long> ids = jdbcTemplate.queryForList("""
+				select id from admin_operator
+				where deleted = 0 and status = 1 and display_name = ?
+				""", Long.class, staffName.trim());
 		return ids.size() == 1 ? ids.get(0) : null;
 	}
 
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalServiceTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalServiceTests.java
index a9622c6..4daaa36 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalServiceTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/portal/ChildrenEntrustmentPortalServiceTests.java
@@ -39,4 +39,16 @@
 				.containsEntry("address", "学校地址")
 				.containsEntry("content", "<p>后台配置的学校详情</p>");
 	}
+
+	@Test
+	void unconfiguredMerchantHasNoSchools() {
+		Long serviceId = 91L;
+		// 新添加、还没配学校的商家:不能回落到历史全局学校表(否则会显示别人的学校)。
+		when(jdbc.queryForList(anyString(), eq(serviceId))).thenReturn(List.of(Map.of(
+				"id", serviceId,
+				"merchant_type_code", "children",
+				"usage_instructions", "")));
+
+		assertThat(service.schools(serviceId)).isEmpty();
+	}
 }
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java
index 06e3478..8e5c8e5 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java
@@ -4,6 +4,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.lang.reflect.Method;
@@ -14,6 +15,7 @@
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.ArgumentCaptor;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.jdbc.core.JdbcTemplate;
 
@@ -191,4 +193,31 @@
 		assertThat(method.invoke(service, plain, "卓越人才卡", "LOCAL", true)).isNotNull();
 		assertThat(method.invoke(service, plain, "卓越人才卡", "OUTSIDE", true)).isNotNull();
 	}
+
+	@Test
+	void newsListSortsPinnedArticlesByModifiedTime() {
+		when(jdbc.queryForObject(anyString(), eq(Long.class), any(Object[].class))).thenReturn(0L);
+		when(jdbc.queryForList(anyString(), any(Object[].class))).thenReturn(List.of());
+
+		service.news(1, 10);
+
+		ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
+		verify(jdbc).queryForList(sql.capture(), any(Object[].class));
+		assertThat(sql.getValue())
+				.contains("from news_article")
+				.contains("is_top desc")
+				.contains("case when is_top = 1 then updated_at end desc");
+	}
+
+	@Test
+	void premiumListTagComesFromMerchantConfigurationOnly() {
+		// 商家权益里配了标签 → 用它;没配(新商家)→ 留空,不再兜底"人才专享"。
+		assertThat(service.configuredMerchantTag(
+				"[{\"level\":\"卓越人才卡\",\"enabled\":true,\"tag\":\"卓越人才卡\"}]", ""))
+				.isEqualTo("卓越人才卡");
+		assertThat(service.configuredMerchantTag("", "")).isEmpty();
+		assertThat(service.configuredMerchantTag("[]", "")).isEmpty();
+		// 权益里没写标签时退回商家服务标签。
+		assertThat(service.configuredMerchantTag("[{\"level\":\"卓越人才卡\",\"enabled\":true}]", "专属服务")).isEqualTo("专属服务");
+	}
 }
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/talent/TalentServiceTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/talent/TalentServiceTests.java
new file mode 100644
index 0000000..335fe7c
--- /dev/null
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/talent/TalentServiceTests.java
@@ -0,0 +1,55 @@
+package com.talentcard.server.business.talent;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import com.talentcard.server.common.BusinessException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.List;
+
+@ExtendWith(MockitoExtension.class)
+class TalentServiceTests {
+
+	@Mock
+	private JdbcTemplate jdbcTemplate;
+
+	@InjectMocks
+	private TalentService service;
+
+	private TalentCreateRequest request(String mobile) {
+		return new TalentCreateRequest(null, "测试持卡人", mobile, "51010119900101001X", (byte) 1, "测试单位",
+				"卓越人才卡", "TC-TEST-0001", "LOCAL", "工作地址", "联系地址");
+	}
+
+	@Test
+	void createRejectsMobileThatAlreadyHasAnAccount() {
+		// 第一次查人才卡号(不存在),第二次查手机号(已存在 1 条)——一个手机号只能存在一个账号。
+		when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), any(Object[].class))).thenReturn(0, 1);
+
+		assertThatThrownBy(() -> service.create(request("18782105413")))
+				.isInstanceOf(BusinessException.class)
+				.hasMessageContaining("一个手机号只能存在一个账号");
+	}
+
+	@Test
+	void importLooksUpServiceStaffInStaffManagement() {
+		// 导入 Excel 里的"服务专员"是「系统管理-员工管理」里的员工姓名(admin_operator),
+		// 不是老的顾问表 talent_card_advisor。
+		when(jdbcTemplate.queryForList(anyString(), eq(Long.class), eq("7"))).thenReturn(List.of(11L));
+		when(jdbcTemplate.queryForList(anyString(), eq(Long.class), eq("查不到的人"))).thenReturn(List.of());
+
+		assertThat(service.serviceStaffIdByName("7")).isEqualTo(11L);
+		assertThat(service.serviceStaffIdByName("查不到的人")).isNull();
+		assertThat(service.serviceStaffIdByName("  ")).isNull();
+	}
+}

--
Gitblit v1.7.1