From 69138ddcf4936098cb3b67688412badfc8f503a0 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期二, 18 八月 2026 17:13:53 +0800
Subject: [PATCH] 又又又更新

---
 talent-card-admin/src/views/OrderView.vue                                                                 |    4 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java      |    1 
 talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java                   |   71 ++-
 talent-card-admin/src/views/BusinessManagementView.vue                                                    |   10 
 talent-card-admin/src/views/TalentNewsPublishView.vue                                                     |   18 
 rencaika/static/card-assets/card-outstanding.jpg                                                          |    0 
 rencaika/static/benefits-assets/food.png                                                                  |    0 
 talent-card-admin/src/router/index.ts                                                                     |    2 
 rencaika_merchant/services/orders.uts                                                                     |   61 +-
 talent-card-admin/src/views/TalentPolicyPublishView.vue                                                   |   10 
 rencaika/static/home-assets/card.png                                                                      |    0 
 rencaika/static/home-assets/login-mascot.png                                                              |    0 
 rencaika/static/policy-assets/policy-explanation.jpg                                                      |    0 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java        |    5 
 talent-card-admin/src/views/RefundRecordView.vue                                                          |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java          |    2 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java |   11 
 talent-card-server/src/main/resources/application.yml                                                     |    6 
 talent-card-server/src/main/resources/db/migration/V113__ticket_package_level_rules.sql                   |    0 
 rencaika/static/entertainment-assets/entertainment-hero.webp                                              |    0 
 rencaika/static/home-assets/top.jpg                                                                       |    0 
 talent-card-admin/src/api/admin.ts                                                                        |    9 
 talent-card-admin/src/views/TalentNewsEditView.vue                                                        |   18 
 rencaika/static/benefits-assets/premium.png                                                               |    0 
 rencaika/static/card-assets/talent-card-background.jpg                                                    |    0 
 talent-card-admin/src/style.css                                                                           |  293 ++++++++++---
 talent-card-admin/src/layouts/AdminLayout.vue                                                             |  107 +++--
 talent-card-admin/src/views/TalentDetailView.vue                                                          |    2 
 rencaika_merchant/pages/verification-confirm/verification-confirm.uvue                                    |   13 
 talent-card-admin/src/views/StaffManagementView.vue                                                       |   48 +
 talent-card-admin/src/views/TalentCreateView.vue                                                          |   22 
 rencaika/static/card-assets/card-elite.jpg                                                                |    0 
 rencaika/static/product-assets/payment-success.png                                                        |    0 
 rencaika/static/card-assets/card-excellence.jpg                                                           |    0 
 rencaika/static/entertainment-assets/wanda-cinema.webp                                                    |    0 
 talent-card-admin/src/views/ProductAuditDetailView.vue                                                    |    6 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java         |    7 
 talent-card-admin/src/views/MerchantView.vue                                                              |    6 
 rencaika/static/benefits-assets/shopping.png                                                              |    0 
 rencaika/static/stay-assets/hotel-hampton.jpg                                                             |    0 
 talent-card-admin/src/views/RoleFormView.vue                                                              |   80 +++
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java         |    4 
 rencaika/manifest.json                                                                                    |    5 
 rencaika/static/benefits-assets/benefits-hero.jpg                                                         |    0 
 talent-card-server/src/main/resources/db/migration/V114__sync_premium_topic_merchant_mappings.sql         |    0 
 rencaika/static/benefits-assets/travel.png                                                                |    0 
 talent-card-admin/src/views/OrderDetailView.vue                                                           |    5 
 rencaika/static/benefits-assets/all.png                                                                   |    0 
 rencaika/static/benefits-assets/culture.png                                                               |    0 
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java    |    6 
 talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java         |    5 
 talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java           |  100 ++++
 rencaika/static/policy-assets/policy-hero.jpg                                                             |    0 
 talent-card-server/src/main/resources/db/migration/V112__news_article_top_time_sorting.sql                |    0 
 talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql            |    9 
 rencaika/static/stay-assets/hotel-vienna.jpg                                                              |    0 
 talent-card-admin/src/views/UpgradeApplicationDetailView.vue                                              |    8 
 rencaika/static/benefits-assets/life.png                                                                  |    0 
 /dev/null                                                                                                 |    1 
 talent-card-admin/src/views/MerchantCreateView.vue                                                        |  152 +++++--
 talent-card-admin/src/views/MerchantOnboardingDetailView.vue                                              |    4 
 rencaika/static/food-assets/food-2.jpg                                                                    |    0 
 talent-card-admin/src/views/TalentPolicyEditView.vue                                                      |    2 
 rencaika/static/food-assets/food-1.jpg                                                                    |    0 
 rencaika/static/news-assets/meeting-thumb.jpg                                                             |    0 
 rencaika/static/benefits-assets/stay.png                                                                  |    0 
 talent-card-admin/src/views/MerchantSettlementView.vue                                                    |   28 +
 67 files changed, 786 insertions(+), 357 deletions(-)

diff --git a/rencaika/manifest.json b/rencaika/manifest.json
index 4ea8f20..959a7c4 100644
--- a/rencaika/manifest.json
+++ b/rencaika/manifest.json
@@ -11,7 +11,8 @@
 	"mp-weixin": {
 		"appid": "wxcfee477acb54d179",
 		"setting": {
-			"urlCheck": false
+			"urlCheck": false,
+			"sourceMap": false
 		},
 		"usingComponents": true
 	},
@@ -66,4 +67,4 @@
 			"mode": ""
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/rencaika/static/benefits-assets/all.png b/rencaika/static/benefits-assets/all.png
index 4f8af36..5076619 100644
--- a/rencaika/static/benefits-assets/all.png
+++ b/rencaika/static/benefits-assets/all.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/benefits-hero.jpg b/rencaika/static/benefits-assets/benefits-hero.jpg
index 3c8a191..2d3fc66 100644
--- a/rencaika/static/benefits-assets/benefits-hero.jpg
+++ b/rencaika/static/benefits-assets/benefits-hero.jpg
Binary files differ
diff --git a/rencaika/static/benefits-assets/culture.png b/rencaika/static/benefits-assets/culture.png
index 8758e84..2e0bf3f 100644
--- a/rencaika/static/benefits-assets/culture.png
+++ b/rencaika/static/benefits-assets/culture.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/food.png b/rencaika/static/benefits-assets/food.png
index 68238e0..9af31a6 100644
--- a/rencaika/static/benefits-assets/food.png
+++ b/rencaika/static/benefits-assets/food.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/life.png b/rencaika/static/benefits-assets/life.png
index a0efa51..2a7057b 100644
--- a/rencaika/static/benefits-assets/life.png
+++ b/rencaika/static/benefits-assets/life.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/premium.png b/rencaika/static/benefits-assets/premium.png
index 357711d..6a48d3e 100644
--- a/rencaika/static/benefits-assets/premium.png
+++ b/rencaika/static/benefits-assets/premium.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/shopping.png b/rencaika/static/benefits-assets/shopping.png
index 7e6db88..dad8b5f 100644
--- a/rencaika/static/benefits-assets/shopping.png
+++ b/rencaika/static/benefits-assets/shopping.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/stay.png b/rencaika/static/benefits-assets/stay.png
index 78a47f7..8563d3a 100644
--- a/rencaika/static/benefits-assets/stay.png
+++ b/rencaika/static/benefits-assets/stay.png
Binary files differ
diff --git a/rencaika/static/benefits-assets/travel.png b/rencaika/static/benefits-assets/travel.png
index f55a8db..4fefdb2 100644
--- a/rencaika/static/benefits-assets/travel.png
+++ b/rencaika/static/benefits-assets/travel.png
Binary files differ
diff --git a/rencaika/static/card-assets/card-elite.jpg b/rencaika/static/card-assets/card-elite.jpg
index f37ba03..c77a35d 100644
--- a/rencaika/static/card-assets/card-elite.jpg
+++ b/rencaika/static/card-assets/card-elite.jpg
Binary files differ
diff --git a/rencaika/static/card-assets/card-excellence.jpg b/rencaika/static/card-assets/card-excellence.jpg
index fc85b31..d93a048 100644
--- a/rencaika/static/card-assets/card-excellence.jpg
+++ b/rencaika/static/card-assets/card-excellence.jpg
Binary files differ
diff --git a/rencaika/static/card-assets/card-outstanding.jpg b/rencaika/static/card-assets/card-outstanding.jpg
index b0a49fd..76b5a80 100644
--- a/rencaika/static/card-assets/card-outstanding.jpg
+++ b/rencaika/static/card-assets/card-outstanding.jpg
Binary files differ
diff --git a/rencaika/static/card-assets/talent-card-background.jpg b/rencaika/static/card-assets/talent-card-background.jpg
index 87b32a1..02026b6 100644
--- a/rencaika/static/card-assets/talent-card-background.jpg
+++ b/rencaika/static/card-assets/talent-card-background.jpg
Binary files differ
diff --git a/rencaika/static/entertainment-assets/entertainment-hero.webp b/rencaika/static/entertainment-assets/entertainment-hero.webp
index bccf187..00163de 100644
--- a/rencaika/static/entertainment-assets/entertainment-hero.webp
+++ b/rencaika/static/entertainment-assets/entertainment-hero.webp
Binary files differ
diff --git a/rencaika/static/entertainment-assets/wanda-cinema.webp b/rencaika/static/entertainment-assets/wanda-cinema.webp
index 5eeddcb..d7fc0b9 100644
--- a/rencaika/static/entertainment-assets/wanda-cinema.webp
+++ b/rencaika/static/entertainment-assets/wanda-cinema.webp
Binary files differ
diff --git a/rencaika/static/fonts/stay-calligraphy-subset.ttf b/rencaika/static/fonts/stay-calligraphy-subset.ttf
deleted file mode 100644
index dc4cf1e..0000000
--- a/rencaika/static/fonts/stay-calligraphy-subset.ttf
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/food-assets/food-1.jpg b/rencaika/static/food-assets/food-1.jpg
index 8b58c80..fd5c44b 100644
--- a/rencaika/static/food-assets/food-1.jpg
+++ b/rencaika/static/food-assets/food-1.jpg
Binary files differ
diff --git a/rencaika/static/food-assets/food-2.jpg b/rencaika/static/food-assets/food-2.jpg
index 827e0ac..77ff201 100644
--- a/rencaika/static/food-assets/food-2.jpg
+++ b/rencaika/static/food-assets/food-2.jpg
Binary files differ
diff --git a/rencaika/static/home-assets/card.png b/rencaika/static/home-assets/card.png
index adffef8..8b4da56 100644
--- a/rencaika/static/home-assets/card.png
+++ b/rencaika/static/home-assets/card.png
Binary files differ
diff --git a/rencaika/static/home-assets/login-mascot.png b/rencaika/static/home-assets/login-mascot.png
index ffbf1b1..9989581 100644
--- a/rencaika/static/home-assets/login-mascot.png
+++ b/rencaika/static/home-assets/login-mascot.png
Binary files differ
diff --git a/rencaika/static/home-assets/play.svg b/rencaika/static/home-assets/play.svg
deleted file mode 100644
index d2c36be..0000000
--- a/rencaika/static/home-assets/play.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="19" height="13.600006103515625" viewBox="0 0 19 13.600006103515625"><rect x="0.5" y="0.5" width="18" height="12.59999942779541" rx="4" fill-opacity="0" stroke-opacity="1" stroke="#FFFFFF" fill="none" stroke-width="1"/><path d="M8.9472136,4.22360682L12.9111452,6.2055726C13.6481938,6.5740969,13.6481938,7.6259031,12.9111452,7.9944272L8.9472135,9.9763932C8.28231257,10.3088431,7.5,9.8253474,7.5,9.0819654L7.5,5.1180341C7.5,4.37465209,8.28231257,3.89115629,8.9472136,4.22360682" fill="#FFFFFF" fill-opacity="1"/></svg>
\ No newline at end of file
diff --git a/rencaika/static/home-assets/top.jpg b/rencaika/static/home-assets/top.jpg
index bf8bbeb..bee2ee6 100644
--- a/rencaika/static/home-assets/top.jpg
+++ b/rencaika/static/home-assets/top.jpg
Binary files differ
diff --git a/rencaika/static/news-assets/meeting-thumb.jpg b/rencaika/static/news-assets/meeting-thumb.jpg
index 2612195..47ba123 100644
--- a/rencaika/static/news-assets/meeting-thumb.jpg
+++ b/rencaika/static/news-assets/meeting-thumb.jpg
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-explanation.jpg b/rencaika/static/policy-assets/policy-explanation.jpg
index c4522a1..93d9b78 100644
--- a/rencaika/static/policy-assets/policy-explanation.jpg
+++ b/rencaika/static/policy-assets/policy-explanation.jpg
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-hero.jpg b/rencaika/static/policy-assets/policy-hero.jpg
index df27645..7dce5ed 100644
--- a/rencaika/static/policy-assets/policy-hero.jpg
+++ b/rencaika/static/policy-assets/policy-hero.jpg
Binary files differ
diff --git a/rencaika/static/product-assets/payment-success.png b/rencaika/static/product-assets/payment-success.png
index ea9c086..e3051ec 100644
--- a/rencaika/static/product-assets/payment-success.png
+++ b/rencaika/static/product-assets/payment-success.png
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-hampton.jpg b/rencaika/static/stay-assets/hotel-hampton.jpg
index 1cffd30..b08c8dc 100644
--- a/rencaika/static/stay-assets/hotel-hampton.jpg
+++ b/rencaika/static/stay-assets/hotel-hampton.jpg
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-vienna.jpg b/rencaika/static/stay-assets/hotel-vienna.jpg
index 2d0f8de..1df04c6 100644
--- a/rencaika/static/stay-assets/hotel-vienna.jpg
+++ b/rencaika/static/stay-assets/hotel-vienna.jpg
Binary files differ
diff --git a/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue b/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
index 0f16395..7f357da 100644
--- a/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
+++ b/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
@@ -48,6 +48,7 @@
 	const payAmount = ref(0)
 	const subsidyAmount = ref(0)
 	const createdAt = ref('')
+	const loadError = ref('')
 	const submitting = ref(false)
 
 	const formatAmount = (amount: number): string => (amount / 100).toFixed(2)
@@ -72,11 +73,21 @@
 
 	const loadOrder = () => {
 		if (orderNo.value.length == 0) return
-		loadMerchantOrderDetail(orderNo.value, (order: MerchantOrderDetail, _traceId: string) => applyOrder(order), () => {})
+		loadMerchantOrderDetail(orderNo.value, (order: MerchantOrderDetail, _traceId: string) => {
+			loadError.value = ''
+			applyOrder(order)
+		}, (error: any) => {
+			loadError.value = error != null && error.message != null ? '' + error.message : '订单详情加载失败,请返回重新扫码'
+			uni.showToast({ title: loadError.value, icon: 'none' })
+		})
 	}
 
 	const confirm = () => {
 		if (submitting.value || verificationId.value.length == 0) return
+		if (loadError.value.length > 0) {
+			uni.showToast({ title: '订单详情未加载完成,请返回重新扫码', icon: 'none' })
+			return
+		}
 		uni.showModal({
 			title: '确认核销',
 			content: '确认后将完成本次订单核销,是否继续?',
diff --git a/rencaika_merchant/services/orders.uts b/rencaika_merchant/services/orders.uts
index ae27e74..3cb207a 100644
--- a/rencaika_merchant/services/orders.uts
+++ b/rencaika_merchant/services/orders.uts
@@ -88,26 +88,41 @@
 	}, onError)
 }
 
-const numberValue = (value: any): number => value == null ? 0 : Number(value)
+const fieldValue = (value: any, camelCaseName: string, snakeCaseName: string): any => {
+	if (value == null) return null
+	if (value[camelCaseName] != null) return value[camelCaseName]
+	return value[snakeCaseName] != null ? value[snakeCaseName] : null
+}
+
+const numberValue = (value: any): number => {
+	if (value == null || value === '') return 0
+	const number = Number(value)
+	return isNaN(number) ? 0 : number
+}
+
+const textValue = (value: any, camelCaseName: string, snakeCaseName: string, fallback: string = ''): string => {
+	const field = fieldValue(value, camelCaseName, snakeCaseName)
+	return field != null ? '' + field : fallback
+}
 
 const toOrder = (value: any): MerchantOrder => ({
-	id: value != null && value.id != null ? '' + value.id : '',
-	orderNo: value != null && value.orderNo != null ? '' + value.orderNo : '',
-	category: value != null && value.category != null ? '' + value.category : 'group',
-	status: value != null && value.status != null ? '' + value.status : 'pending_verification',
-	statusText: value != null && value.statusText != null ? '' + value.statusText : '待核销',
-	title: value != null && value.title != null ? '' + value.title : '订单',
-	customerName: value != null && value.customerName != null ? '' + value.customerName : '',
-	customerMobile: value != null && value.customerMobile != null ? '' + value.customerMobile : '',
-	cardLevel: value != null && value.cardLevel != null ? '' + value.cardLevel : '卓越人才卡',
-	orderAmount: numberValue(value != null ? value.orderAmount : 0),
-	basePayAmount: numberValue(value != null ? value.basePayAmount : 0),
-	couponAmount: numberValue(value != null ? value.couponAmount : 0),
-	talentDiscountAmount: numberValue(value != null ? value.talentDiscountAmount : 0),
-	payAmount: numberValue(value != null ? value.payAmount : 0),
-	subsidyAmount: numberValue(value != null ? value.subsidyAmount : 0),
-	createdAt: value != null && value.createdAt != null ? '' + value.createdAt : '',
-	verifyTime: value != null && value.verifyTime != null ? '' + value.verifyTime : ''
+	id: textValue(value, 'id', 'id'),
+	orderNo: textValue(value, 'orderNo', 'order_no'),
+	category: textValue(value, 'category', 'category', 'group'),
+	status: textValue(value, 'status', 'status', 'pending_verification'),
+	statusText: textValue(value, 'statusText', 'status_text', '待核销'),
+	title: textValue(value, 'title', 'title', '订单'),
+	customerName: textValue(value, 'customerName', 'customer_name'),
+	customerMobile: textValue(value, 'customerMobile', 'customer_mobile'),
+	cardLevel: textValue(value, 'cardLevel', 'card_level', '卓越人才卡'),
+	orderAmount: numberValue(fieldValue(value, 'orderAmount', 'order_amount')),
+	basePayAmount: numberValue(fieldValue(value, 'basePayAmount', 'base_pay_amount')),
+	couponAmount: numberValue(fieldValue(value, 'couponAmount', 'coupon_amount')),
+	talentDiscountAmount: numberValue(fieldValue(value, 'talentDiscountAmount', 'talent_discount_amount')),
+	payAmount: numberValue(fieldValue(value, 'payAmount', 'pay_amount')),
+	subsidyAmount: numberValue(fieldValue(value, 'subsidyAmount', 'subsidy_amount')),
+	createdAt: textValue(value, 'createdAt', 'created_at'),
+	verifyTime: textValue(value, 'verifyTime', 'verify_time')
 })
 
 const toOrderDetail = (value: any): MerchantOrderDetail => {
@@ -117,11 +132,11 @@
 		basePayAmount: order.basePayAmount != null ? order.basePayAmount : order.orderAmount,
 		couponAmount: order.couponAmount != null ? order.couponAmount : 0,
 		talentDiscountAmount: order.talentDiscountAmount != null ? order.talentDiscountAmount : 0,
-		benefitName: value != null && value.benefitName != null ? '' + value.benefitName : order.title,
-		merchantName: value != null && value.merchantName != null ? '' + value.merchantName : '',
-		canRevoke: value != null && value.canRevoke == true,
-		revokeDeadline: value != null && value.revokeDeadline != null ? '' + value.revokeDeadline : '',
-		revokeReason: value != null && value.revokeReason != null ? '' + value.revokeReason : ''
+		benefitName: textValue(value, 'benefitName', 'benefit_name', order.title),
+		merchantName: textValue(value, 'merchantName', 'merchant_name'),
+		canRevoke: fieldValue(value, 'canRevoke', 'can_revoke') == true,
+		revokeDeadline: textValue(value, 'revokeDeadline', 'revoke_deadline'),
+		revokeReason: textValue(value, 'revokeReason', 'revoke_reason')
 	}
 }
 
diff --git a/talent-card-admin/src/api/admin.ts b/talent-card-admin/src/api/admin.ts
index 0208fdb..075709f 100644
--- a/talent-card-admin/src/api/admin.ts
+++ b/talent-card-admin/src/api/admin.ts
@@ -73,6 +73,14 @@
 }
 
 export function getPlatformSettings() { return http.get<Record<string, string>, Record<string, string>>('/api/admin/management/settings') }
+export interface AdminPendingNotice {
+  id: string
+  category: string
+  content: string
+  time: string
+  path: string
+}
+export function getAdminPendingNotices() { return http.get<AdminPendingNotice[], AdminPendingNotice[]>('/api/admin/business/pending-notices') }
 export function savePlatformSettings(payload: Record<string, string>) { return http.put<Record<string, string>, Record<string, string>>('/api/admin/management/settings', payload) }
 export function getProtocolSetting(key: string) { return http.get<Record<string, any>, Record<string, any>>(`/api/admin/management/protocols/${key}`) }
 export function saveProtocolSetting(key: string, contentHtml: string) { return http.put<Record<string, any>, Record<string, any>>(`/api/admin/management/protocols/${key}`, { contentHtml }) }
@@ -82,6 +90,7 @@
 export function updateAdminRoleStatus(id: number | string, status: number) { return http.patch<AdminRole, AdminRole>(`/api/admin/management/roles/${id}/status`, { status }) }
 export function deleteAdminRole(id: number | string) { return http.delete<any, any>(`/api/admin/management/roles/${id}`) }
 export function getAdminStaff() { return http.get<AdminStaff[], AdminStaff[]>('/api/admin/management/staff') }
+export function checkAdminStaffAccount(account: string) { return http.get<{ available: boolean }, { available: boolean }>('/api/admin/management/staff/account-available', { params: { account } }) }
 export function createAdminStaff(payload: Omit<AdminStaff, 'id' | 'createdAt'> & { password: string }) { return http.post<AdminStaff, AdminStaff>('/api/admin/management/staff', payload) }
 export function updateAdminStaff(id: number | string, payload: Omit<AdminStaff, 'id' | 'createdAt'> & { password?: string }) { return http.put<AdminStaff, AdminStaff>(`/api/admin/management/staff/${id}`, payload) }
 export function resetAdminStaffPassword(id: number | string, password: string) { return http.patch<any, any>(`/api/admin/management/staff/${id}/password`, { password }) }
diff --git a/talent-card-admin/src/layouts/AdminLayout.vue b/talent-card-admin/src/layouts/AdminLayout.vue
index f711298..8b0a299 100644
--- a/talent-card-admin/src/layouts/AdminLayout.vue
+++ b/talent-card-admin/src/layouts/AdminLayout.vue
@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed, ref } from 'vue'
+import { computed, nextTick, onMounted, ref, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { ElMessageBox } from 'element-plus'
 import {
@@ -15,6 +15,8 @@
   Wallet,
 } from '@element-plus/icons-vue'
 import { logoutAdmin } from '../api/auth'
+import { getAdminPendingNotices } from '../api/admin'
+import type { AdminPendingNotice } from '../api/admin'
 import { useAppStore } from '../stores/app'
 
 interface MenuItem {
@@ -36,38 +38,23 @@
 const appStore = useAppStore()
 const avatarText = computed(() => (appStore.user.name || 'A').trim().slice(0, 1).toUpperCase())
 
-interface HeaderNotice {
-  id: number
-  content: string
-  time: string
-  read: boolean
+const notices = ref<AdminPendingNotice[]>([])
+const adminMenuRef = ref<{ open: (index: string) => void; close: (index: string) => void }>()
+
+function formatNoticeTime(value: string) {
+  return value ? value.replace('T', ' ').replace(/\.\d+$/, '') : '-'
 }
 
-const notices = ref<HeaderNotice[]>([
-  {
-    id: 1,
-    content: '上午好,欢迎登录攀枝花市东区人才服务平台。',
-    time: '2026/6/25 09:50:54',
-    read: false,
-  },
-  {
-    id: 2,
-    content: '您有一条新的酒店预约,请及时查看。',
-    time: '2026/6/25 09:54:08',
-    read: false,
-  },
-])
-
-const unreadNoticeCount = computed(() => notices.value.filter((item) => !item.read).length)
-
-function markNoticeRead(notice: HeaderNotice) {
-  notice.read = true
+async function loadNotices() {
+  try {
+    notices.value = await getAdminPendingNotices()
+  } catch {
+    notices.value = []
+  }
 }
 
-function markAllNoticesRead() {
-  notices.value.forEach((item) => {
-    item.read = true
-  })
+function openNotice(notice: AdminPendingNotice) {
+  router.push(notice.path)
 }
 
 const cardMenus: MenuItem[] = [
@@ -84,7 +71,7 @@
 ]
 
 const financeMenus: MenuItem[] = [
-  { path: '/merchant-settlement', label: '提现管理' },
+  { path: '/merchant-settlement', label: '商户结算审核' },
 ]
 
 const orderMenus: MenuItem[] = [
@@ -131,11 +118,11 @@
 // Keep the navigation in sync with the permissions returned at login.  Newer
 // routes without a dedicated permission code intentionally remain visible.
 const routePermissions: Record<string, string> = {
-  '/talents': 'admin.talents.view', '/family-card-applications': 'admin.family-cards.view', '/upgrade-applications': 'admin.talents.view', '/family-cards': 'admin.family-cards.view', '/child-cards': 'admin.family-cards.view',
-  '/merchants': 'admin.merchants.view', '/products': 'admin.products.view', '/orders': 'admin.orders.view', '/refund-records': 'admin.refunds.view', '/hotel-free-verifications': 'admin.orders.view',
-  '/medical-reservations': 'admin.medical-reservations.view', '/medical-applications': 'admin.medical-reservations.view', '/parking-plates': 'admin.parking-plates.view', '/fuel-services': 'admin.parking-plates.view', '/spouse-employment': 'admin.parking-plates.view', '/hotel-reservations': 'admin.medical-reservations.view',
-  '/merchant-settlement': 'admin.merchants.view', '/equity-images': 'admin.equity-images.view', '/talent-news-management': 'admin.news.view', '/talent-policy-management': 'admin.news.view', '/movie-tickets': 'admin.news.view',
-  '/system-settings': 'admin.settings.view', '/protocol-settings': 'admin.settings.view', '/roles': 'admin.roles.view', '/staff': 'admin.staff.view',
+  '/talents': 'admin.talents.view', '/family-card-applications': 'admin.family-card-applications.view', '/upgrade-applications': 'admin.upgrade-applications.view', '/family-cards': 'admin.family-cards.view', '/child-cards': 'admin.child-cards.view',
+  '/merchants': 'admin.merchants.view', '/products': 'admin.products.view', '/orders': 'admin.orders.view', '/refund-records': 'admin.refunds.view', '/hotel-free-verifications': 'admin.hotel-free-verifications.view',
+  '/appointment-management': 'admin.appointment-management.view', '/medical-reservations': 'admin.medical-reservations.view', '/medical-applications': 'admin.medical-applications.view', '/parking-plates': 'admin.parking-plates.view', '/car-wash-benefits': 'admin.car-wash-benefits.view', '/fuel-services': 'admin.fuel-services.view', '/spouse-employment': 'admin.spouse-employment.view', '/hotel-reservations': 'admin.hotel-reservations.view',
+  '/merchant-settlement': 'admin.merchant-settlement.view', '/equity-images': 'admin.equity-images.view', '/talent-news-management': 'admin.news.view', '/talent-policy-management': 'admin.policies.view', '/movie-tickets': 'admin.movie-tickets.view',
+  '/system-settings': 'admin.settings.view', '/protocol-settings': 'admin.protocol-settings.view', '/roles': 'admin.roles.view', '/staff': 'admin.staff.view',
 }
 const canAccess = (path?: string) => {
   if (!path || appStore.user.role === '平台管理') return true
@@ -204,6 +191,32 @@
   return route.path
 })
 
+function currentMenuGroupIndex() {
+  return menuGroups.find((group) => findMenuLabel(group.items, activeMenu.value))?.index
+}
+
+function closeOtherMenuGroups(openedGroupIndex?: string) {
+  menuGroups.forEach((group) => {
+    if (group.index !== openedGroupIndex) adminMenuRef.value?.close(group.index)
+  })
+}
+
+function handleMenuOpen(_index: string, indexPath: string[]) {
+  // indexPath 的第一个值始终是一级模块;无论点击一级或嵌套菜单,均只保留该模块展开。
+  const openedGroupIndex = indexPath[0]
+  if (menuGroups.some((group) => group.index === openedGroupIndex)) {
+    closeOtherMenuGroups(openedGroupIndex)
+  }
+}
+
+watch(activeMenu, () => {
+  const groupIndex = currentMenuGroupIndex()
+  nextTick(() => {
+    closeOtherMenuGroups(groupIndex)
+    if (groupIndex) adminMenuRef.value?.open(groupIndex)
+  })
+}, { immediate: true })
+
 function findMenuLabel(items: MenuItem[], path: string): string | undefined {
   for (const item of items) {
     if (item.path === path) return item.label
@@ -257,6 +270,8 @@
     router.push('/login')
   }
 }
+
+onMounted(loadNotices)
 </script>
 
 <template>
@@ -272,31 +287,27 @@
       </div>
 
       <div class="header-right">
-        <el-popover placement="bottom-end" :width="330" trigger="click" popper-class="notice-popover">
+        <el-popover placement="bottom-end" :width="330" trigger="click" popper-class="notice-popover" @show="loadNotices">
           <template #reference>
-            <el-badge :is-dot="unreadNoticeCount > 0" class="notice-badge">
+            <el-badge :is-dot="notices.length > 0" class="notice-badge">
               <el-button :icon="Bell" circle aria-label="消息通知" />
             </el-badge>
           </template>
 
-          <div class="notice-panel">
-            <div class="notice-panel-header">
-              <strong>通知公告</strong>
-              <button type="button" :disabled="unreadNoticeCount === 0" @click="markAllNoticesRead">
-                全部已读
-              </button>
-            </div>
+            <div class="notice-panel">
+              <div class="notice-panel-header">
+              <strong>待办通知</strong>
+              </div>
             <div v-if="notices.length" class="notice-list">
               <button
                 v-for="notice in notices"
                 :key="notice.id"
                 type="button"
                 class="notice-item"
-                @click="markNoticeRead(notice)"
+                @click="openNotice(notice)"
               >
                 <span class="notice-content">{{ notice.content }}</span>
-                <span v-if="!notice.read" class="notice-unread">未读</span>
-                <time>{{ notice.time }}</time>
+                <time>{{ formatNoticeTime(notice.time) }}</time>
               </button>
             </div>
             <div v-else class="notice-empty">暂无通知</div>
@@ -312,10 +323,12 @@
     <el-container class="admin-body">
       <el-aside width="220px" class="admin-aside">
         <el-menu
+          ref="adminMenuRef"
           router
           :default-active="activeMenu"
           :default-openeds="defaultOpeneds"
           unique-opened
+          @open="handleMenuOpen"
           class="admin-menu"
         >
           <el-menu-item index="/dashboard">
diff --git a/talent-card-admin/src/router/index.ts b/talent-card-admin/src/router/index.ts
index 79bbe5d..d17b165 100644
--- a/talent-card-admin/src/router/index.ts
+++ b/talent-card-admin/src/router/index.ts
@@ -110,7 +110,7 @@
           path: 'merchant-settlement',
           name: 'merchant-settlement',
           component: () => import('../views/MerchantSettlementView.vue'),
-          meta: { title: '提现管理' },
+          meta: { title: '商户结算审核' },
         },
         {
           path: 'merchant-onboardings/:id',
diff --git a/talent-card-admin/src/style.css b/talent-card-admin/src/style.css
index 613c329..aa3024f 100644
--- a/talent-card-admin/src/style.css
+++ b/talent-card-admin/src/style.css
@@ -963,7 +963,7 @@
 .merchant-rights-detail-row {
   display: grid;
   grid-template-columns: 96px minmax(360px, 520px) 270px;
-  gap: 14px 24px;
+  gap: 6px 24px;
   align-items: start;
   padding: 14px 0 0;
 }
@@ -976,8 +976,10 @@
 
 .merchant-rights-detail-row label {
   grid-column: 1 / 3;
+  margin: 0;
   color: #334155;
   font-size: 13px;
+  line-height: 20px;
 }
 
 .merchant-rights-level-row + .merchant-rights-detail-row {
@@ -1032,6 +1034,11 @@
   align-items: start;
   color: #111827;
   font-size: 13px;
+}
+
+/* 商家权益仅保留后台配置表单,不展示小程序示例预览。 */
+.merchant-rights-panel .merchant-rights-example {
+  display: none !important;
 }
 
 .merchant-rights-phone-preview {
@@ -1282,7 +1289,7 @@
 
 .consume-desc-row {
   padding-left: 186px;
-  padding-top: 14px;
+  padding-top: 10px;
 }
 
 .merchant-products-panel,
@@ -1339,8 +1346,14 @@
 .merchant-coupons-filter .el-select__wrapper {
   min-height: 28px;
   border-radius: 0;
-  background: #f7f8fb;
-  box-shadow: none;
+  background: #ffffff;
+  box-shadow: 0 0 0 1px #d9dee8 inset;
+}
+
+.merchant-products-filter .el-date-editor.el-input__wrapper,
+.merchant-coupons-filter .el-date-editor.el-input__wrapper {
+  background: #ffffff;
+  box-shadow: 0 0 0 1px #d9dee8 inset;
 }
 
 .merchant-products-filter .el-date-editor.el-input__wrapper,
@@ -1516,7 +1529,7 @@
 }
 
 .merchant-coupons-filter .coupon-created-time-picker {
-  width: 320px;
+  width: 320px !important;
 }
 
 .merchant-products-filter .product-created-time-picker {
@@ -1544,12 +1557,12 @@
 
 .product-form-grid {
   display: flex;
-  gap: 26px;
+  gap: 34px;
   align-items: start;
 }
 
 .product-info-left {
-  width: 360px;
+  width: 430px;
   flex-shrink: 0;
 }
 
@@ -1573,14 +1586,29 @@
 .product-form-row,
 .product-static-row {
   display: grid;
-  grid-template-columns: 54px minmax(0, 1fr);
+  grid-template-columns: 68px minmax(0, 1fr);
   align-items: center;
-  gap: 10px;
+  gap: 16px;
   margin-bottom: 18px;
 }
 
 .product-form-row.amount {
-  grid-template-columns: 54px 134px 24px;
+  grid-template-columns: 68px minmax(0, 1fr);
+  gap: 16px;
+}
+
+.product-form-row.amount .el-form-item__content {
+  display: flex;
+  flex-wrap: nowrap;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+  margin-left: 0 !important;
+}
+
+.product-form-row.amount .el-form-item__content > span {
+  flex: 0 0 auto;
+  white-space: nowrap;
 }
 
 .product-form-row.amount.hotel-amount {
@@ -1608,6 +1636,11 @@
   white-space: nowrap;
 }
 
+.merchant-product-edit-form .el-form-item__label {
+  width: 68px !important;
+  white-space: nowrap;
+}
+
 .product-static-row span {
   color: #475569;
   font-weight: 600;
@@ -1615,15 +1648,16 @@
 
 .product-image-row {
   display: grid;
-  grid-template-columns: 54px 256px 78px;
-  gap: 10px;
-  align-items: end;
+  grid-template-columns: 68px 256px 78px;
+  column-gap: 16px;
+  align-items: center;
   margin-bottom: 20px;
 }
 
 .product-image-row.compact {
-	grid-template-columns: 54px minmax(172px, 1fr) 78px;
-	align-items: start;
+	grid-template-columns: 68px minmax(172px, 1fr) 78px;
+	column-gap: 16px;
+	align-items: center;
 }
 
 .product-image-list {
@@ -1634,7 +1668,21 @@
 }
 
 .product-image-row > label {
-  align-self: start;
+  align-self: center;
+}
+
+.product-image-row > .el-button {
+  align-self: center;
+  justify-self: start;
+}
+
+.product-image-row .el-form-item__content {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 12px;
+  min-width: 0;
+  margin-left: 0 !important;
 }
 
 .product-cover-preview,
@@ -1707,13 +1755,13 @@
 
 .product-detail-row {
   display: grid;
-  grid-template-columns: 54px minmax(0, 1fr);
-  gap: 10px;
+  grid-template-columns: 68px minmax(0, 1fr);
+  gap: 16px;
   margin-top: 4px;
 }
 
 .product-detail-row label {
-  align-self: start;
+  align-self: center;
 }
 
 .product-detail-row .el-textarea {
@@ -1732,6 +1780,12 @@
   gap: 8px;
 }
 
+.product-notice-field .el-date-editor,
+.product-notice-field .el-time-editor,
+.product-notice-field .el-input {
+  width: 100%;
+}
+
 .product-rights-list {
   display: grid;
   gap: 28px;
@@ -1744,57 +1798,79 @@
 }
 
 .product-right-main {
-  display: flex;
+  /*
+   * 三种权益状态使用同一套列:避免免费、超过次数、打折三行因为
+   * 内容宽度或 margin 不同而把“是否政府补贴”挤到不同的位置。
+   */
+  display: grid;
+  grid-template-columns: 86px 42px 58px 64px 42px 40px 88px 8px 22px 24px 58px 100px 12px 88px 42px;
   align-items: center;
   min-height: 30px;
 }
 
 .product-right-sub {
-  display: flex;
+  display: grid;
+  /* 与主行共用列定义,输入框和补贴开关逐列对齐。 */
+  grid-template-columns: 86px 42px 58px 64px 42px 40px 88px 8px 22px 24px 58px 100px 12px 88px 42px;
   align-items: center;
   min-height: 28px;
-  padding-left: 186px;
 }
 
-.product-right-sub > span:first-child {
-  width: 64px;
-  margin-right: 18px;
+.product-right-main .right-level { grid-column: 1; }
+.product-right-main .right-enabled { grid-column: 2; margin: 0; }
+.product-right-main .right-free-label { grid-column: 4; }
+.product-right-main .right-free-switch { grid-column: 5; margin: 0; }
+.product-right-main.free .right-limit-field { grid-column: 7; margin: 0; }
+.product-right-main.free .right-unit { grid-column: 9; margin: 0; }
+.product-right-main.free .right-reset-label { grid-column: 11; margin: 0; }
+.product-right-main.free .right-reset-select { grid-column: 12; margin: 0; }
+/* 打折行使用一个独立的紧凑控件组,补贴列仍固定在第 14 列。 */
+.product-right-main:not(.free) .right-discount-control {
+  grid-column: 7 / span 6;
+  display: flex;
+  align-items: center;
+  gap: 8px;
 }
 
-.product-right-sub > span:nth-child(3) {
-  width: 22px;
-  margin-left: 8px;
+.product-right-main:not(.free) .right-discount-label {
+  flex: 0 0 38px;
 }
 
-.product-right-sub > span:nth-child(4) {
-  width: 88px;
-  margin-left: 10px;
+.product-right-main:not(.free) .right-discount-input {
+  flex: 0 0 100px;
+  width: 100px;
+  margin: 0;
 }
+
+.product-right-main:not(.free) .right-unit {
+  flex: 0 0 22px;
+}
+.product-right-main .right-subsidy-label { grid-column: 14; margin: 0 !important; white-space: nowrap; }
+.product-right-main .right-subsidy-switch { grid-column: 15; margin: 0; }
+
+.product-right-sub .right-exceed-label { grid-column: 4 / span 3; }
+.product-right-sub .right-exceed-discount-field { grid-column: 7; }
+.product-right-sub .right-exceed-unit { grid-column: 9; }
+.product-right-sub .right-exceed-subsidy-label { grid-column: 14; white-space: nowrap; }
+.product-right-sub .right-exceed-subsidy-switch { grid-column: 15; }
 
 .product-right-row .right-level {
-  flex: 0 0 86px;
   color: #111827;
   font-size: 12px;
 }
 
 .product-right-row .right-enabled {
-  flex: 0 0 42px;
-  margin-right: 58px;
 }
 
 .product-right-row .right-free-label {
-  flex: 0 0 64px;
 }
 
 .product-right-row .right-free-switch {
-  flex: 0 0 42px;
-  margin-right: 40px;
 }
 
 .product-right-row .right-limit-field {
   position: relative;
-  flex: 0 0 88px;
-  margin-right: 8px;
+  width: 88px;
 }
 
 .product-right-row .right-limit-field .el-input-number {
@@ -1802,38 +1878,25 @@
 }
 
 .product-right-row .right-unit {
-  flex: 0 0 22px;
 }
 
 .product-right-row .right-reset-label {
-  flex: 0 0 58px;
-  margin-left: 24px;
 }
 
 .product-right-row .right-reset-select {
-  flex: 0 0 96px;
-  margin-right: 12px;
+  width: 100px;
 }
 
 .product-right-row .right-discount-label {
-  flex: 0 0 38px;
 }
 
 .product-right-row .right-discount-input {
-  flex: 0 0 100px;
-  margin-right: 8px;
 }
 
 .product-right-row .right-subsidy-label {
-  flex: 0 0 88px;
 }
 
 .product-right-row .right-subsidy-switch {
-  flex: 0 0 42px;
-}
-
-.product-right-main:not(.free) .right-subsidy-label {
-  margin-left: 30px;
 }
 
 .merchant-product-form-panel .product-right-row .el-input {
@@ -1845,7 +1908,23 @@
 }
 
 .merchant-product-form-panel .product-right-sub .right-exceed-discount-input {
-  width: 100px;
+  width: 88px;
+}
+
+.product-right-sub .right-exceed-discount-field {
+  position: relative;
+  width: 88px;
+}
+
+.product-right-sub .field-error {
+  position: absolute;
+  top: calc(100% + 4px);
+  left: 0;
+  width: max-content;
+  color: #f56c6c;
+  font-size: 12px;
+  line-height: 16px;
+  white-space: nowrap;
 }
 
 .merchant-product-form-panel .product-right-row .right-discount-input {
@@ -1854,6 +1933,12 @@
 
 .merchant-product-form-panel .product-right-row .el-select {
   width: 64px;
+}
+
+/* 团单免费次数的重置周期需完整展示“每天/每周/每月/每年”。 */
+.merchant-product-form-panel .product-right-row .right-reset-select {
+  flex-basis: 100px;
+  width: 100px;
 }
 
 .product-right-tip {
@@ -1881,8 +1966,19 @@
 .merchant-product-form-panel .el-textarea__inner {
   min-height: 28px;
   border-radius: 0;
-  background: #f7f8fb;
+  background: #ffffff;
   box-shadow: 0 0 0 1px #d9dee8 inset;
+}
+
+/* Element Plus 会为禁用/只读控件注入灰底;团单编辑页仍需和其他表单控件保持同一白底与边框。 */
+.merchant-product-form-panel .el-input.is-disabled .el-input__wrapper,
+.merchant-product-form-panel .el-input.is-readonly .el-input__wrapper,
+.merchant-product-form-panel .el-input-number.is-disabled .el-input__wrapper,
+.merchant-product-form-panel .el-select.is-disabled .el-select__wrapper,
+.merchant-product-form-panel .el-date-editor.is-disabled .el-input__wrapper,
+.merchant-product-form-panel .el-textarea.is-disabled .el-textarea__inner {
+  background: #ffffff !important;
+  box-shadow: 0 0 0 1px #d9dee8 inset !important;
 }
 
 .merchant-product-form-panel .el-button,
@@ -1975,17 +2071,21 @@
 }
 
 .coupon-requirement-row .coupon-full-amount {
-  width: 108px;
-  margin: 0 6px 0 10px;
+  width: 168px;
+  margin: 0 50px 0 10px;
 }
 
-.coupon-requirement-row .coupon-full-unit {
-  display: inline-flex;
-  flex: 0 0 44px;
-  align-items: center;
-  margin: 0 50px 0 0;
-  line-height: 28px;
+.coupon-requirement-row .coupon-full-amount .el-input-group__append {
+  padding: 0 0 0 8px;
+  color: #334155;
+  background: transparent;
+  border: 0;
+  box-shadow: none;
   white-space: nowrap;
+}
+
+.coupon-requirement-row .coupon-full-amount .el-input__wrapper {
+  border-radius: var(--el-border-radius-base);
 }
 
 .coupon-requirement-row .coupon-none-radio {
@@ -2487,6 +2587,10 @@
 
 .order-filter-form .el-form-item {
   margin-bottom: 0;
+}
+
+.order-filter-form .family-order-filter .el-form-item__label {
+  white-space: nowrap;
 }
 
 .order-filter-form .el-input,
@@ -3112,6 +3216,11 @@
   margin-bottom: 0;
 }
 
+/* 角色管理筛选项与标题左边缘对齐。 */
+.role-management-filter-form .el-form-item__label {
+  justify-content: flex-start;
+}
+
 .business-fill-filter-form .el-input,
 .business-fill-filter-form .el-select,
 .business-fill-filter-form .el-date-editor {
@@ -3635,6 +3744,7 @@
   color: #334155;
   font-size: 13px;
   line-height: 32px;
+  white-space: nowrap;
 }
 
 .news-publish-form .el-input {
@@ -3856,22 +3966,44 @@
   padding: 16px 0 10px;
 }
 
-.news-cover-preview {
-  width: 120px;
-  height: 72px;
-  margin-left: 12px;
-  vertical-align: middle;
-  border: 1px solid #d9dee8;
-  border-radius: 4px;
+.article-cover-upload .el-upload {
+  display: block;
 }
 
-.policy-cover-preview {
-  width: 120px;
-  height: 72px;
-  margin-left: 12px;
-  vertical-align: middle;
-  border: 1px solid #d9dee8;
-  border-radius: 4px;
+.article-cover-upload-card,
+.article-cover-preview {
+  width: 76px;
+  height: 76px;
+  box-sizing: border-box;
+  border: 1px dashed #d7dce4;
+}
+
+.article-cover-upload-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #a8b0bb;
+  background: #ffffff;
+}
+
+.article-cover-upload-card span {
+  height: 30px;
+  font-size: 31px;
+  font-weight: 200;
+  line-height: 26px;
+}
+
+.article-cover-upload-card small {
+  margin-top: 5px;
+  font-size: 12px;
+  line-height: 16px;
+}
+
+.article-cover-preview {
+  display: block;
+  border-style: solid;
+  object-fit: cover;
 }
 
 .news-editor-file-input {
@@ -4086,6 +4218,13 @@
   width: 100%;
 }
 
+/* 管理后台的日期/时间选择控件统一为 320px,保证范围内容完整且各页面一致。 */
+.page-stack .el-date-editor,
+.prototype-page .el-date-editor,
+.merchant-prototype-page .el-date-editor {
+  width: 320px !important;
+}
+
 .prototype-table-tools {
   display: flex;
   align-items: center;
diff --git a/talent-card-admin/src/views/BusinessManagementView.vue b/talent-card-admin/src/views/BusinessManagementView.vue
index a3e7f77..3e07bd7 100644
--- a/talent-card-admin/src/views/BusinessManagementView.vue
+++ b/talent-card-admin/src/views/BusinessManagementView.vue
@@ -171,7 +171,7 @@
   <div class="page-stack prototype-page business-fill-page">
     <section class="panel">
       <div class="panel-header"><h3>{{ title }}</h3></div>
-      <el-form class="business-fill-filter-form" label-width="86px">
+      <el-form class="business-fill-filter-form" :class="{ 'role-management-filter-form': isRole }" label-width="86px">
         <el-form-item :label="isUpgrade ? '持卡人' : isRole ? '角色' : '员工'">
           <el-input v-model="filters.keyword" :placeholder="isUpgrade ? '持卡人姓名、手机号、卡号' : '请输入关键词'" clearable />
         </el-form-item>
@@ -210,7 +210,7 @@
         <el-table-column label="操作" width="150" fixed="right" align="center">
           <template #default="{ row }">
             <el-button link type="primary" :icon="View" @click="view(row)">查看</el-button>
-            <el-button v-if="!isUpgrade" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
+            <el-button v-if="!isUpgrade && !(isRole && isPlatformAdminRole(row))" link type="primary" :icon="Edit" @click="openEdit(row)">编辑</el-button>
             <template v-if="isRole && !isPlatformAdminRole(row)">
               <el-button link type="primary" @click="changeRoleStatus(row)">{{ Number(row.status) === 1 ? '停用' : '启用' }}</el-button>
               <el-button link type="danger" @click="removeRole(row)">删除</el-button>
@@ -229,9 +229,9 @@
     </el-dialog>
     <el-dialog v-model="editVisible" :title="`编辑${title}`" width="480px" destroy-on-close :close-on-click-modal="false">
       <el-form ref="formRef" :model="form" :rules="rules" label-width="92px">
-        <el-form-item :label="isRole ? '角色名称' : '员工姓名'" prop="name"><el-input v-model="form.name" /></el-form-item>
-        <el-form-item :label="isRole ? '角色编码' : '登录账号'" prop="code"><el-input v-model="form.code" :placeholder="isRole ? '' : '仅支持英文和数字'" maxlength="32" /></el-form-item>
-        <el-form-item v-if="!isRole" label="登录密码" prop="password"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item>
+        <el-form-item :label="isRole ? '角色名称' : '员工姓名'" prop="name" required><el-input v-model="form.name" /></el-form-item>
+        <el-form-item :label="isRole ? '角色编码' : '登录账号'" prop="code" required><el-input v-model="form.code" :placeholder="isRole ? '' : '仅支持英文和数字'" maxlength="32" /></el-form-item>
+        <el-form-item v-if="!isRole" label="登录密码" prop="password" :required="!editingId"><el-input v-model="form.password" type="password" :placeholder="editingId ? '留空则不修改' : '请输入登录密码'" /></el-form-item>
         <el-form-item v-if="isRole" label="备注"><el-input v-model="form.remark" /></el-form-item>
         <el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
       </el-form>
diff --git a/talent-card-admin/src/views/MerchantCreateView.vue b/talent-card-admin/src/views/MerchantCreateView.vue
index 97be179..c343c9c 100644
--- a/talent-card-admin/src/views/MerchantCreateView.vue
+++ b/talent-card-admin/src/views/MerchantCreateView.vue
@@ -75,7 +75,9 @@
 const routeTab = typeof route.query.tab === 'string' ? route.query.tab : 'settings'
 const activeTab = ref(['settings', 'rights', 'products', 'coupons'].includes(routeTab) ? routeTab : 'settings')
 const couponMode = ref<'list' | 'edit'>('list')
-const isEditMode = computed(() => route.query.mode === 'edit')
+const hasValidMerchantId = computed(() => /^\d+$/.test(queryString('merchantId')))
+const isEditMode = computed(() => route.query.mode === 'edit' && hasValidMerchantId.value)
+const hasExistingMerchant = computed(() => isEditMode.value)
 const normalizedMerchantType = computed(() => String(form.merchantType || '').trim())
 const isFoodMerchant = computed(() => normalizedMerchantType.value === '餐饮服务')
 const isApartmentMerchant = computed(() => normalizedMerchantType.value === '公寓租赁')
@@ -89,6 +91,7 @@
 const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业')
 const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批')
 const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务')
+const requiresFoodMerchantSettings = computed(() => !isEditMode.value && isFoodMerchant.value)
 const supportsStoreConsumption = computed(() => [
   '酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检',
 ].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal'))
@@ -99,6 +102,10 @@
   '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务',
 ]
 const usesSimplifiedMerchantSettings = computed(() => simplifiedMerchantSettingsTypes.includes(normalizedMerchantType.value))
+const settingsAndRightsOnlyMerchantTypes = [
+  '加油服务', '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务',
+]
+const usesSettingsAndRightsOnlyTabs = computed(() => settingsAndRightsOnlyMerchantTypes.includes(normalizedMerchantType.value))
 const supportsMerchantAppLogin = computed(() => (
   isServiceMerchant.value
   || isApartmentMerchant.value
@@ -159,7 +166,22 @@
   return merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
 }
 
+function applyNewMerchantDefaults() {
+  if (isEditMode.value) return
+
+  const merchantType = queryString('merchantType')
+  form.merchantType = merchantType
+  form.recommend = merchantType === '餐饮服务' || merchantType === '景区服务' ? 1 : 0
+  form.sortNo = undefined
+}
+
 const tabs = computed(() => {
+  if (usesSettingsAndRightsOnlyTabs.value) {
+    return [
+      { label: '商家设置', value: 'settings' },
+      { label: '商家权益', value: 'rights' },
+    ]
+  }
   if (isApartmentMerchant.value) {
     return [
       { label: '商家设置', value: 'settings' },
@@ -235,9 +257,12 @@
     trigger: 'change',
   }],
   sortNo: [{
-    validator: (_rule, value, callback) => value === undefined || value === null || String(value).trim() === ''
-      ? callback(new Error('请输入排序'))
-      : callback(),
+    validator: (_rule, value, callback) => {
+      const text = String(value ?? '').trim()
+      if (!text) return callback(new Error('请输入排序'))
+      if (!/^\d+$/.test(text)) return callback(new Error('排序只能输入非负整数'))
+      callback()
+    },
     trigger: 'blur',
   }],
 }))
@@ -376,7 +401,7 @@
   validPeriod: '',
   useMethod: '',
   availableTime: '',
-  otherRule: '',
+  otherRules: '',
 })
 const productValidPeriodRange = ref<string[]>([])
 const productAvailableTimeRange = ref<string[]>([])
@@ -416,6 +441,7 @@
     exceedSubsidy: true,
   },
 ])
+const productExceedDiscountErrors = reactive<Record<string, string>>({})
 
 function defaultProductRights() {
   return [
@@ -961,6 +987,7 @@
   '提供包间、免费停车、WiFi等服务',
   '菜品以家常菜、特色川菜为主,具体以门店实际为准',
 ]
+
 const locationDialogVisible = ref(false)
 const pendingLongitude = ref('')
 const pendingLatitude = ref('')
@@ -1536,7 +1563,7 @@
   productNoticeForm.validPeriod = notice.validPeriod || ''
   productNoticeForm.useMethod = notice.useMethod || ''
   productNoticeForm.availableTime = notice.availableTime || ''
-  productNoticeForm.otherRule = notice.otherRule || ''
+  productNoticeForm.otherRules = notice.otherRules || notice.otherRule || ''
   productValidPeriodRange.value = rangeValues(productNoticeForm.validPeriod, /\d{4}-\d{2}-\d{2}/g)
   productAvailableTimeRange.value = rangeValues(productNoticeForm.availableTime, /\d{2}:\d{2}/g)
   if (isHealthCheckMerchant.value) {
@@ -1609,6 +1636,16 @@
   return amount > 0 && amount <= 10
 }
 
+function validateProductExceedDiscount(item: { level: string, exceedDiscount: unknown }) {
+  const key = item.level
+  if (validExceedDiscount(item.exceedDiscount)) {
+    delete productExceedDiscountErrors[key]
+    return true
+  }
+  productExceedDiscountErrors[key] = '请输入大于 0 且不超过 10 的一位小数'
+  return false
+}
+
 function validFreeLimitCount(value: unknown) {
   const text = String(value ?? '').trim()
   if (!/^\d+$/.test(text)) return false
@@ -1624,8 +1661,7 @@
       ElMessage.warning('免费次数请输入 1 至 999 的整数,999 表示不限次数')
       return
     }
-    if (productRights.some((item) => item.free && !validExceedDiscount(item.exceedDiscount))) {
-      ElMessage.warning('超过次数折扣请输入大于 0 且不超过 10 的一位小数')
+    if (productRights.some((item) => item.free && !validateProductExceedDiscount(item))) {
       return
     }
   } else {
@@ -2036,6 +2072,12 @@
   }
 })
 
+watch(tabs, (availableTabs) => {
+  if (!availableTabs.some((item) => item.value === activeTab.value)) {
+    activeTab.value = 'settings'
+  }
+}, { immediate: true })
+
 watch(() => form.coverUrl, resetMerchantCoverPreview)
 watch(() => productForm.coverUrl, resetProductCoverPreview)
 watch(productValidPeriodRange, (value) => {
@@ -2061,6 +2103,10 @@
 watch(() => form.miniProgramJumpUrl, () => {
   if (isCarWashMerchant.value) syncCarWashJumpMeta()
 })
+
+// The create page is reused when the service type changes, so reset these
+// create-only defaults from the current route instead of retaining old values.
+watch(() => [route.query.mode, route.query.merchantType], applyNewMerchantDefaults, { immediate: true })
 
 onMounted(() => {
 	if (isEditMode.value && queryString('merchantId')) {
@@ -2116,16 +2162,16 @@
         <el-form ref="merchantFormRef" :model="form" :rules="merchantRules" class="merchant-settings-form" label-width="58px" label-position="left">
         <div class="merchant-create-content" :class="{ 'service-merchant-content': isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant }">
           <div class="merchant-create-left" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
-            <el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName">
+            <el-form-item class="merchant-form-row three-columns" label="商家名称" prop="merchantName" required>
               <el-input v-model="form.merchantName" />
             </el-form-item>
 
-            <el-form-item v-if="!isApartmentMerchant" class="merchant-form-row address-row" :label="isCarWashMerchant || isHealthCheckMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? '服务点' : '地址'" prop="address">
+            <el-form-item v-if="!isApartmentMerchant" class="merchant-form-row address-row" :label="isCarWashMerchant || isHealthCheckMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? '服务点' : '地址'" prop="address" required>
               <el-input v-model="form.address" />
               <el-button type="primary" @click="markLocation">地图标点</el-button>
             </el-form-item>
 
-            <el-form-item class="merchant-cover-row" label="封面图" prop="coverUrl">
+            <el-form-item class="merchant-cover-row" label="封面图" prop="coverUrl" required>
               <div class="merchant-cover-preview">
                 <img
                   v-if="form.coverUrl && !merchantCoverPreviewFailed"
@@ -2140,7 +2186,7 @@
           </div>
 
           <div v-if="!isServiceMerchant && !isApartmentMerchant && !isHotelMerchant && !isCarWashMerchant && !isHealthCheckMerchant && !isParkingMerchant && !isChildcareMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && !isApprovalMerchant" class="merchant-create-middle">
-            <el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile">
+            <el-form-item class="merchant-form-row" label="联系电话" prop="contactMobile" required>
               <el-input v-model="form.contactMobile" />
             </el-form-item>
 
@@ -2164,16 +2210,16 @@
           </div>
 
           <div class="merchant-create-right" :style="isApartmentMerchant || isFoodMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant ? 'flex: 1' : ''">
-            <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile">
+            <el-form-item v-if="isServiceMerchant || isApartmentMerchant || isHotelMerchant || isCarWashMerchant || isHealthCheckMerchant || isParkingMerchant || isChildcareMerchant || isFinancialMerchant || isMovieMerchant || isSpouseEmploymentMerchant || isApprovalMerchant" class="merchant-form-row service-contact-row" label="联系电话" prop="contactMobile" required>
               <el-input v-model="form.contactMobile" placeholder="请输入联系电话" />
             </el-form-item>
 
-            <div v-if="isEditMode" class="merchant-form-row">
+            <div v-if="hasExistingMerchant" class="merchant-form-row">
               <label>创建时间</label>
               <el-input v-model="form.createdAt" readonly />
             </div>
 
-            <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours">
+            <el-form-item v-if="!isApprovalMerchant" class="merchant-hours-block" label="营业时间" prop="businessHours" required>
               <div class="merchant-hours-list">
                 <div v-for="(item, index) in businessHours" :key="item.id" class="merchant-hour-row">
                   <el-time-picker
@@ -2206,7 +2252,7 @@
               <el-switch v-model="form.recommend" :active-value="1" :inactive-value="0" />
             </div>
 
-            <el-form-item class="merchant-form-row" label="排序" prop="sortNo">
+            <el-form-item class="merchant-form-row" label="排序" prop="sortNo" required>
               <el-input v-model="form.sortNo" placeholder="请输入" />
             </el-form-item>
           </div>
@@ -2673,17 +2719,17 @@
       </template>
 
       <template v-else>
-        <el-form ref="productFormRef" :model="productForm" :rules="productRules" class="merchant-product-edit-form" label-width="54px" label-position="left">
+        <el-form ref="productFormRef" :model="productForm" :rules="productRules" class="merchant-product-edit-form" label-width="68px" label-position="left">
         <section class="merchant-product-form-panel">
           <h3>商品信息</h3>
 
           <div class="product-form-grid">
             <div class="product-info-left">
-              <el-form-item class="product-form-row" label="商品名称" prop="productName">
+              <el-form-item class="product-form-row" label="商品名称" prop="productName" required>
                 <el-input v-model="productForm.productName" placeholder="请输入商品名称" />
               </el-form-item>
 
-              <el-form-item class="product-image-row" label="封面图" prop="coverUrl">
+              <el-form-item class="product-image-row" label="封面图" prop="coverUrl" required>
                 <div class="product-cover-preview">
                   <img
                     v-if="productForm.coverUrl && !productCoverPreviewFailed"
@@ -2697,7 +2743,7 @@
                 <el-button type="primary" :loading="imageUploading" @click="changeCover('productCover')">更换封面</el-button>
               </el-form-item>
 
-              <el-form-item v-if="!isHealthCheckMerchant" class="product-image-row compact" label="商品图片" prop="productImages">
+              <el-form-item v-if="!isHealthCheckMerchant" class="product-image-row compact" label="商品图片" prop="productImages" required>
                 <div class="product-image-list">
                   <div v-for="(image, index) in productImages" :key="image" class="product-thumb-preview">
                     <img :src="image" alt="商品图片" />
@@ -2725,7 +2771,7 @@
             </div>
 
             <div v-if="!isHealthCheckMerchant" class="product-info-middle">
-              <el-form-item class="product-form-row amount" :class="{ 'hotel-amount': isHotelMerchant }" label="商品金额" prop="salePrice">
+              <el-form-item class="product-form-row amount" :class="{ 'hotel-amount': isHotelMerchant }" label="商品金额" prop="salePrice" required>
                 <template v-if="isHotelMerchant">
                   <el-input-number
                     v-model="productForm.salePrice"
@@ -2772,7 +2818,7 @@
 
           </div>
 
-          <el-form-item v-if="!isHealthCheckMerchant" class="product-detail-row" label="商品详情" prop="detailText">
+          <el-form-item v-if="!isHealthCheckMerchant" class="product-detail-row" label="商品详情" prop="detailText" required>
             <AdminRichTextEditor :model-value="productForm.detailText || ''" class="merchant-product-rich-editor" placeholder="请输入商品详情" :height="260" @update:model-value="value => { productForm.detailText = value; productFormRef?.validateField('detailText') }" />
           </el-form-item>
 
@@ -2840,7 +2886,7 @@
             </div>
             <div class="product-notice-field">
               <label>其他规则</label>
-              <el-input v-model="productNoticeForm.otherRule" placeholder="请输入" />
+              <el-input v-model="productNoticeForm.otherRules" placeholder="请输入" />
             </div>
           </div>
         </section>
@@ -2877,28 +2923,35 @@
                   </el-select>
                 </template>
                 <template v-else>
-                  <span class="right-discount-label">折扣</span>
-                  <el-input v-model="item.discount" class="right-discount-input" placeholder="请输入" />
-                  <span class="right-unit">折</span>
+                  <div class="right-discount-control">
+                    <span class="right-discount-label">折扣</span>
+                    <el-input v-model="item.discount" class="right-discount-input" placeholder="请输入" />
+                    <span class="right-unit">折</span>
+                  </div>
                 </template>
                 <span class="right-subsidy-label">是否政府补贴</span>
                 <el-switch v-model="item.subsidy" class="right-subsidy-switch" />
               </div>
               <div v-if="item.free" class="product-right-sub">
-                <span>超过次数</span>
-                <el-input-number
-                  v-model="item.exceedDiscount"
-                  class="right-exceed-discount-input"
-                  :min="0.1"
-                  :max="10"
-                  :step="0.1"
-                  :precision="1"
-                  :controls="false"
-                  placeholder="请输入"
-                />
-                <span>折</span>
-                <span>是否政府补贴</span>
-                <el-switch v-model="item.exceedSubsidy" />
+                <span class="right-exceed-label">超过次数</span>
+                <div class="right-exceed-discount-field">
+                  <el-input-number
+                    v-model="item.exceedDiscount"
+                    class="right-exceed-discount-input"
+                    :min="0.1"
+                    :max="10"
+                    :step="0.1"
+                    :precision="1"
+                    :controls="false"
+                    placeholder="请输入"
+                    @change="validateProductExceedDiscount(item)"
+                    @blur="validateProductExceedDiscount(item)"
+                  />
+                  <span v-if="productExceedDiscountErrors[item.level]" class="field-error">{{ productExceedDiscountErrors[item.level] }}</span>
+                </div>
+                <span class="right-exceed-unit">折</span>
+                <span class="right-exceed-subsidy-label">是否政府补贴</span>
+                <el-switch v-model="item.exceedSubsidy" class="right-exceed-subsidy-switch" />
               </div>
             </div>
           </div>
@@ -3003,19 +3056,20 @@
 
         <template v-else>
           <el-form ref="couponFormRef" :model="couponEditForm" :rules="couponRules" class="merchant-coupon-edit-form" label-width="72px" label-position="left">
-            <el-form-item class="coupon-edit-row" label="优惠券名称" prop="couponName">
+            <el-form-item class="coupon-edit-row" label="优惠券名称" prop="couponName" required>
               <el-input v-model="couponEditForm.couponName" placeholder="请输入优惠券名称" />
             </el-form-item>
 
-            <el-form-item class="coupon-edit-row coupon-amount-row" label="优惠券金额" prop="couponAmount">
+            <el-form-item class="coupon-edit-row coupon-amount-row" label="优惠券金额" prop="couponAmount" required>
               <div class="coupon-amount-control"><el-input-number v-model="couponEditForm.couponAmount" :min="0.01" :precision="2" :step="1" controls-position="right" placeholder="请输入优惠券金额" /><span>元</span></div>
             </el-form-item>
 
-            <el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount">
+            <el-form-item class="coupon-edit-row coupon-requirement-row" label="使用要求" prop="fullAmount" :required="couponEditForm.useRequirement === 'full'">
               <el-radio-group v-model="couponEditForm.useRequirement">
                 <el-radio label="full" class="coupon-full-radio">满</el-radio>
-                <el-input v-model="couponEditForm.fullAmount" class="coupon-full-amount" />
-                <span class="coupon-full-unit">元可用</span>
+                <el-input v-model="couponEditForm.fullAmount" class="coupon-full-amount">
+                  <template #append>元可用</template>
+                </el-input>
                 <el-radio label="none" class="coupon-none-radio">不限</el-radio>
               </el-radio-group>
             </el-form-item>
@@ -3042,7 +3096,7 @@
               </div>
             </div>
 
-            <el-form-item class="coupon-edit-row" label="有效期" prop="validTime">
+            <el-form-item class="coupon-edit-row" label="有效期" prop="validTime" required>
               <el-date-picker
                 v-model="couponEditForm.validTime"
                 class="coupon-valid-time-picker"
@@ -3054,7 +3108,7 @@
               />
             </el-form-item>
 
-            <el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope">
+            <el-form-item class="coupon-edit-row coupon-product-scope-row" label="可用商品" prop="productScope" :required="couponEditForm.productScope === 'selected'">
               <el-radio-group v-model="couponEditForm.productScope">
                 <el-radio label="all">不限</el-radio>
                 <el-radio label="selected">选择</el-radio>
@@ -3113,10 +3167,10 @@
 
     <el-dialog v-model="schoolDialogVisible" :title="editingSchoolIndex >= 0 ? '编辑学校' : '新增学校'" width="820px" destroy-on-close :close-on-click-modal="false">
       <el-form ref="schoolFormRef" :model="editingSchool" :rules="schoolRules" label-width="100px" style="padding: 20px 20px 0;">
-        <el-form-item label="学校名称" prop="name">
+        <el-form-item label="学校名称" prop="name" required>
           <el-input v-model="editingSchool.name" placeholder="请输入" style="width: 320px" />
         </el-form-item>
-        <el-form-item label="地址" prop="address">
+        <el-form-item label="地址" prop="address" required>
           <el-input v-model="editingSchool.address" placeholder="请输入" style="width: 320px" />
         </el-form-item>
         <el-form-item label="定位点">
diff --git a/talent-card-admin/src/views/MerchantOnboardingDetailView.vue b/talent-card-admin/src/views/MerchantOnboardingDetailView.vue
index b8075ea..b801a4b 100644
--- a/talent-card-admin/src/views/MerchantOnboardingDetailView.vue
+++ b/talent-card-admin/src/views/MerchantOnboardingDetailView.vue
@@ -96,9 +96,9 @@
 .detail-panel h3 { margin: 0; color: #1d2129; font-size: 15px; }
 .status-value { margin-top: 16px; color: #1d2129; font-size: 13px; font-weight: 600; }
 .onboarding-heading { margin-top: 24px !important; }
-.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 46px; margin-top: 22px; max-width: 860px; }
+.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 46px; margin-top: 22px; max-width: 1032px; }
 .common-grid { margin-bottom: -2px; }
-.detail-field { display: grid; grid-template-columns: 118px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 32px; }
+.detail-field { display: grid; grid-template-columns: 144px minmax(0, 1fr); align-items: center; gap: 12px; min-height: 32px; }
 .detail-field > span { color: #303133; font-size: 13px; font-weight: 500; white-space: nowrap; }
 .detail-field > strong { min-height: 32px; box-sizing: border-box; padding: 7px 10px; color: #606266; font-size: 13px; font-weight: 400; line-height: 18px; word-break: break-all; background: #f5f7fa; }
 .placeholder-field { visibility: hidden; }
diff --git a/talent-card-admin/src/views/MerchantSettlementView.vue b/talent-card-admin/src/views/MerchantSettlementView.vue
index 78217af..9037cf3 100644
--- a/talent-card-admin/src/views/MerchantSettlementView.vue
+++ b/talent-card-admin/src/views/MerchantSettlementView.vue
@@ -1,14 +1,15 @@
 <script setup lang="ts">
-import { computed, onMounted, reactive, ref } from 'vue'
-import { useRouter } from 'vue-router'
+import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Refresh, Search } from '@element-plus/icons-vue'
 import { getMerchantSettlement, reviewMerchantSettlement } from '../api/admin'
 
 type Kind = 'onboardings' | 'bank-cards' | 'withdrawals'
 
-const kind = ref<Kind>('onboardings')
+const kind = ref<Kind>('withdrawals')
 const router = useRouter()
+const route = useRoute()
 const loading = ref(false)
 const rows = ref<Record<string, any>[]>([])
 const total = ref(0)
@@ -111,6 +112,13 @@
   load()
 }
 
+function applyRouteKind() {
+  const value = String(route.query.kind || '')
+  if (value === 'onboardings' || value === 'bank-cards' || value === 'withdrawals') {
+    kind.value = value
+  }
+}
+
 function openReview(row: Record<string, any>) {
   if (kind.value === 'onboardings') {
     router.push({ name: 'merchant-onboarding-detail', params: { id: row.id } })
@@ -152,7 +160,17 @@
   }
 }
 
-onMounted(load)
+watch(() => route.query.kind, () => {
+  applyRouteKind()
+  page.value = 1
+  filters.status = ''
+  load()
+})
+
+onMounted(() => {
+  applyRouteKind()
+  load()
+})
 </script>
 
 <template>
@@ -234,7 +252,7 @@
           <el-table-column prop="bankName" label="开户银行" min-width="145" show-overflow-tooltip />
           <el-table-column prop="branchName" label="开户支行" min-width="180" show-overflow-tooltip />
           <el-table-column prop="cityName" label="开户城市" min-width="120" show-overflow-tooltip />
-          <el-table-column prop="cardMasked" label="银行卡号" min-width="150" />
+          <el-table-column prop="cardNo" label="银行卡号" min-width="180" />
         </template>
         <el-table-column label="申请时间" min-width="165">
           <template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
diff --git a/talent-card-admin/src/views/MerchantView.vue b/talent-card-admin/src/views/MerchantView.vue
index f5657a0..1feb33d 100644
--- a/talent-card-admin/src/views/MerchantView.vue
+++ b/talent-card-admin/src/views/MerchantView.vue
@@ -167,7 +167,9 @@
   typeDialogVisible.value = false
   router.push({
     path: '/merchants/new',
-    query: { merchantType: selectedCreateType.value },
+    // Explicitly mark this as a create route so a previously opened edit page
+    // cannot leave its mode or data in the reused route component.
+    query: { mode: 'create', merchantType: selectedCreateType.value },
   })
 }
 
@@ -361,7 +363,7 @@
 
     <el-drawer v-model="drawerVisible" :title="drawerTitle" size="520px" @closed="resetForm">
       <el-form ref="formRef" :model="form" :rules="rules" label-width="96px" class="drawer-form">
-        <el-form-item label="商家名称" prop="merchantName">
+        <el-form-item label="商家名称" prop="merchantName" required>
           <el-input v-model="form.merchantName" placeholder="请输入商家名称" />
         </el-form-item>
         <el-form-item label="商家类型">
diff --git a/talent-card-admin/src/views/OrderDetailView.vue b/talent-card-admin/src/views/OrderDetailView.vue
index 20e52e7..2a84e81 100644
--- a/talent-card-admin/src/views/OrderDetailView.vue
+++ b/talent-card-admin/src/views/OrderDetailView.vue
@@ -53,10 +53,6 @@
   return value.verifyStatus === 1 ? '已核销' : '待核销'
 }
 
-function paymentStatus(value: string) {
-  return ({ pending: '待支付', processing: '支付中', paid: '支付成功', failed: '支付失败', closed: '已关闭' } as Record<string, string>)[value] || value || '-'
-}
-
 function previewImage(url: string) {
   if (!url) return
   previewUrl.value = url
@@ -197,7 +193,6 @@
             <div><span>支付方式</span><strong>{{ payment.provider === 'HUIFU' ? '微信支付' : payment.provider || '-' }}</strong></div>
             <div><span>支付流水号</span><strong>{{ payment.providerTransactionNo || payment.paymentNo || '-' }}</strong></div>
             <div><span>支付金额</span><strong class="amount-strong">{{ money(payment.amount) }}</strong></div>
-            <div><span>支付状态</span><strong>{{ paymentStatus(payment.status) }}</strong></div>
           </div>
         </div>
         <div v-else class="detail-empty">暂无支付流水</div>
diff --git a/talent-card-admin/src/views/OrderView.vue b/talent-card-admin/src/views/OrderView.vue
index 339674b..d2a8b8b 100644
--- a/talent-card-admin/src/views/OrderView.vue
+++ b/talent-card-admin/src/views/OrderView.vue
@@ -12,7 +12,7 @@
 const orders = ref<Order[]>([])
 const router = useRouter()
 
-const cardLevels = ['钻石卡', '卓越人才卡', '精英人才卡', '青年人才卡']
+const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
 
 const filters = reactive({
   talentKeyword: '',
@@ -189,7 +189,7 @@
             <el-option label="已退款" :value="2" />
           </el-select>
         </el-form-item>
-        <el-form-item v-if="isGroupOrder" label="是否家属下单">
+        <el-form-item v-if="isGroupOrder" class="family-order-filter" label="是否家属下单">
           <el-select v-model="filters.familyOrder" placeholder="全部" clearable>
             <el-option label="全部" :value="undefined" />
             <el-option label="是" :value="1" />
diff --git a/talent-card-admin/src/views/ProductAuditDetailView.vue b/talent-card-admin/src/views/ProductAuditDetailView.vue
index 7002be4..a3a8d8a 100644
--- a/talent-card-admin/src/views/ProductAuditDetailView.vue
+++ b/talent-card-admin/src/views/ProductAuditDetailView.vue
@@ -74,10 +74,10 @@
 
   try {
     const parsed = typeof value === 'string' ? JSON.parse(value) : value
-    notice.validPeriod = parsed.validPeriod || parsed.valid_period || ''
-    notice.useMethod = parsed.useMethod || parsed.use_method || ''
+    notice.validPeriod = parsed.validPeriod || parsed.valid_period || parsed.validity || ''
+    notice.useMethod = parsed.useMethod || parsed.use_method || parsed.usage || ''
     notice.availableTime = parsed.availableTime || parsed.available_time || ''
-    notice.otherRules = parsed.otherRules || parsed.other_rules || ''
+    notice.otherRules = parsed.otherRules || parsed.other_rules || parsed.otherRule || parsed.other_rule || ''
   } catch {
     notice.otherRules = typeof value === 'string' ? value : ''
   }
diff --git a/talent-card-admin/src/views/RefundRecordView.vue b/talent-card-admin/src/views/RefundRecordView.vue
index 4628515..f77665c 100644
--- a/talent-card-admin/src/views/RefundRecordView.vue
+++ b/talent-card-admin/src/views/RefundRecordView.vue
@@ -12,7 +12,7 @@
 const loading = ref(false)
 const rows = ref<RefundRecord[]>([])
 
-const cardLevels = ['钻石卡', '卓越人才卡', '精英人才卡', '青年人才卡']
+const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
 const itemNameLabel = computed(() => (activeTab.value === 'GROUP' ? '团购名称' : '消费项目'))
 
 const filters = reactive({
diff --git a/talent-card-admin/src/views/RoleFormView.vue b/talent-card-admin/src/views/RoleFormView.vue
index dd76e17..098c390 100644
--- a/talent-card-admin/src/views/RoleFormView.vue
+++ b/talent-card-admin/src/views/RoleFormView.vue
@@ -10,26 +10,78 @@
 type Module = { name: string; pages: Page[] }
 const modules: Module[] = [
   { name: '首页', pages: [{ code: 'dashboard', name: '首页', actions: ['查看'] }] },
-  { name: '账户权限管理', pages: [{ code: 'staff', name: '账号管理', actions: ['查看', '新增', '编辑', '启用/禁用', '删除', '重置密码'] }] },
-  { name: '角色管理', pages: [{ code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '删除'] }] },
-  { name: '持卡管理', pages: [{ code: 'talents', name: '持卡人管理', actions: ['查看', '新增', '编辑', '启用/禁用'] }] },
-  { name: '家属卡管理', pages: [{ code: 'family-cards', name: '家属卡管理', actions: ['查看', '审核'] }] },
-  { name: '商家管理', pages: [{ code: 'merchants', name: '商家查询', actions: ['查看', '新增', '编辑'] }] },
-  { name: '团单审核', pages: [{ code: 'products', name: '团单审核', actions: ['查看', '审核'] }] },
-  { name: '订单管理', pages: [{ code: 'orders', name: '订单列表', actions: ['查看', '退款处理'] }] },
-  { name: '退款记录', pages: [{ code: 'refunds', name: '退款记录', actions: ['查看'] }] },
-  { name: '预约管理', pages: [{ code: 'medical-reservations', name: '体检预约管理', actions: ['查看', '审核'] }] },
-  { name: '停车车牌管理', pages: [{ code: 'parking-plates', name: '停车车牌管理', actions: ['查看'] }] },
-  { name: '权益图片管理', pages: [{ code: 'equity-images', name: '权益图片管理', actions: ['查看', '编辑'] }] },
-  { name: '人才资讯管理', pages: [{ code: 'news', name: '人才资讯管理', actions: ['查看', '新增', '编辑'] }] },
-  { name: '系统管理', pages: [{ code: 'settings', name: '平台设置', actions: ['查看', '编辑'] }] },
+  {
+    name: '持卡管理',
+    pages: [
+      { code: 'talents', name: '持卡人管理', actions: ['查看', '新增', '编辑', '启用/停用'] },
+      { code: 'family-card-applications', name: '家属卡申请', actions: ['查看', '审核'] },
+      { code: 'upgrade-applications', name: '升级申请', actions: ['查看', '审核'] },
+      { code: 'family-cards', name: '家属卡管理', actions: ['查看', '启用/停用'] },
+      { code: 'child-cards', name: '子女管理', actions: ['查看', '审核'] },
+    ],
+  },
+  {
+    name: '商家管理',
+    pages: [
+      { code: 'merchants', name: '商家查询', actions: ['查看', '新增', '编辑', '启用/停用'] },
+      { code: 'products', name: '团单审核', actions: ['查看', '审核'] },
+    ],
+  },
+  {
+    name: '订单管理',
+    pages: [
+      { code: 'orders', name: '订单列表', actions: ['查看'] },
+      { code: 'refunds', name: '退款记录', actions: ['查看'] },
+      { code: 'hotel-free-verifications', name: '酒店免费核销记录', actions: ['查看'] },
+    ],
+  },
+  {
+    name: '预约管理',
+    pages: [
+      { code: 'appointment-management', name: '预约服务管理', actions: ['查看', '审核'] },
+      { code: 'medical-reservations', name: '体检预约管理', actions: ['查看', '审核'] },
+      { code: 'medical-applications', name: '就医申请管理', actions: ['查看', '审核'] },
+      { code: 'parking-plates', name: '停车车牌管理', actions: ['查看', '审核'] },
+      { code: 'car-wash-benefits', name: '洗车权益管理', actions: ['查看', '审核'] },
+      { code: 'fuel-services', name: '加油服务管理', actions: ['查看', '审核'] },
+      { code: 'spouse-employment', name: '配偶就业管理', actions: ['查看', '审核'] },
+      { code: 'hotel-reservations', name: '酒店预约管理', actions: ['查看', '审核'] },
+    ],
+  },
+  { name: '财务管理', pages: [{ code: 'merchant-settlement', name: '商户结算审核', actions: ['查看', '审核'] }] },
+  {
+    name: '运营管理',
+    pages: [
+      { code: 'equity-images', name: '权益图片管理', actions: ['查看', '编辑'] },
+      { code: 'news', name: '人才资讯管理', actions: ['查看', '新增', '编辑', '删除'] },
+      { code: 'policies', name: '人才政策管理', actions: ['查看', '新增', '编辑', '删除'] },
+      { code: 'movie-tickets', name: '电影票管理', actions: ['查看', '新增', '编辑', '审核'] },
+    ],
+  },
+  {
+    name: '系统管理',
+    pages: [
+      { code: 'settings', name: '平台设置', actions: ['查看', '编辑'] },
+      { code: 'protocol-settings', name: '协议设置', actions: ['查看', '编辑'] },
+      { code: 'roles', name: '角色管理', actions: ['查看', '新增', '编辑', '删除'] },
+      { code: 'staff', name: '员工管理', actions: ['查看', '新增', '编辑', '启用/停用', '删除', '重置密码'] },
+    ],
+  },
 ]
 const route = useRoute(), router = useRouter(), saving = ref(false)
 const id = computed(() => route.params.id ? String(route.params.id) : '')
 const form = reactive({ roleName: '', remark: '', status: 1, permissionCodes: [] as string[] })
 const formRef = ref<FormInstance>()
 const rules: FormRules = { roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }] }
-const code = (page: string, action: string) => `admin.${page}.${action === '启用/禁用' ? 'disable' : action === '重置密码' ? 'reset' : action === '退款处理' ? 'refund' : action === '查看' ? 'view' : action === '新增' ? 'create' : action === '编辑' ? 'edit' : action === '删除' ? 'delete' : 'audit'}`
+const code = (page: string, action: string) => `admin.${page}.${({
+  '查看': 'view',
+  '新增': 'create',
+  '编辑': 'edit',
+  '启用/停用': 'disable',
+  '删除': 'delete',
+  '重置密码': 'reset',
+  '审核': 'audit',
+} as Record<string, string>)[action] || 'view'}`
 const pageCodes = (page: Page) => page.actions.map((action) => code(page.code, action))
 const moduleCodes = (module: Module) => module.pages.flatMap(pageCodes)
 const checked = (values: string[]) => values.every((value) => form.permissionCodes.includes(value))
diff --git a/talent-card-admin/src/views/StaffManagementView.vue b/talent-card-admin/src/views/StaffManagementView.vue
index d122626..236067d 100644
--- a/talent-card-admin/src/views/StaffManagementView.vue
+++ b/talent-card-admin/src/views/StaffManagementView.vue
@@ -5,6 +5,7 @@
 import { Plus, Refresh, Search } from "@element-plus/icons-vue";
 import {
   createAdminStaff,
+  checkAdminStaffAccount,
   deleteAdminStaff,
   getAdminRoles,
   getAdminStaff,
@@ -31,6 +32,7 @@
   status: 1,
 });
 const formRef = ref<FormInstance>();
+const accountTaken = ref(false);
 const accountPattern = /^[A-Za-z0-9]+$/;
 const rules = computed<FormRules>(() => ({
   displayName: [{ required: true, message: "请输入人员名称", trigger: "blur" }],
@@ -50,9 +52,11 @@
   password: [
     {
       validator: (_rule, value, callback) => {
-        if (editId.value) return callback();
-        if (!value) return callback(new Error("请输入登录密码"));
-        if (String(value).length < 6) return callback(new Error("新建员工密码至少 6 位"));
+        if (!value) {
+          if (editId.value) return callback();
+          return callback(new Error("请输入登录密码"));
+        }
+        if (String(value).length < 6) return callback(new Error("密码至少 6 位"));
         callback();
       },
       trigger: "blur",
@@ -61,7 +65,10 @@
   confirmPassword: [
     {
       validator: (_rule, value, callback) => {
-        if (editId.value) return callback();
+        if (!form.password) {
+          if (editId.value) return callback();
+          return callback(new Error("请输入确认密码"));
+        }
         if (!value) return callback(new Error("请输入确认密码"));
         if (value !== form.password) return callback(new Error("两次输入的密码不一致"));
         callback();
@@ -101,6 +108,15 @@
 const keepMobileDigits = () => {
   form.mobile = form.mobile.replace(/\D/g, "").slice(0, 11);
 };
+const normalizeAccount = () => {
+  form.account = form.account.replace(/[^A-Za-z0-9]/g, "").slice(0, 32);
+  accountTaken.value = false;
+};
+async function checkAccountAvailable() {
+  if (editId.value || !accountPattern.test(form.account.trim())) return;
+  const result = await checkAdminStaffAccount(form.account.trim());
+  accountTaken.value = !result.available;
+}
 async function load() {
   loading.value = true;
   try {
@@ -117,6 +133,7 @@
 }
 function create() {
   editId.value = undefined;
+  accountTaken.value = false;
   Object.assign(form, {
     displayName: "",
     mobile: "",
@@ -132,6 +149,7 @@
 }
 function edit(row: AdminStaff) {
   editId.value = row.id;
+  accountTaken.value = false;
   Object.assign(form, {
     displayName: row.displayName,
     mobile: row.mobile || "",
@@ -148,6 +166,10 @@
 async function save() {
   const valid = await formRef.value?.validate().catch(() => false);
   if (!valid) return;
+  if (!editId.value) {
+    await checkAccountAvailable();
+    if (accountTaken.value) return;
+  }
   saving.value = true;
   try {
     const { confirmPassword, ...formPayload } = form;
@@ -267,7 +289,7 @@
           }}</template></el-table-column
         ><el-table-column label="操作" width="225" fixed="right"
           ><template #default="{ row }"
-            ><el-button link type="primary" @click="edit(row)">编辑</el-button
+            ><el-button v-if="!isDefaultAdmin(row)" link type="primary" @click="edit(row)">编辑</el-button
             ><el-button
               v-if="!isDefaultAdmin(row)"
               link
@@ -304,7 +326,7 @@
       destroy-on-close
       :close-on-click-modal="false"
       ><el-form ref="formRef" :model="form" :rules="rules" label-width="92px"
-        ><el-form-item label="人员名称" prop="displayName"
+        ><el-form-item label="人员名称" prop="displayName" required
           ><el-input v-model="form.displayName" /></el-form-item
         ><el-form-item label="联系电话"
           ><el-input
@@ -312,13 +334,14 @@
             inputmode="numeric"
             maxlength="11"
             @input="keepMobileDigits" /></el-form-item
-        ><el-form-item label="登录账号" prop="account"
+        ><el-form-item label="登录账号" prop="account" required :error="accountTaken ? '登录账号已存在,请更换' : ''"
           ><el-input
             v-model="form.account"
             :disabled="Boolean(editId)"
             maxlength="32"
             placeholder="仅支持英文和数字"
-            /></el-form-item
+            @input="normalizeAccount"
+            @blur="checkAccountAvailable" /></el-form-item
         ><el-form-item label="登录密码" prop="password" :required="!editId"
           ><el-input
             v-model="form.password"
@@ -326,19 +349,20 @@
             :placeholder="
               editId ? '留空则不修改' : '至少 6 位'
             " /></el-form-item
-        ><el-form-item v-if="!editId" label="确认密码" prop="confirmPassword" required
+        ><el-form-item v-if="!editId || form.password" label="确认密码" prop="confirmPassword" :required="!editId || Boolean(form.password)"
           ><el-input
             v-model="form.confirmPassword"
             type="password"
+            show-password
             placeholder="请再次输入密码" /></el-form-item
-        ><el-form-item label="角色" prop="roleCode"
+        ><el-form-item label="角色" prop="roleCode" required
           ><el-select v-model="form.roleCode" class="full"
             ><el-option
               v-for="role in roles"
               :key="role.id"
               :label="role.roleName"
               :value="role.roleCode" /></el-select></el-form-item
-        ><el-form-item label="职务" prop="positionName"
+        ><el-form-item label="职务" prop="positionName" required
           ><el-input v-model="form.positionName" /></el-form-item
         ><el-form-item label="客服微信"
           ><el-input v-model="form.customerWechat" /></el-form-item
@@ -363,8 +387,6 @@
 .filters {
   margin: 14px 20px 0;
   padding: 16px;
-  border: 1px solid #dce3ea;
-  border-radius: 4px;
 }
 .filters :deep(.el-input),
 .filters :deep(.el-select) {
diff --git a/talent-card-admin/src/views/TalentCreateView.vue b/talent-card-admin/src/views/TalentCreateView.vue
index c8a4f86..e540e8d 100644
--- a/talent-card-admin/src/views/TalentCreateView.vue
+++ b/talent-card-admin/src/views/TalentCreateView.vue
@@ -128,7 +128,7 @@
   <div class="talent-detail-page talent-create-page">
     <el-form ref="formRef" :model="form" :rules="rules" label-width="104px">
     <section class="detail-block create-mobile-summary">
-      <el-form-item label="手机号码:" prop="mobile">
+      <el-form-item label="手机号码:" prop="mobile" required>
           <el-input
             v-model="form.mobile"
             placeholder="请输入"
@@ -143,10 +143,10 @@
       <h3>持卡人认证</h3>
       <div class="identity-edit-card">
         <div class="identity-edit-form">
-          <el-form-item label="持卡人姓名:" prop="name">
+          <el-form-item label="持卡人姓名:" prop="name" required>
             <el-input v-model="form.name" placeholder="请输入" />
           </el-form-item>
-          <el-form-item label="身份证号:" prop="idCardNo">
+          <el-form-item label="身份证号:" prop="idCardNo" required>
             <el-input
               v-model="form.idCardNo"
               placeholder="请输入18位身份证号"
@@ -154,7 +154,7 @@
               @input="normalizeIdCardNo"
             />
           </el-form-item>
-          <el-form-item label="联系方式:" prop="mobile">
+          <el-form-item label="联系方式:" prop="mobile" required>
             <el-input
               v-model="form.mobile"
               placeholder="请输入"
@@ -163,13 +163,13 @@
               @input="normalizeMobile"
             />
           </el-form-item>
-          <el-form-item label="联系地址:" prop="contactAddress">
+          <el-form-item label="联系地址:" prop="contactAddress" required>
             <el-input v-model="form.contactAddress" placeholder="请输入" />
           </el-form-item>
-          <el-form-item label="工作地址:" prop="workAddress">
+          <el-form-item label="工作地址:" prop="workAddress" required>
             <el-input v-model="form.workAddress" placeholder="请输入" />
           </el-form-item>
-          <el-form-item label="性别:" prop="gender">
+          <el-form-item label="性别:" prop="gender" required>
             <el-select v-model="form.gender" placeholder="选择">
               <el-option label="男" :value="1" />
               <el-option label="女" :value="2" />
@@ -181,7 +181,7 @@
 
     <section class="detail-block">
       <h3>人才卡管理</h3>
-      <el-form-item label="人才等级/卡号" prop="talentCardNo">
+      <el-form-item label="人才等级/卡号" prop="talentCardNo" required>
         <div class="card-manage-panel">
           <div class="talent-card-stack">
             <button type="button" class="talent-card-preview create-card-preview" @click="openCardDialog">
@@ -198,7 +198,7 @@
 
     <section class="detail-block">
       <h3>专属服务人员</h3>
-      <el-form-item label="专属服务人员" prop="serviceStaff" class="service-staff-form-item">
+      <el-form-item label="专属服务人员" prop="serviceStaff" class="service-staff-form-item" required>
       <div class="service-staff-panel">
         <el-select v-model="serviceStaff" clearable placeholder="请选择专属服务人员" @change="formRef?.validateField('serviceStaff')">
           <el-option
@@ -224,7 +224,7 @@
       :close-on-click-modal="false"
     >
       <el-form ref="cardFormRef" :model="cardForm" :rules="cardRules" label-width="100px">
-      <el-form-item label="人才等级" prop="talentLevel">
+      <el-form-item label="人才等级" prop="talentLevel" required>
       <div class="talent-level-options">
         <button
           v-for="level in levels"
@@ -245,7 +245,7 @@
       </el-radio-group>
 
       <div class="talent-card-number-field">
-        <el-form-item label="人才卡号" prop="talentCardNo">
+        <el-form-item label="人才卡号" prop="talentCardNo" required>
         <el-input
           v-model="cardForm.talentCardNo"
           maxlength="64"
diff --git a/talent-card-admin/src/views/TalentDetailView.vue b/talent-card-admin/src/views/TalentDetailView.vue
index 9862536..50af126 100644
--- a/talent-card-admin/src/views/TalentDetailView.vue
+++ b/talent-card-admin/src/views/TalentDetailView.vue
@@ -339,7 +339,7 @@
       :close-on-click-modal="false"
     >
       <el-form ref="levelFormRef" :model="{ talentLevel: pendingTalentLevel }" :rules="levelRules" label-width="88px">
-      <el-form-item label="人才等级" prop="talentLevel">
+      <el-form-item label="人才等级" prop="talentLevel" required>
       <div class="talent-level-options">
         <button
           v-for="level in talentLevels"
diff --git a/talent-card-admin/src/views/TalentNewsEditView.vue b/talent-card-admin/src/views/TalentNewsEditView.vue
index 77d9b0a..711b18f 100644
--- a/talent-card-admin/src/views/TalentNewsEditView.vue
+++ b/talent-card-admin/src/views/TalentNewsEditView.vue
@@ -128,16 +128,16 @@
     <section class="news-publish-panel">
       <h3>资讯内容</h3>
 
-      <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="72px" label-position="left">
-        <el-form-item label="资讯标题" prop="title">
+      <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="96px" label-position="left">
+        <el-form-item label="资讯标题" prop="title" required>
           <el-input v-model="form.title" placeholder="资讯标题" />
         </el-form-item>
 
-        <el-form-item label="资讯标签" prop="summary">
-          <el-input v-model="form.summary" placeholder="请输入资讯标签" />
+        <el-form-item label="资讯标签" prop="summary" required>
+          <el-input v-model="form.summary" placeholder="资讯标签" />
         </el-form-item>
 
-        <el-form-item class="news-editor-form-item" label="资讯内容" prop="content">
+        <el-form-item class="news-editor-form-item" label="资讯内容" prop="content" required>
           <AdminRichTextEditor v-model="form.content" placeholder="请输入资讯内容" @update:model-value="validateContent" />
         </el-form-item>
 
@@ -145,11 +145,11 @@
           <el-switch v-model="form.pinned" />
         </el-form-item>
 
-        <el-form-item label="封面图" prop="coverUrl">
-          <el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
-            <el-button>上传图片</el-button>
+        <el-form-item label="封面图" prop="coverUrl" required>
+          <el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
+            <el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" />
+            <div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div>
           </el-upload>
-          <el-image v-if="form.coverUrl" class="news-cover-preview" :src="form.coverUrl" fit="cover" />
         </el-form-item>
 
         <el-form-item>
diff --git a/talent-card-admin/src/views/TalentNewsPublishView.vue b/talent-card-admin/src/views/TalentNewsPublishView.vue
index 16d31d5..5de2827 100644
--- a/talent-card-admin/src/views/TalentNewsPublishView.vue
+++ b/talent-card-admin/src/views/TalentNewsPublishView.vue
@@ -81,16 +81,16 @@
     <section class="news-publish-panel">
       <h3>资讯内容</h3>
 
-      <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="72px" label-position="left">
-        <el-form-item label="资讯标题" prop="title">
+      <el-form ref="formRef" class="news-publish-form" :model="form" :rules="rules" label-width="96px" label-position="left">
+        <el-form-item label="资讯标题" prop="title" required>
           <el-input v-model="form.title" placeholder="资讯标题" />
         </el-form-item>
 
-        <el-form-item label="资讯标签" prop="summary">
-          <el-input v-model="form.summary" placeholder="请输入资讯标签" />
+        <el-form-item label="资讯标签" prop="summary" required>
+          <el-input v-model="form.summary" placeholder="资讯标签" />
         </el-form-item>
 
-        <el-form-item class="news-editor-form-item" label="资讯内容" prop="content">
+        <el-form-item class="news-editor-form-item" label="资讯内容" prop="content" required>
           <AdminRichTextEditor v-model="form.content" placeholder="请输入资讯内容" @update:model-value="validateContent" />
         </el-form-item>
 
@@ -98,11 +98,11 @@
           <el-switch v-model="form.pinned" />
         </el-form-item>
 
-        <el-form-item label="封面图" prop="coverUrl">
-          <el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
-            <el-button>上传图片</el-button>
+        <el-form-item label="封面图" prop="coverUrl" required>
+          <el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
+            <el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" />
+            <div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div>
           </el-upload>
-          <el-image v-if="form.coverUrl" class="news-cover-preview" :src="form.coverUrl" fit="cover" />
         </el-form-item>
 
         <el-form-item>
diff --git a/talent-card-admin/src/views/TalentPolicyEditView.vue b/talent-card-admin/src/views/TalentPolicyEditView.vue
index c094798..71db82b 100644
--- a/talent-card-admin/src/views/TalentPolicyEditView.vue
+++ b/talent-card-admin/src/views/TalentPolicyEditView.vue
@@ -41,6 +41,6 @@
 </script>
 <template>
   <div class="policy-edit-page" v-loading="loading"><section class="news-meta-panel"><h3>发布信息</h3><div class="news-meta-grid"><div><span>发布时间</span><strong>{{ createdAt }}</strong></div><div><span>修改时间</span><strong>{{ updatedAt }}</strong></div></div></section><section class="policy-publish-panel"><h3>政策内容</h3><el-form ref="formRef" class="policy-publish-form" :model="form" :rules="rules" label-width="88px">
-    <el-form-item label="政策标题" prop="title"><el-input v-model="form.title" /></el-form-item><el-form-item label="政策标签" prop="subtitle"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item><el-form-item class="news-editor-form-item" label="政策内容" prop="content"><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item><el-form-item label="封面图" prop="coverUrl"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item><el-form-item label="排序" prop="sortNo"><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
+    <el-form-item label="政策标题" prop="title"><el-input v-model="form.title" /></el-form-item><el-form-item label="政策标签" prop="subtitle"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item><el-form-item class="news-editor-form-item" label="政策内容" prop="content"><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item><el-form-item label="封面图" prop="coverUrl"><el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" /><div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div></el-upload></el-form-item><el-form-item label="排序" prop="sortNo"><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item><el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
   </el-form></section></div>
 </template>
diff --git a/talent-card-admin/src/views/TalentPolicyPublishView.vue b/talent-card-admin/src/views/TalentPolicyPublishView.vue
index dc6ed4e..1eb13eb 100644
--- a/talent-card-admin/src/views/TalentPolicyPublishView.vue
+++ b/talent-card-admin/src/views/TalentPolicyPublishView.vue
@@ -51,11 +51,11 @@
 </script>
 <template>
   <div class="policy-publish-page"><section class="policy-publish-panel"><h3>政策内容</h3><el-form ref="formRef" class="policy-publish-form" :model="form" :rules="rules" label-width="88px">
-    <el-form-item label="政策标题" prop="title"><el-input v-model="form.title" placeholder="请输入政策标题" /></el-form-item>
-    <el-form-item label="政策标签" prop="subtitle"><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item>
-    <el-form-item class="news-editor-form-item" label="政策内容" prop="content"><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item>
-    <el-form-item label="封面图" prop="coverUrl"><el-upload accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-button>上传图片</el-button></el-upload><el-image v-if="form.coverUrl" class="policy-cover-preview" :src="form.coverUrl" fit="cover" /></el-form-item>
-    <el-form-item label="排序" prop="sortNo"><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item>
+    <el-form-item label="政策标题" prop="title" required><el-input v-model="form.title" placeholder="请输入政策标题" /></el-form-item>
+    <el-form-item label="政策标签" prop="subtitle" required><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item>
+    <el-form-item class="news-editor-form-item" label="政策内容" prop="content" required><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item>
+    <el-form-item label="封面图" prop="coverUrl" required><el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" /><div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div></el-upload></el-form-item>
+    <el-form-item label="排序" prop="sortNo" required><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item>
     <el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
   </el-form></section></div>
 </template>
diff --git a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
index b5d3f80..aedde8c 100644
--- a/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
+++ b/talent-card-admin/src/views/UpgradeApplicationDetailView.vue
@@ -90,10 +90,10 @@
         <div>申请时间:<strong>{{ formatDate(detail.applyTime) }}</strong></div>
         <div class="full-line">申请理由:<strong>{{ text(detail.applyReason) }}</strong></div>
       </div>
-      <div v-for="honor in (detail.honors || [])" :key="honor.id" class="honor-block">
+      <div v-for="honor in (detail.honors || [])" :key="honor.id" class="upgrade-detail-grid honor-block">
         <div>新获得的荣誉称号/大奖项:<strong>{{ text(honor.honorName) }}</strong></div>
         <div>获得时间:<strong>{{ text(honor.obtainedDate) }}</strong></div>
-        <div class="honor-images">
+        <div class="honor-images full-line">
           <el-image
             v-if="imageUrl(honor)"
             :src="resolveImageUrl(imageUrl(honor))"
@@ -125,8 +125,8 @@
 .upgrade-detail-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 36px; padding: 18px; background: #f5f6f8; color: #606266; }
 .upgrade-detail-grid strong { color: #303133; font-weight: 500; }
 .full-line { grid-column: 1 / -1; }
-.honor-block { margin-top: 14px; padding: 16px; background: #f5f6f8; color: #606266; }
-.honor-images { display: flex; gap: 20px; min-height: 130px; margin-top: 14px; }
+.honor-block { margin-top: 14px; }
+.honor-images { display: flex; gap: 20px; align-items: flex-start; }
 .honor-image { width: 150px; height: 110px; background: #e9eaec; cursor: zoom-in; }
 .honor-images span { display: flex; align-items: center; justify-content: center; width: 150px; height: 110px; color: #909399; background: #e9eaec; }
 .upgrade-detail-actions { display: flex; align-items: center; gap: 12px; padding: 18px 0; }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java
index 27deace..f032d4b 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessController.java
@@ -36,6 +36,11 @@
 		return ApiResponse.success(adminBusinessService.dashboard());
 	}
 
+	@GetMapping("/pending-notices")
+	public ApiResponse<java.util.List<Map<String, Object>>> pendingNotices() {
+		return ApiResponse.success(adminBusinessService.pendingNotices());
+	}
+
 	@GetMapping("/news")
 	public ApiResponse<PageResult<Map<String, Object>>> news(
 			@RequestParam(required = false) String title,
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 f18842c..46f596c 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
@@ -2,9 +2,12 @@
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
 import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -228,21 +231,7 @@
 		StringBuilder sql = new StringBuilder("""
 				select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile,
 				       m.logo_url, m.cover_url, m.address, m.is_recommend, m.sort_no, m.status,
-				       case
-				         when m.status <> 1 then '休息中'
-				         when lower(coalesce(m.business_status, 'open')) in ('closed', '休息中') then '休息中'
-				         when exists (
-				           select 1
-				           from merchant_business_hours h
-				           join merchant_store s on s.id = h.store_id and s.status = 1 and s.deleted = 0
-				           where h.merchant_id = m.id
-				             and (
-				               (h.start_time <= h.end_time and current_time() >= h.start_time and current_time() < h.end_time)
-				               or (h.start_time > h.end_time and (current_time() >= h.start_time or current_time() < h.end_time))
-				             )
-				         ) then '营业中'
-				         else '休息中'
-				       end as business_status, m.created_at,
+				       m.created_at,
 				       (select count(*) from merchant_product p where p.merchant_id = m.id and p.deleted = 0) as product_count,
 				       (select count(*) from benefit_order o where o.merchant_id = m.id and o.deleted = 0) as order_count,
 				       0 as reservation_count
@@ -290,7 +279,45 @@
 			args.add(createdEnd);
 		}
 		sql.append(" order by m.is_recommend desc, m.sort_no desc, m.id desc");
-		return page(sql.toString(), countSql.toString(), page, size, args.toArray());
+		PageResult<Map<String, Object>> result = page(sql.toString(), countSql.toString(), page, size, args.toArray());
+		applyBusinessStatus(result.records());
+		return result;
+	}
+
+	@Transactional(readOnly = true)
+	public List<Map<String, Object>> pendingNotices() {
+		List<Map<String, Object>> notices = new ArrayList<>();
+		addPendingNotice(notices, "product-audit", "团单审核", "您有 %d 条团单待审核", "/products",
+				"select count(*) as pending_count, max(coalesce(submit_time, created_at)) as latest_time from merchant_product where deleted = 0 and audit_status = 1");
+		addPendingNotice(notices, "family-card", "家属卡申请", "您有 %d 条家属卡申请待审核", "/family-card-applications",
+				"select count(*) as pending_count, max(apply_time) as latest_time from family_card_application where deleted = 0 and status = '待审核'");
+		addPendingNotice(notices, "upgrade", "升级申请", "您有 %d 条升级申请待审核", "/upgrade-applications",
+				"select count(*) as pending_count, max(apply_time) as latest_time from upgrade_application where deleted = 0 and status = '待审核'");
+		addPendingNotice(notices, "merchant-onboarding", "商户进件", "您有 %d 条商户进件待审核", "/merchant-settlement?kind=onboardings",
+				"select count(*) as pending_count, max(created_at) as latest_time from merchant_onboarding_application where status = 'pending'");
+		addPendingNotice(notices, "merchant-bank-card", "银行卡申请", "您有 %d 条银行卡申请待审核", "/merchant-settlement?kind=bank-cards",
+				"select count(*) as pending_count, max(created_at) as latest_time from merchant_bank_card where status = 'pending'");
+		addPendingNotice(notices, "merchant-withdrawal", "提现审核", "您有 %d 条提现申请待审核", "/merchant-settlement?kind=withdrawals",
+				"select count(*) as pending_count, max(created_at) as latest_time from merchant_withdrawal where status = 'pending'");
+		addPendingNotice(notices, "medical-reservation", "体检预约", "您有 %d 条体检预约待处理", "/medical-reservations",
+				"select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_RESERVATION' and status = '待处理'");
+		addPendingNotice(notices, "medical-application", "就医申请", "您有 %d 条就医申请待处理", "/medical-applications",
+				"select count(*) as pending_count, max(submitted_at) as latest_time from service_application where deleted = 0 and application_type = 'MEDICAL_APPLICATION' and status = '待处理'");
+		notices.sort((left, right) -> String.valueOf(right.get("time")).compareTo(String.valueOf(left.get("time"))));
+		return notices;
+	}
+
+	private void addPendingNotice(List<Map<String, Object>> notices, String id, String category, String template, String path, String sql) {
+		Map<String, Object> summary = DatabaseRows.normalize(jdbcTemplate.queryForMap(sql));
+		long count = summary.get("pendingCount") instanceof Number number ? number.longValue() : 0;
+		if (count == 0) return;
+		Map<String, Object> notice = new LinkedHashMap<>();
+		notice.put("id", id);
+		notice.put("category", category);
+		notice.put("content", template.formatted(count));
+		notice.put("time", summary.get("latestTime"));
+		notice.put("path", path);
+		notices.add(notice);
 	}
 
 	@Transactional(readOnly = true)
@@ -335,7 +362,7 @@
 	@Transactional
 	public Map<String, Object> createMerchant(MerchantCreateRequest request) {
 		validateMerchantAddress(request.merchantType(), request.address());
-		validateScenicMerchantCreate(request);
+		validateRequiredMerchantCreateFields(request);
 		jdbcTemplate.update("""
 				insert into merchant(merchant_name, merchant_type, contact_name, contact_mobile, address,
 					                     logo_url, cover_url, business_status, longitude, latitude, can_login_merchant_app, group_buying_enabled,
@@ -396,8 +423,8 @@
 		return MERCHANT_APP_LOGIN_TYPES.contains(merchantType);
 	}
 
-	private void validateScenicMerchantCreate(MerchantCreateRequest request) {
-		if (!"景区服务".equals(request.merchantType())) {
+	private void validateRequiredMerchantCreateFields(MerchantCreateRequest request) {
+		if (!Set.of("景区服务", "餐饮服务").contains(request.merchantType())) {
 			return;
 		}
 		if (request.businessHours() == null || request.businessHours().isEmpty()
@@ -907,6 +934,41 @@
 		return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
 	}
 
+	private void applyBusinessStatus(List<Map<String, Object>> merchants) {
+		if (merchants.isEmpty()) return;
+		List<Long> merchantIds = merchants.stream().map(row -> Long.valueOf(String.valueOf(row.get("id")))).toList();
+		String placeholders = String.join(",", java.util.Collections.nCopies(merchantIds.size(), "?"));
+		List<Map<String, Object>> periods = DatabaseRows.normalize(jdbcTemplate.queryForList("""
+				select h.merchant_id, h.start_time, h.end_time
+				from merchant_business_hours h
+				join merchant_store s on s.id = h.store_id
+				where s.status = 1 and s.deleted = 0 and h.merchant_id in (""" + placeholders + ")", merchantIds.toArray()));
+		LocalTime now = LocalTime.now(ZoneId.of("Asia/Shanghai"));
+		Set<Long> openMerchantIds = new HashSet<>();
+		for (Map<String, Object> period : periods) {
+			if (isWithinBusinessPeriod(now, String.valueOf(period.get("startTime")), String.valueOf(period.get("endTime")))) {
+				openMerchantIds.add(Long.valueOf(String.valueOf(period.get("merchantId"))));
+			}
+		}
+		for (Map<String, Object> merchant : merchants) {
+			Long merchantId = Long.valueOf(String.valueOf(merchant.get("id")));
+			boolean enabled = "1".equals(String.valueOf(merchant.get("status")));
+			merchant.put("businessStatus", enabled && openMerchantIds.contains(merchantId) ? "营业中" : "休息中");
+		}
+	}
+
+	private boolean isWithinBusinessPeriod(LocalTime now, String startValue, String endValue) {
+		try {
+			LocalTime start = LocalTime.parse(startValue);
+			LocalTime end = LocalTime.parse(endValue);
+			return start.isBefore(end)
+					? !now.isBefore(start) && now.isBefore(end)
+					: !now.isBefore(start) || now.isBefore(end);
+		} catch (DateTimeParseException ignored) {
+			return false;
+		}
+	}
+
 	private void ensureUpdated(int updated) {
 		if (updated == 0) {
 			throw new BusinessException(ErrorCode.NOT_FOUND, "Data not found");
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
index d160e5e..ad5c05a 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
@@ -24,6 +24,7 @@
   @PatchMapping("/roles/{id}/status") public ApiResponse<Map<String,Object>> updateRoleStatus(@PathVariable Long id, @Valid @RequestBody AdminRoleStatusRequest request) { return ApiResponse.success(service.updateRoleStatus(id, request.status())); }
   @DeleteMapping("/roles/{id}") public ApiResponse<Map<String,Object>> deleteRole(@PathVariable Long id) { return ApiResponse.success(service.deleteRole(id)); }
   @GetMapping("/staff") public ApiResponse<List<Map<String,Object>>> staff() { return ApiResponse.success(service.staff()); }
+  @GetMapping("/staff/account-available") public ApiResponse<Map<String,Object>> staffAccountAvailable(@RequestParam String account) { return ApiResponse.success(service.staffAccountAvailable(account)); }
   @PostMapping("/staff") public ApiResponse<Map<String,Object>> createStaff(@Valid @RequestBody AdminStaffRequest request) { return ApiResponse.success(service.createStaff(request)); }
   @PutMapping("/staff/{id}") public ApiResponse<Map<String,Object>> updateStaff(@PathVariable Long id, @Valid @RequestBody AdminStaffRequest request) { return ApiResponse.success(service.updateStaff(id, request)); }
   @PatchMapping("/staff/{id}/password") public ApiResponse<Map<String,Object>> resetStaffPassword(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(service.resetStaffPassword(id, body.get("password"))); }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
index a57ec71..f523d0e 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementService.java
@@ -24,13 +24,14 @@
 
     public List<Map<String, Object>> roles() { return rows("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role order by id"); }
     @Transactional public Map<String, Object> createRole(AdminRoleRequest request) { String code = roleCode(request.roleCode()); jdbc.update("insert into admin_role(role_name,role_code,remark,permission_codes,status) values (?,?,?,?,?)", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status()); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=last_insert_id()"); }
-    @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { String code = request.roleCode() == null || request.roleCode().isBlank() ? existingRoleCode(id) : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
+    @Transactional public Map<String, Object> updateRole(Long id, AdminRoleRequest request) { if ("platform_admin".equals(existingRoleCode(id))) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员角色不可编辑"); String code = request.roleCode() == null || request.roleCode().isBlank() ? existingRoleCode(id) : request.roleCode().trim(); if (jdbc.update("update admin_role set role_name=?,role_code=?,remark=?,permission_codes=?,status=? where id=?", request.roleName().trim(), code, request.remark(), permissions(request.permissionCodes()), request.status(), id) == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Role does not exist"); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
     @Transactional public Map<String, Object> updateRoleStatus(Long id, Byte status) { String code = existingRoleCode(id); if (status == 0 && "platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可禁用"); jdbc.update("update admin_role set status=? where id=?", status, id); return one("select id, role_name, role_code, remark, permission_codes, status, created_at from admin_role where id=?", id); }
     @Transactional public Map<String, Object> deleteRole(Long id) { String code = existingRoleCode(id); if ("platform_admin".equals(code)) throw new BusinessException(ErrorCode.CONFLICT, "平台管理员角色不可删除"); if (Boolean.TRUE.equals(jdbc.queryForObject("select exists(select 1 from admin_operator where role_code=? and deleted=0)", Boolean.class, code))) throw new BusinessException(ErrorCode.CONFLICT, "该角色仍有员工使用,不能删除"); jdbc.update("delete from admin_role where id=?", id); return Map.of("id", id, "deleted", true); }
 
     public List<Map<String, Object>> staff() { return rows("select id,account,display_name,mobile,role_code,position_name,customer_wechat,status,created_at from admin_operator where deleted=0 order by id"); }
-    @Transactional public Map<String, Object> createStaff(AdminStaffRequest request) { if (request.password() == null || request.password().isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "A password is required for a new staff account"); jdbc.update("insert into admin_operator(account,password_hash,display_name,mobile,role_code,position_name,customer_wechat,status) values (?,?,?,?,?,?,?,?)", request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status()); return staffOne("last_insert_id()"); }
-    @Transactional public Map<String, Object> updateStaff(Long id, AdminStaffRequest request) { if (!matchesExistingAccount(id, request.account())) throw new BusinessException(ErrorCode.CONFLICT, "登录账号不可修改"); if (request.status() == 0 && isDefaultAdmin(id)) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员不可禁用"); String sql = request.password() == null || request.password().isBlank() ? "update admin_operator set account=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?" : "update admin_operator set account=?,password_hash=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?"; int updated = request.password() == null || request.password().isBlank() ? jdbc.update(sql, request.account().trim(), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id) : jdbc.update(sql, request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return staffOne("?", id); }
+    public Map<String, Object> staffAccountAvailable(String account) { String normalized = account == null ? "" : account.trim(); if (!normalized.matches("^[A-Za-z0-9]{1,32}$")) return Map.of("available", false); Long count = jdbc.queryForObject("select count(*) from admin_operator where account=?", Long.class, normalized); return Map.of("available", count == null || count == 0); }
+    @Transactional public Map<String, Object> createStaff(AdminStaffRequest request) { if (request.password() == null || request.password().isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "A password is required for a new staff account"); if (!Boolean.TRUE.equals(staffAccountAvailable(request.account()).get("available"))) throw new BusinessException(ErrorCode.CONFLICT, "登录账号已存在,请更换"); jdbc.update("insert into admin_operator(account,password_hash,display_name,mobile,role_code,position_name,customer_wechat,status) values (?,?,?,?,?,?,?,?)", request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status()); return staffOne("last_insert_id()"); }
+    @Transactional public Map<String, Object> updateStaff(Long id, AdminStaffRequest request) { if (isDefaultAdmin(id)) throw new BusinessException(ErrorCode.CONFLICT, "超级管理员账号不可编辑"); if (!matchesExistingAccount(id, request.account())) throw new BusinessException(ErrorCode.CONFLICT, "登录账号不可修改"); String sql = request.password() == null || request.password().isBlank() ? "update admin_operator set account=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?" : "update admin_operator set account=?,password_hash=?,display_name=?,mobile=?,role_code=?,position_name=?,customer_wechat=?,status=? where id=?"; int updated = request.password() == null || request.password().isBlank() ? jdbc.update(sql, request.account().trim(), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id) : jdbc.update(sql, request.account().trim(), MerchantPasswordHasher.encode(request.password()), request.displayName().trim(), blankToNull(request.mobile()), request.roleCode(), request.positionName().trim(), blankToNull(request.customerWechat()), request.status(), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return staffOne("?", id); }
     @Transactional public Map<String, Object> resetStaffPassword(Long id, String password) { if (password == null || password.length() < 6 || password.length() > 64) throw new BusinessException(ErrorCode.BAD_REQUEST, "Password must be 6-64 characters"); int updated = jdbc.update("update admin_operator set password_hash=?, login_failed_count=0, locked_until=null where id=? and deleted=0", MerchantPasswordHasher.encode(password), id); if (updated == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "Staff account does not exist"); return Map.of("id", id, "reset", true); }
     @Transactional public Map<String, Object> deleteStaff(Long id) { int updated = jdbc.update("update admin_operator set deleted=1, status=0 where id=? and deleted=0 and account <> 'admin'", id); if (updated == 0) throw new BusinessException(ErrorCode.CONFLICT, "The account cannot be deleted or does not exist"); return Map.of("id", id, "deleted", true); }
 
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java
index a69b8d3..d6f012a 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantCreateRequest.java
@@ -2,6 +2,7 @@
 
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.Size;
 import jakarta.validation.Valid;
 
@@ -28,6 +29,7 @@
 		String businessStatus,
 		Byte recommend,
 		@NotNull(message = "请输入排序")
+		@Min(value = 0, message = "排序只能输入非负整数")
 		Integer sortNo,
 		Double longitude,
 		Double latitude,
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
index 7ca49ae..09f73bb 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
@@ -31,10 +31,10 @@
         List<Map<String, Object>> rows = rows("select a.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id where a.id = ?", id);
         if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商户进件不存在");
         Map<String, Object> result = rows.get(0);
-        if ("individual".equalsIgnoreCase(String.valueOf(result.get("subjectType")))) {
-            String encrypted = String.valueOf(result.get("subjectIdNoEncrypted"));
-            if (!encrypted.isBlank() && !"null".equals(encrypted)) result.put("subjectIdNo", sensitiveDataCrypto.decrypt(encrypted));
-        }
+        // 个人主体的历史记录可能使用 individual、personal 或中文类型值;
+        // 只要已保存加密身份证号,后台详情均应解密后完整展示。
+        String encrypted = String.valueOf(result.get("subjectIdNoEncrypted"));
+        if (!encrypted.isBlank() && !"null".equals(encrypted)) result.put("subjectIdNo", sensitiveDataCrypto.decrypt(encrypted));
         return result;
     }
     @Transactional(readOnly = true)
@@ -42,7 +42,8 @@
         PageResult<Map<String, Object>> result = page("select c.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "select count(*) from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "c", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate);
         result.records().forEach(row -> {
             String encrypted = String.valueOf(row.remove("cardNoEncrypted"));
-            if (!encrypted.isBlank() && !"null".equals(encrypted)) row.put("cardMasked", sensitiveDataCrypto.decrypt(encrypted));
+            if (!encrypted.isBlank() && !"null".equals(encrypted)) row.put("cardNo", sensitiveDataCrypto.decrypt(encrypted));
+            else row.put("cardNo", row.get("cardMasked"));
             row.put("cityName", bankCardCityName(row.get("cityCode")));
         });
         return result;
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
index 0ad37bd..d83c8d9 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -83,13 +83,13 @@
         Map<String, Object> merchant = merchant(principal);
         String orderNo = BusinessIdGenerator.next("MC");
         jdbc.update("""
-                insert into benefit_order(order_no, talent_id, card_id, merchant_id, store_id, order_type,
+				insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, store_id, order_type,
                                           order_amount, subsidy_amount, base_pay_amount, pay_amount, coupon_id, coupon_amount, expires_at, price_version,
                                           merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot,
                                           merchant_longitude_snapshot, merchant_latitude_snapshot, product_name_snapshot,
                                           order_status, verify_status, merchant_verification_attempt_id, client_idempotency_key)
-                values (?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
-                """, orderNo, number(source.get("talentId")), number(source.get("cardId")), principal.merchantId(), principal.storeId(),
+				values (?, ?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
+				""", orderNo, number(source.get("talentId")), number(source.get("cardId")), string(source.get("cardLevel")), principal.merchantId(), principal.storeId(),
                 amounts.get("originalAmount"), amounts.get("subsidyAmount"), amounts.get("basePayAmount"), amounts.get("payAmount"), request.couponId(), amounts.get("couponAmount"),
                 LocalDateTime.now().plusMinutes(15), merchant.get("merchantName"), merchant.get("address"), merchant.get("contactMobile"),
                 merchant.get("longitude"), merchant.get("latitude"), "到店消费 - " + rule.get("ruleName"),
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
index 1e03d74..0c3a0a0 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
@@ -46,7 +46,7 @@
 
 		String recordSql = """
 				select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
-				       coalesce(nullif(tu.talent_level, ''), tc.card_level) as card_level,
+				       coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level,
 				       b.benefit_name, o.product_name_snapshot, o.order_type, o.order_amount, o.subsidy_amount, o.pay_amount,
 				       o.order_status, o.verify_status, o.created_at, verified.verify_time
 				from benefit_order o
@@ -71,7 +71,7 @@
 	public Map<String, Object> order(String orderNo, Long merchantId, Long storeId) {
 		List<Map<String, Object>> rows = query("""
 				select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
-				       o.card_id, tc.card_no, coalesce(nullif(tu.talent_level, ''), tc.card_level) as card_level,
+				       o.card_id, tc.card_no, coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level,
 				       o.benefit_id, b.benefit_name,
 				       o.merchant_id, m.merchant_name, o.product_id, o.product_name_snapshot, p.rights_config,
 				       o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount,
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java b/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
index 379711e..285a92f 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
@@ -20,6 +20,9 @@
 @Service
 public class OrderService {
 
+	private static final String ORDER_CARD_LEVEL_SQL =
+			"coalesce(nullif(o.card_level_snapshot, ''), tc.card_level, tu.talent_level)";
+
 	private final JdbcTemplate jdbcTemplate;
 	private final HuifuRefundService refundService;
 
@@ -35,14 +38,16 @@
 		long subsidyAmount = MoneyUtils.yuanToCent(request.subsidyAmount());
 		long payAmount = MoneyUtils.yuanToCent(request.payAmount());
 		LocalDateTime expiresAt = payAmount == 0 ? null : LocalDateTime.now().plusMinutes(15);
+		String cardLevel = jdbcTemplate.query("select card_level from talent_card where id = ? and deleted = 0", rs -> rs.next() ? rs.getString(1) : null, request.cardId());
 		jdbcTemplate.update("""
-				insert into benefit_order(order_no, talent_id, card_id, benefit_id, merchant_id, store_id,
+				insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, benefit_id, merchant_id, store_id,
 				                          order_type, order_amount, subsidy_amount, base_pay_amount, pay_amount, expires_at, order_status, verify_status)
-				values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
+				values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
 				""",
 				orderNo,
 				request.talentId(),
 				request.cardId(),
+				cardLevel,
 				request.benefitId(),
 				request.merchantId(),
 				request.storeId(),
@@ -110,7 +115,7 @@
 	public Map<String, Object> detail(String orderNo) {
 		List<Map<String, Object>> rows = query("""
 				select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile,
-				       o.card_id, tc.card_no, coalesce(tc.card_level, tu.talent_level) as talent_level, o.benefit_id, b.benefit_name,
+				       o.card_id, tc.card_no, %s as talent_level, o.benefit_id, b.benefit_name,
 				       o.product_id, o.product_name_snapshot, o.product_cover_url_snapshot,
 				       p.product_name, p.cover_url as product_cover_url,
 				       o.merchant_id, m.merchant_name, m.merchant_type, m.contact_mobile,
@@ -126,7 +131,7 @@
 				left join merchant_coupon mc on mc.id = o.coupon_id and mc.deleted = 0
 				left join merchant m on m.id = o.merchant_id
 				where o.deleted = 0 and o.order_no = ?
-				""", orderNo);
+				""".formatted(ORDER_CARD_LEVEL_SQL), orderNo);
 		if (rows.isEmpty()) {
 			throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在");
 		}
@@ -137,7 +142,21 @@
 				where order_id = ?
 				order by id desc limit 1
 				""", detail.get("id"));
-		detail.put("verifyTime", verificationRows.isEmpty() ? null : verificationRows.get(0).get("verifyTime"));
+		Object verifyTime = verificationRows.isEmpty() ? null : verificationRows.get(0).get("verifyTime");
+		// 到店消费在支付成功后即自动核销,历史数据未必有人工核销记录;此时以成功支付时间作为核销时间。
+		if (verifyTime == null
+				&& "STORE_CONSUME".equalsIgnoreCase(String.valueOf(detail.get("orderType")))
+				&& detail.get("verifyStatus") instanceof Number verifyStatus
+				&& verifyStatus.intValue() == 1) {
+			List<Map<String, Object>> paidTransactions = query("""
+					select paid_at as verify_time
+					from payment_transaction
+					where order_no = ? and status = 'SUCCEEDED' and paid_at is not null
+					order by id desc limit 1
+					""", orderNo);
+			verifyTime = paidTransactions.isEmpty() ? null : paidTransactions.get(0).get("verifyTime");
+		}
+		detail.put("verifyTime", verifyTime);
 		Object itemId = detail.get("productId") == null ? detail.get("benefitId") : detail.get("productId");
 		Object itemName = firstNonNull(detail.get("productNameSnapshot"), detail.get("productName"), detail.get("benefitName"));
 		Object itemCover = firstNonNull(detail.get("productCoverUrlSnapshot"), detail.get("productCoverUrl"), detail.get("merchantCoverUrl"));
@@ -197,11 +216,7 @@
 			where.append(" and m.merchant_name like ?");
 			args.add("%" + merchantName.trim() + "%");
 		}
-		if (cardLevel != null && !cardLevel.isBlank()) {
-			where.append(" and (tc.card_level = ? or tu.talent_level = ?)");
-			args.add(cardLevel);
-			args.add(cardLevel);
-		}
+		appendCardLevelFilter(where, args, cardLevel);
 		if (hasSubsidy != null) {
 			where.append(hasSubsidy == 1 ? " and o.subsidy_amount > 0" : " and o.subsidy_amount = 0");
 		}
@@ -225,16 +240,18 @@
 				select r.id, r.refund_no, r.order_no, r.order_type, r.paid_amount, r.refund_amount,
 				       r.refundable_amount, r.refund_status, r.reason, r.operator_name, r.refund_time,
 				       tu.name as talent_name, tu.mobile as talent_mobile,
-				       coalesce(tc.card_level, tu.talent_level) as talent_level, tc.card_no,
-				       m.merchant_name, m.merchant_type, coalesce(b.benefit_name, o.product_name_snapshot) as benefit_name,
+				       %s as talent_level, tc.card_no,
+				       m.merchant_name, m.merchant_type,
+				       coalesce(nullif(o.product_name_snapshot, ''), nullif(p.product_name, ''), b.benefit_name) as benefit_name,
 				       o.order_amount, o.subsidy_amount, o.pay_amount, o.created_at as order_time
 				from order_refund r
 				left join benefit_order o on o.id = r.order_id
 				left join talent_user tu on tu.id = r.talent_id
 				left join talent_card tc on tc.id = r.card_id
 				left join benefit b on b.id = r.benefit_id
+				left join merchant_product p on p.id = o.product_id and p.deleted = 0
 				left join merchant m on m.id = r.merchant_id
-				""" + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
+				""".formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
 		Long total = jdbcTemplate.queryForObject("""
 				select count(*)
 				from order_refund r
@@ -281,11 +298,7 @@
 			where.append(" and m.merchant_name like ?");
 			args.add("%" + merchantName.trim() + "%");
 		}
-		if (cardLevel != null && !cardLevel.isBlank()) {
-			where.append(" and (tc.card_level = ? or tu.talent_level = ?)");
-			args.add(cardLevel);
-			args.add(cardLevel);
-		}
+		appendCardLevelFilter(where, args, cardLevel);
 		if ("未核销".equals(status)) {
 			return new PageResult<>(List.of(), safePage, safeSize, 0);
 		}
@@ -305,7 +318,7 @@
 				select r.id, r.verify_no, r.verify_type, r.verify_amount, r.subsidy_amount, r.verify_time,
 				       r.remark, o.order_no, o.order_type,
 				       tu.name as talent_name, tu.mobile as talent_mobile,
-				       coalesce(tc.card_level, tu.talent_level) as talent_level, tc.card_no,
+				       %s as talent_level, tc.card_no,
 				       m.merchant_name, m.merchant_type, b.benefit_name,
 				       '已核销' as status
 				from verify_record r
@@ -314,7 +327,7 @@
 				left join talent_card tc on tc.id = r.card_id
 				left join benefit b on b.id = r.benefit_id
 				left join merchant m on m.id = r.merchant_id
-				""" + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
+				""".formatted(ORDER_CARD_LEVEL_SQL) + where + " order by r.id desc limit ? offset ?", pageArgs.toArray());
 		Long total = jdbcTemplate.queryForObject("""
 				select count(*)
 				from verify_record r
@@ -375,11 +388,7 @@
 			where.append(" and m.merchant_name like ?");
 			args.add("%" + merchantName.trim() + "%");
 		}
-		if (cardLevel != null && !cardLevel.isBlank()) {
-			where.append(" and (tc.card_level = ? or tu.talent_level = ?)");
-			args.add(cardLevel);
-			args.add(cardLevel);
-		}
+		appendCardLevelFilter(where, args, cardLevel);
 		if (hasSubsidy != null) {
 			where.append(hasSubsidy == 1 ? " and o.subsidy_amount > 0" : " and o.subsidy_amount = 0");
 		}
@@ -404,7 +413,7 @@
 		pageArgs.add((safePage - 1) * safeSize);
 		List<Map<String, Object>> records = query("""
 				select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile,
-				       coalesce(tc.card_level, tu.talent_level) as talent_level,
+				       %s as talent_level,
 				       o.card_id, tc.card_no, tc.card_type,
 				       o.merchant_id, m.merchant_name, m.merchant_type,
 				       o.benefit_id, b.benefit_name, o.order_type,
@@ -415,7 +424,7 @@
 				left join talent_card tc on tc.id = o.card_id
 				left join benefit b on b.id = o.benefit_id
 				left join merchant m on m.id = o.merchant_id
-				""" + where + " order by o.id desc limit ? offset ?", pageArgs.toArray());
+				""".formatted(ORDER_CARD_LEVEL_SQL) + where + " order by o.id desc limit ? offset ?", pageArgs.toArray());
 		Long total = jdbcTemplate.queryForObject("""
 				select count(*)
 				from benefit_order o
@@ -439,6 +448,14 @@
 		return rows.get(0);
 	}
 
+	private void appendCardLevelFilter(StringBuilder where, List<Object> args, String cardLevel) {
+		if (cardLevel == null || cardLevel.isBlank()) {
+			return;
+		}
+		where.append(" and ").append(ORDER_CARD_LEVEL_SQL).append(" = ?");
+		args.add(cardLevel.trim());
+	}
+
 	private void appendOrderTypeFilter(StringBuilder where, List<Object> args, String alias, String orderType) {
 		if (orderType == null || orderType.isBlank()) {
 			return;
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
index b8eac50..7f5ff34 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
@@ -57,18 +57,19 @@
 
 		try {
 			jdbc.update("""
-					insert into benefit_order(order_no, talent_id, card_id, merchant_id, order_type,
+					insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, order_type,
 					                          order_amount, subsidy_amount, base_pay_amount, pay_amount,
 					                          coupon_id, coupon_amount, expires_at, price_version,
 					                          merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot,
 					                          merchant_longitude_snapshot, merchant_latitude_snapshot,
 					                          product_id, product_name_snapshot, product_cover_url_snapshot,
 					                          order_status, verify_status)
-					values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+					values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 					""",
 					orderNo,
 					talentId,
 					talentCard.get("id"),
+					talentCard.get("cardLevel"),
 					request.merchantId(),
 					normalizeOrderType(product.get("productType")),
 					originalAmount,
diff --git a/talent-card-server/src/main/resources/application.yml b/talent-card-server/src/main/resources/application.yml
index d2ee9ac..05b62af 100644
--- a/talent-card-server/src/main/resources/application.yml
+++ b/talent-card-server/src/main/resources/application.yml
@@ -57,7 +57,7 @@
       system-id: 6666000212191071
       product-id: KAZX
       merchant-id: 6666000212191071
-      mini-program-app-id: wx6ae2cadfea28fce1
+      mini-program-app-id: wxcfee477acb54d179
       private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcsR3R3nUFCv/4l6vczGk6j4UgZhuMm/fTs2MQQqFkIuoISd38TOiIMaLS3KLYL+D+qhUCps56svOK/3M/kdyXfPsBphJyXB5Ar7il8pJ6BDa38StzTrlRm+IXX+4diJvqxUimetjpx0OfnqRkNYQWc/ZVeoEdQbITqLyxogy0R3rkR/wnOTwxHBbkCYsDCxSon4W0SoM5fLs3o947INbPUbUsf0vL5l6NiqlV9YsZ3qAWvvPoPX4HI1s5maMiVKfUwe27P3ARW9n6n3tadBzbB6lrmAEcFoihXpZjNN9UaRKjYfYjTS9JRkDqRyYGhz5cHp9GthEksMvhLP84oEanAgMBAAECggEAKV6d5+Ya8TzpPiIVTxNtCPgRhu9OgL5BHVS0Z/ufjselEJEoGc/YaPTp71AvcnKZrcMMDwopY73RjTZKoFMLcbeesd296xGlLyD12GKGB88K/1+eQDBdv00v0UoTnRqPR9nUh5fRo09n7EG/V3HFsQPm2sSgSDArKwOW5jNJtWLsGIVCW8gg3RQ9YlZ5HW85neBnjBFnrMA9S/hxwZD336M8CiViIa0v9o5CS7qxAqXMzzoOC7Vr3hZ/X0i/5GrSH9DRO7ougSmP4F/zCEc2dViA0rOJ6m3OuS0osh5FulihOBOjI5uLETEt4K/7PuV9xOd4oAz0OBpJ2bhPf3YpGQKBgQDQdEcmnlULa3jTSrbr0vYA3iMpcls/8F8+I/QymMXHAUeVyc8XKuGzblx+/n3x8ZoAvdVg1ZAO5wgLh/P0BEL74iWhfHedjt8xcdk2R5U0RuOnx1TUTKrmLKRZ5gStNFnGlK4yVwpiNqHn/cr+gYYeZSPARFVi00WdXbqVbEEf2wKBgQDAbmo4uHrmHN5GYvt8lVmT9aha1IpKOOLenjd+Y/vKGJcvHzOVCHnOh8OGO/4p7CxOq0sshER713jW2cFGO3xpFEFpIc45pDQ8Ut0XuSoOjKtwPzid8vKX7A1If6FDLMNy7bYR1qqnIJNJnN/f3lsksX+es7QTuRShhA0X0mnEJQKBgBE5OlPHi1FZPO+AYeLUWH0z3YXkLAP5cXUp9Vhk5RqPUYHf3duVMHbp7HyVvIRMKSRciw/oNXH1xuVlA1QrMfmr8t4H8b4z+t8UABWLDpn2LYNEZg7wksJX9/Bxn/tLswtEZ6xDItmAmBoaDza+Lv2975lITCHRQRRClVMZUbw3AoGAVFrSs6SjM2aqRZ60ccuipgrFT3PmX10Kjt46o2w+/zvCYoTB/u3lP5UiMCDybPw7/Ax4+r3Mq6EsNrrkT95ZzlJzzhtkpL4Hq/4DL5gbCJ+/s4fzUMIkeFahCAvy7FieIS51ZuJ8kqDkEBv1w9RcjfxFXpOWOCMlGRyjBLNXbAUCgYEAjcTEuXctlGEjtO1MBaHc4hkkpiPG7gxV/Bq1XXLszv4LTaOfDEpjGGBpVYtb3URYrif/HzX8T1KHELvPziuqP2ghaJQzbztep1TGQ48A5VpOvHdA/w/+k4JJl0PyKBPrFXiHIw1if7rnvOLt27TymwXSaJquSz7Sz2+wXFxzKJk=
       platform-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgxOuXt1UEdZNPk6sntqiIlcs7e+0fZ3CVgKMV2Qv4ixwKmha5etL5WyIONmjoGEHePHpanYkXvm0kHkbbDkjpyHDFdEshuP1eGx5tRUTISRvXY/iFuD3xyeWSsvvjW6hS29OvqVCPcnBUC7jcRj15dJS2IpYR0iDGNS+/M0TyOFwRQME07At+qO+MSjKu4aRIjlglukjD7YbE2s35uV066nsBUCZUNpGPuju1pieUW4zyoIW6K70dyXCQy61oRI6JmA4JRCOJV1BE80N3GrLI4Pbq1C3ZBTgOPi5RWYNq1hfu807BDkPLc3dJKYt22Ea1jPJsg6e/B9Xbzh6JaUNAQIDAQAB
       notify-url: ${HUIFU_NOTIFY_URL:http://221.182.45.100:11451/api/payments/huifu/notify}
@@ -67,9 +67,9 @@
   wechat:
     mini-program:
       # The app id must be the same mini-program that invokes uni.login and uni.requestPayment.
-      app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wx6ae2cadfea28fce1}
+      app-id: ${WECHAT_MINI_PROGRAM_APP_ID:wxcfee477acb54d179}
       # Keep this only in the deployment environment. It is required to exchange a wx.login code for an OpenID.
-      app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:7bf743b066523bbb2d6de80decabc15b}
+      app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:716aae42f04d3c5f17904e894f1c1ee9}
 
 management:
   endpoints:
diff --git a/talent-card-server/src/main/resources/db/migration/V105__news_article_top_time_sorting.sql b/talent-card-server/src/main/resources/db/migration/V112__news_article_top_time_sorting.sql
similarity index 100%
rename from talent-card-server/src/main/resources/db/migration/V105__news_article_top_time_sorting.sql
rename to talent-card-server/src/main/resources/db/migration/V112__news_article_top_time_sorting.sql
diff --git a/talent-card-server/src/main/resources/db/migration/V108__ticket_package_level_rules.sql b/talent-card-server/src/main/resources/db/migration/V113__ticket_package_level_rules.sql
similarity index 100%
rename from talent-card-server/src/main/resources/db/migration/V108__ticket_package_level_rules.sql
rename to talent-card-server/src/main/resources/db/migration/V113__ticket_package_level_rules.sql
diff --git a/talent-card-server/src/main/resources/db/migration/V109__sync_premium_topic_merchant_mappings.sql b/talent-card-server/src/main/resources/db/migration/V114__sync_premium_topic_merchant_mappings.sql
similarity index 100%
rename from talent-card-server/src/main/resources/db/migration/V109__sync_premium_topic_merchant_mappings.sql
rename to talent-card-server/src/main/resources/db/migration/V114__sync_premium_topic_merchant_mappings.sql
diff --git a/talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql b/talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql
new file mode 100644
index 0000000..db09323
--- /dev/null
+++ b/talent-card-server/src/main/resources/db/migration/V115__benefit_order_card_level_snapshot.sql
@@ -0,0 +1,9 @@
+alter table benefit_order
+    add column card_level_snapshot varchar(64) default null comment '下单时人才卡等级快照' after card_id;
+
+update benefit_order o
+left join talent_card c on c.id = o.card_id
+set o.card_level_snapshot = c.card_level
+where (o.card_level_snapshot is null or o.card_level_snapshot = '')
+  and c.card_level is not null
+  and c.card_level <> '';

--
Gitblit v1.7.1