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