From cb5a42c97fe9ae23b891bdc3231dc8eb100de218 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期一, 31 八月 2026 16:49:41 +0800
Subject: [PATCH] fix: route hotel free voucher scans to dedicated verification page

---
 talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java         |    5 
 rencaika_merchant/pages/scan/scan.uvue                                                                          |   14 +
 rencaika_merchant/pages/verification-confirm/verification-confirm.uvue                                          |    1 
 talent-card-server/src/test/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapperTests.java      |  160 +++++++++++
 rencaika/pages/benefits/benefits.uvue                                                                           |   12 
 talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalControllerTests.java               |   12 
 rencaika_merchant/pages.json                                                                                    |    8 
 talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java                    |    6 
 rencaika/pages/benefit-search/benefit-search.uvue                                                               |   39 --
 talent-card-server/src/main/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapper.java           |   60 ++-
 rencaika/utils/merchant-navigation.uts                                                                          |   92 ++++++
 rencaika_merchant/pages/free-stay-confirm/free-stay-confirm.uvue                                                |  121 ++++++++
 rencaika_merchant/services/verification.uts                                                                     |   10 
 talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java                       |   24 +
 talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java                  |   14 +
 rencaika/services/portal.uts                                                                                    |    6 
 rencaika/pages/transport-benefits/transport-benefits.uvue                                                       |    9 
 talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java |   81 +++++
 talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java                           |  102 +++++++
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java       |   63 ++++
 20 files changed, 748 insertions(+), 91 deletions(-)

diff --git a/rencaika/pages/benefit-search/benefit-search.uvue b/rencaika/pages/benefit-search/benefit-search.uvue
index fbdfe11..f0ac7a8 100644
--- a/rencaika/pages/benefit-search/benefit-search.uvue
+++ b/rencaika/pages/benefit-search/benefit-search.uvue
@@ -61,6 +61,7 @@
 	import { resolveApiUrl } from '../../services/http.uts'
 	import { getSession } from '../../services/session.uts'
 	import { safeBack } from '../../utils/navigation.uts'
+	import { openMerchantDetail } from '../../utils/merchant-navigation.uts'
 
 	const navTop = ref(28)
 	const navHeight = ref(32)
@@ -298,43 +299,7 @@
 		keyword.value = value
 		performSearch()
 	}
-	const openMerchant = (item: any) => {
-		const action = item.targetAction != null ? item.targetAction as string : 'MERCHANT'
-		const detailBusinessId = item.detailBusinessId != null ? item.detailBusinessId as string : item.id as string
-		if (action == 'CINEMA') {
-			uni.navigateTo({ url: '/pages/cinema-merchant/cinema-merchant?merchantId=' + encodeURIComponent(detailBusinessId) + '&name=' + encodeURIComponent(item.name) })
-			return
-		}
-		if (action == 'SHOPPING') {
-			uni.navigateTo({ url: '/pages/shopping-merchant/shopping-merchant?name=' + encodeURIComponent(item.name) + '&discount=' + encodeURIComponent(item.discount) + '&address=' + encodeURIComponent(item.address) })
-			return
-		}
-		if (action == 'MEDICAL') {
-			uni.navigateTo({ url: '/pages/merchant/merchant?type=medical&name=' + encodeURIComponent(item.name) })
-			return
-		}
-		if (action == 'FINANCE') {
-			uni.navigateTo({ url: '/pages/finance-merchant/finance-merchant?name=' + encodeURIComponent(item.name) + '&discount=' + encodeURIComponent(item.discount) + '&address=' + encodeURIComponent(item.address) + '&image=' + encodeURIComponent(item.image) })
-			return
-		}
-		if (action == 'CAR_WASH') {
-			uni.navigateTo({ url: '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(detailBusinessId) + '&name=' + encodeURIComponent(item.name) })
-			return
-		}
-		if (action == 'FUEL' || action == 'PARKING') {
-			const transportAction = action == 'PARKING' ? 'plate' : 'mobile'
-			uni.navigateTo({ url: '/pages/merchant/merchant?type=transport&name=' + encodeURIComponent(item.name) + '&action=' + transportAction + '&plateMode=withParking' })
-			return
-		}
-		uni.navigateTo({
-			url: '/pages/merchant/merchant?merchantType=' + encodeURIComponent(item.merchantType)
-				+ '&merchantId=' + encodeURIComponent(detailBusinessId)
-				+ '&name=' + encodeURIComponent(item.name)
-				+ '&image=' + encodeURIComponent(item.image)
-				+ '&address=' + encodeURIComponent(item.address)
-				+ '&discount=' + encodeURIComponent(item.discount)
-		})
-	}
+	const openMerchant = (item: any) => openMerchantDetail(item)
 	const loadMoreMerchants = () => {
 		if (initialLoading.value || loadingMore.value || !hasMore.value) return
 		const requestVersion = listRequestVersion.value
diff --git a/rencaika/pages/benefits/benefits.uvue b/rencaika/pages/benefits/benefits.uvue
index bbe24fb..2d2a4f2 100644
--- a/rencaika/pages/benefits/benefits.uvue
+++ b/rencaika/pages/benefits/benefits.uvue
@@ -69,6 +69,7 @@
 	import { getMerchants } from '../../services/portal.uts'
 	import { resolveApiUrl } from '../../services/http.uts'
 	import { getSession } from '../../services/session.uts'
+	import { openMerchantDetail } from '../../utils/merchant-navigation.uts'
 
 	const navTop = ref(28)
 	const navHeight = ref(32)
@@ -157,6 +158,9 @@
 				id: '' + item.id,
 				name: item.merchantName != null ? item.merchantName as string : '人才权益商户',
 				merchantType: merchantType,
+				detailType: item.detailType != null ? item.detailType as string : 'MERCHANT',
+				detailBusinessId: item.detailBusinessId != null ? '' + item.detailBusinessId : '' + item.id,
+				targetAction: item.targetAction != null ? item.targetAction as string : 'MERCHANT',
 				hasCoupon: hasCoupon,
 				discount: discount,
 				tags: tags,
@@ -242,13 +246,7 @@
 		}
 		uni.showToast({ title: '已选择' + name, icon: 'none' })
 	}
-	const openMerchant = (item: any) => uni.navigateTo({
-		url: '/pages/merchant/merchant?merchantType=' + encodeURIComponent(item.merchantType != null ? item.merchantType : 'food') + '&merchantId=' + encodeURIComponent(item.id)
-			+ '&name=' + encodeURIComponent(item.name)
-			+ '&image=' + encodeURIComponent(item.image)
-			+ '&address=' + encodeURIComponent(item.address)
-			+ '&discount=' + encodeURIComponent(item.discount)
-	})
+	const openMerchant = (item: any) => openMerchantDetail(item)
 	const loadMoreBenefits = () => {
 		if (initialLoading.value || loadingMore.value || !hasMoreBenefits.value) return
 		loadMoreError.value = false
diff --git a/rencaika/pages/transport-benefits/transport-benefits.uvue b/rencaika/pages/transport-benefits/transport-benefits.uvue
index 6fcb952..69bff88 100644
--- a/rencaika/pages/transport-benefits/transport-benefits.uvue
+++ b/rencaika/pages/transport-benefits/transport-benefits.uvue
@@ -42,7 +42,7 @@
 						<image class="service-image" :src="item.image" mode="aspectFill" @error="replaceServiceImage(item)"></image>
 						<view class="service-info">
 							<text class="service-name">{{item.name}}</text>
-							<text class="service-discount">{{item.available ? item.discount : '已下架'}}</text>
+							<text v-if="!item.available || item.benefitTagVisible" class="service-discount">{{item.available ? item.discount : '已下架'}}</text>
 							<view class="address-row"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view>
 						</view>
 					</view>
@@ -60,6 +60,7 @@
 	import { isApiConfigured } from '../../config/api.uts'
 	import { getTransportTopic, getTransportTopicMerchants } from '../../services/portal.uts'
 	import { resolveApiUrl } from '../../services/http.uts'
+	import { getSession } from '../../services/session.uts'
 	import { safeBack } from '../../utils/navigation.uts'
 
 	const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
@@ -68,6 +69,7 @@
 	const topicTitle = ref('行--畅行无忧'); const topicDescription = ref('整合加油、停车、洗车等出行服务资源,为人才提供专属折扣、停车优惠和便捷洗车服务,全方位保障人才出行便捷。'); const topicError = ref(false)
 	const activeIndex = ref(0); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644); const scenes = ref<any[]>(fallbackScenes); const currentScene = ref({ id: '', name: '', image: '' })
 	const serviceTabs = [{ key: 'fuel', label: '加油服务' }, { key: 'parking', label: '停车服务' }, { key: 'carWash', label: '自助洗车服务' }]
+	const talentId = ref('')
 	const activeTab = ref('fuel'); const visibleServices = ref<any[]>([]); const initialLoading = ref(true); const initialError = ref(false); const loadingMore = ref(false); const loadMoreError = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false)
 
 	onLoad((options: any) => {
@@ -75,6 +77,7 @@
 		const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); windowWidth.value = info.windowWidth; navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right
 		// #endif
 		if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' }
+		const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string
 		selectScene(0); loadTransportTopic(); loadInitialServices()
 	})
 	const loadTransportTopic = () => {
@@ -99,10 +102,10 @@
 		const requestVersion = listRequestVersion.value
 		if (!append && failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return }
 		if (append && failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return }
-		getTransportTopicMerchants(activeTab.value, currentPage.value + 1, 4, (data: any) => {
+		getTransportTopicMerchants(activeTab.value, talentId.value, currentPage.value + 1, 4, (data: any) => {
 			if (requestVersion != listRequestVersion.value) return
 			const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []
-			for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : activeTab.value, name: item.merchantName != null ? item.merchantName as string : '行服务', discount: item.discountText != null ? item.discountText as string : '人才专享', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, externalLinkType: item.externalLinkType != null ? item.externalLinkType as string : '' }) }
+			for (let index = 0; index < records.length; index++) { const item = records[index]; const merchantType = item.merchantType != null ? item.merchantType as string : activeTab.value; const discount = item.discountText != null ? item.discountText as string : ''; next.push({ id: '' + item.id, merchantType: merchantType, name: item.merchantName != null ? item.merchantName as string : '行服务', discount: discount, benefitTagVisible: merchantType == 'carWash' ? item.benefitTagVisible === true : discount != '', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false, externalLinkType: item.externalLinkType != null ? item.externalLinkType as string : '' }) }
 			visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true
 		}, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true })
 	}
diff --git a/rencaika/services/portal.uts b/rencaika/services/portal.uts
index 796c385..bb86856 100644
--- a/rencaika/services/portal.uts
+++ b/rencaika/services/portal.uts
@@ -49,7 +49,11 @@
 export const getEntertainmentTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appEntertainmentTopic'), method: 'GET', skipAuth: true }, onSuccess, onError)
 export const getEntertainmentTopicMerchants = (page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appEntertainmentTopicMerchants'), method: 'GET', skipAuth: true, data: { page: page, size: size } }, onSuccess, onError)
 export const getTransportTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTransportTopic'), method: 'GET', skipAuth: true }, onSuccess, onError)
-export const getTransportTopicMerchants = (type: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTransportTopicMerchants'), method: 'GET', skipAuth: true, data: { type: type, page: page, size: size } }, onSuccess, onError)
+export const getTransportTopicMerchants = (type: string, talentId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => {
+	const data: any = { type: type, page: page, size: size }
+	if (talentId != '') data.talentId = Number(talentId)
+	return request<any>({ path: getApiEndpoint('appTransportTopicMerchants'), method: 'GET', data: data }, onSuccess, onError)
+}
 export const getShoppingTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingTopic'), method: 'GET', skipAuth: true }, onSuccess, onError)
 export const getShoppingTopicMerchants = (categoryId: string, talentId: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => {
 	const data: any = { categoryId: categoryId, page: page, size: size }
diff --git a/rencaika/utils/merchant-navigation.uts b/rencaika/utils/merchant-navigation.uts
new file mode 100644
index 0000000..04250cf
--- /dev/null
+++ b/rencaika/utils/merchant-navigation.uts
@@ -0,0 +1,92 @@
+const textValue = (value: any, fallback: string = ''): string => {
+	if (value == null) return fallback
+	return String(value)
+}
+
+const encoded = (value: any, fallback: string = ''): string => encodeURIComponent(textValue(value, fallback))
+
+const inferTargetAction = (merchantType: string): string => {
+	if (merchantType == 'cinema') return 'CINEMA'
+	if (merchantType == 'fuel') return 'FUEL'
+	if (merchantType == 'parking') return 'PARKING'
+	if (merchantType == 'carWash') return 'CAR_WASH'
+	if (merchantType == 'medical') return 'MEDICAL'
+	if (merchantType == 'employment') return 'SPOUSE_EMPLOYMENT'
+	if (merchantType == 'finance') return 'FINANCE'
+	if (merchantType == 'approval') return 'ADMIN_APPROVAL'
+	if (merchantType == 'children') return 'CHILD_ENTRUSTMENT'
+	if (merchantType == 'housing' || merchantType == 'car' || merchantType == 'appliance' || merchantType == 'digital' || merchantType == 'culture' || merchantType == 'telecom' || merchantType == 'shopping') return 'SHOPPING'
+	return 'MERCHANT'
+}
+
+export const openMerchantDetail = (item: any) => {
+	if (item == null) return
+	if (item.available === false) {
+		uni.showToast({ title: textValue(item.unavailableReason, '该服务当前不可用'), icon: 'none' })
+		return
+	}
+
+	const merchantId = textValue(item.detailBusinessId, textValue(item.id))
+	const merchantType = textValue(item.merchantType, 'food')
+	const configuredAction = textValue(item.targetAction, textValue(item.detailType))
+	const action = configuredAction == '' || configuredAction == 'MERCHANT' ? inferTargetAction(merchantType) : configuredAction
+	const name = encoded(item.name)
+	const image = encoded(item.image)
+	const address = encoded(item.address)
+	const discount = encoded(item.discount)
+
+	if (action == 'CINEMA') {
+		uni.navigateTo({ url: '/pages/cinema-merchant/cinema-merchant?merchantId=' + encoded(merchantId) + '&name=' + name + '&image=' + image + '&address=' + address + '&discount=' + discount })
+		return
+	}
+	if (action == 'SHOPPING' && merchantType != 'culture') {
+		uni.navigateTo({ url: '/pages/shopping-merchant/shopping-merchant?merchantId=' + encoded(merchantId) })
+		return
+	}
+	if (action == 'MEDICAL') {
+		uni.navigateTo({ url: '/pages/merchant/merchant?type=medical&merchantType=medical&merchantId=' + encoded(merchantId) + '&name=' + name })
+		return
+	}
+	if (action == 'SPOUSE_EMPLOYMENT') {
+		uni.navigateTo({ url: '/pages/spouse-employment/spouse-employment?serviceId=' + encoded(merchantId) })
+		return
+	}
+	if (action == 'FINANCE') {
+		uni.navigateTo({ url: '/pages/finance-merchant/finance-merchant?serviceId=' + encoded(merchantId) + '&merchantId=' + encoded(merchantId) + '&name=' + name + '&discount=' + discount + '&address=' + address + '&image=' + image })
+		return
+	}
+	if (action == 'ADMIN_APPROVAL') {
+		uni.navigateTo({ url: '/pages/admin-approval/admin-approval?serviceId=' + encoded(merchantId) })
+		return
+	}
+	if (action == 'CHILD_ENTRUSTMENT') {
+		uni.navigateTo({ url: '/pages/children-entrustment/children-entrustment?serviceId=' + encoded(merchantId) })
+		return
+	}
+	if (action == 'CAR_WASH') {
+		uni.navigateTo({ url: '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encoded(merchantId) + '&name=' + name })
+		return
+	}
+	if (action == 'FUEL' || action == 'PARKING') {
+		const transportAction = action == 'PARKING' && textValue(item.discount).indexOf('人才码') >= 0 ? 'talentCode' : ''
+		uni.navigateTo({
+			url: '/pages/merchant/merchant?type=transport&merchantType=' + encoded(merchantType)
+				+ '&merchantId=' + encoded(merchantId)
+				+ '&name=' + name
+				+ '&image=' + image
+				+ '&address=' + address
+				+ '&discount=' + discount
+				+ '&action=' + encoded(transportAction)
+		})
+		return
+	}
+
+	uni.navigateTo({
+		url: '/pages/merchant/merchant?merchantType=' + encoded(merchantType)
+			+ '&merchantId=' + encoded(merchantId)
+			+ '&name=' + name
+			+ '&image=' + image
+			+ '&address=' + address
+			+ '&discount=' + discount
+	})
+}
diff --git a/rencaika_merchant/pages.json b/rencaika_merchant/pages.json
index 004f76e..f30f354 100644
--- a/rencaika_merchant/pages.json
+++ b/rencaika_merchant/pages.json
@@ -193,6 +193,14 @@
 				"backgroundColor": "#F7F7F7",
 				"disableScroll": true
 			}
+		},
+		{
+			"path": "pages/free-stay-confirm/free-stay-confirm",
+			"style": {
+				"navigationStyle": "custom",
+				"backgroundColor": "#F7F8FA",
+				"disableScroll": true
+			}
 		}
 	],
 	"globalStyle": {
diff --git a/rencaika_merchant/pages/free-stay-confirm/free-stay-confirm.uvue b/rencaika_merchant/pages/free-stay-confirm/free-stay-confirm.uvue
new file mode 100644
index 0000000..7ac0328
--- /dev/null
+++ b/rencaika_merchant/pages/free-stay-confirm/free-stay-confirm.uvue
@@ -0,0 +1,121 @@
+<template>
+	<view class="page">
+		<view class="navigation-bar">
+			<view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view>
+			<text class="navigation-title">核销输入</text>
+		</view>
+		<scroll-view class="content" scroll-y="true" :show-scrollbar="false">
+			<view class="talent-card">
+				<view class="card-decoration"></view>
+				<view class="talent-head"><text class="talent-name">{{customerName}}</text><text class="talent-level">{{cardLevel}}</text></view>
+				<text class="talent-mobile">{{displayMobile(customerMobile)}}</text>
+				<view class="current-benefit"><text class="benefit-label">当前权益</text><text class="benefit-text">免费入住{{totalCount}}天</text></view>
+			</view>
+			<view class="method-card">
+				<text class="section-label">优惠方式</text>
+				<view class="tag-list"><text class="benefit-tag">免费入住{{totalCount}}天</text><text v-if="discountText.length > 0" class="benefit-tag">{{discountText}}</text></view>
+			</view>
+			<view class="stay-ticket">
+				<view class="ticket-left"><text class="ticket-vertical">入<br/>住<br/>券</text></view>
+				<view class="ticket-divider"></view>
+				<view class="ticket-right"><text class="ticket-count">一天</text><text class="ticket-free">免费</text></view>
+				<view class="ticket-notch ticket-notch-left"></view><view class="ticket-notch ticket-notch-right"></view>
+			</view>
+			<view class="content-bottom-space"></view>
+		</scroll-view>
+		<view class="bottom-bar"><view class="confirm-button" :class="submitting || loadError.length > 0 ? 'button-disabled' : ''" @tap="confirm"><text>{{submitting ? '核销中...' : '确认核销'}}</text></view></view>
+	</view>
+</template>
+
+<script setup lang="uts">
+	import { confirmVerificationCode } from '../../services/verification.uts'
+
+	const verificationId = ref('')
+	const customerName = ref('持卡人')
+	const customerMobile = ref('')
+	const cardLevel = ref('人才卡')
+	const totalCount = ref(5)
+	const discountText = ref('')
+	const loadError = ref('')
+	const submitting = ref(false)
+
+	const textOption = (options: any, name: string, fallback: string): string => options != null && options[name] != null && ('' + options[name]).length > 0 ? decodeURIComponent('' + options[name]) : fallback
+	const displayMobile = (mobile: string): string => mobile.length < 7 ? mobile : mobile.substring(0, 3) + '****' + mobile.substring(mobile.length - 4)
+	const goBack = () => uni.navigateBack()
+
+	const parseDiscount = (value: string): string => {
+		if (value.length == 0 || value.indexOf('折') < 0) return ''
+		const end = value.indexOf('折') + 1
+		const parts = value.substring(0, end).split(';')
+		return parts[parts.length - 1].split(',')[0].trim()
+	}
+
+	const confirm = () => {
+		if (submitting.value || verificationId.value.length == 0 || loadError.value.length > 0) return
+		uni.showModal({
+			title: '确认核销',
+			content: '确认核销本次免费入住券吗?',
+			confirmText: '确认核销',
+			confirmColor: '#FF6B2C',
+			success: (modal: any) => {
+				if (!modal.confirm) return
+				submitting.value = true
+				confirmVerificationCode(verificationId.value, 'merchant-free-stay-' + verificationId.value + '-' + Date.now(), (_result: any, _traceId: string) => {
+					submitting.value = false
+					uni.redirectTo({ url: '/pages/verification-success/verification-success?orderNo=' + encodeURIComponent('免费入住券') })
+				}, (error: any) => {
+					submitting.value = false
+					uni.showToast({ title: error != null && error.message != null ? '' + error.message : '核销失败,请重试', icon: 'none' })
+				})
+			}
+		})
+	}
+
+	onLoad((options: any) => {
+		verificationId.value = textOption(options, 'verificationId', '')
+		customerName.value = textOption(options, 'customerName', '持卡人')
+		customerMobile.value = textOption(options, 'customerMobile', '')
+		cardLevel.value = textOption(options, 'cardLevel', '人才卡')
+		totalCount.value = Math.max(Number(textOption(options, 'freeStayTotalCount', '5')), 1)
+		discountText.value = parseDiscount(textOption(options, 'benefitText', ''))
+		if (verificationId.value.length == 0) loadError.value = '核销凭证无效,请返回重新扫码'
+	})
+</script>
+
+<style>
+	page { background-color: #F7F8FA; }
+	.page { display: flex; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F7F8FA; }
+	.navigation-bar { display: flex; height: 174rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 25rpx; box-sizing: border-box; position: relative; }
+	.navigation-title { color: #1F2937; font-size: 35rpx; line-height: 49rpx; font-weight: 700; }
+	.back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; top: 82rpx; left: 28rpx; }
+	.back-icon { width: 40rpx; height: 40rpx; }
+	.content { height: 0; flex: 1; padding: 20rpx 34rpx 0; box-sizing: border-box; }
+	.talent-card { min-height: 238rpx; padding: 34rpx 34rpx 28rpx; border-radius: 38rpx; background-image: linear-gradient(135deg, #C99D32 0%, #F7E69D 55%, #E5BD4E 100%); box-shadow: 0 18rpx 38rpx rgba(188, 139, 37, 0.2); box-sizing: border-box; position: relative; overflow: hidden; }
+	.card-decoration { width: 220rpx; height: 220rpx; border: 1rpx solid rgba(255, 255, 255, 0.35); border-radius: 110rpx; position: absolute; top: -72rpx; right: -12rpx; }
+	.talent-head { display: flex; flex-direction: row; justify-content: space-between; align-items: center; position: relative; }
+	.talent-name { color: #FFFFFF; font-size: 39rpx; line-height: 55rpx; font-weight: 700; }
+	.talent-level { padding: 9rpx 20rpx; border-radius: 24rpx; color: #FFFFFF; font-size: 22rpx; line-height: 31rpx; font-weight: 600; background-color: rgba(149, 104, 22, 0.46); }
+	.talent-mobile { display: block; margin-top: 15rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 600; position: relative; }
+	.current-benefit { display: flex; margin-top: 22rpx; flex-direction: row; align-items: center; position: relative; }
+	.benefit-label { color: rgba(255, 255, 255, 0.78); font-size: 23rpx; line-height: 33rpx; }
+	.benefit-text { margin-left: 18rpx; color: #FFFFFF; font-size: 23rpx; line-height: 33rpx; font-weight: 600; }
+	.method-card { padding: 31rpx 34rpx; margin-top: 22rpx; border-radius: 30rpx; background-color: #FFFFFF; box-shadow: 0 14rpx 30rpx rgba(57, 67, 80, 0.06); }
+	.section-label { display: block; color: #8D99AA; font-size: 27rpx; line-height: 38rpx; }
+	.tag-list { display: flex; margin-top: 20rpx; flex-direction: row; flex-wrap: wrap; }
+	.benefit-tag { display: flex; min-height: 48rpx; padding: 7rpx 18rpx; margin-right: 12rpx; margin-bottom: 4rpx; justify-content: center; align-items: center; border-radius: 24rpx; color: #FF772C; font-size: 23rpx; line-height: 33rpx; background-color: #FFF0E5; box-sizing: border-box; }
+	.stay-ticket { display: flex; height: 210rpx; margin: 40rpx 42rpx 0; flex-direction: row; align-items: center; border-radius: 32rpx; background-image: linear-gradient(100deg, #FF5D25 0%, #FF8A27 100%); box-shadow: 0 18rpx 34rpx rgba(255, 98, 35, 0.23); position: relative; overflow: hidden; }
+	.ticket-left { display: flex; width: 134rpx; justify-content: center; align-items: center; }
+	.ticket-vertical { color: #FFFFFF; font-size: 28rpx; line-height: 37rpx; font-weight: 700; text-align: center; }
+	.ticket-divider { width: 2rpx; height: 144rpx; background-color: rgba(255, 255, 255, 0.3); }
+	.ticket-right { display: flex; flex: 1; flex-direction: column; justify-content: center; align-items: center; }
+	.ticket-count { color: #FFFFFF; font-size: 43rpx; line-height: 60rpx; font-weight: 700; }
+	.ticket-free { color: #FFFFFF; font-size: 43rpx; line-height: 60rpx; font-weight: 700; }
+	.ticket-notch { width: 34rpx; height: 34rpx; border-radius: 20rpx; background-color: #F7F8FA; position: absolute; top: 88rpx; }
+	.ticket-notch-left { left: -17rpx; }
+	.ticket-notch-right { right: -17rpx; }
+	.remaining-hint { margin-top: 20rpx; color: #98A2B1; font-size: 23rpx; line-height: 33rpx; text-align: center; }
+	.content-bottom-space { height: 180rpx; }
+	.bottom-bar { display: flex; height: 154rpx; padding: 20rpx 96rpx 34rpx; background-color: #FFFFFF; box-sizing: border-box; position: fixed; right: 0; bottom: 0; left: 0; z-index: 10; }
+	.confirm-button { display: flex; height: 94rpx; flex: 1; justify-content: center; align-items: center; border-radius: 48rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7625 0%, #FF962F 100%); box-shadow: 0 12rpx 24rpx rgba(255, 107, 36, 0.22); }
+	.button-disabled { opacity: 0.6; box-shadow: none; }
+</style>
diff --git a/rencaika_merchant/pages/scan/scan.uvue b/rencaika_merchant/pages/scan/scan.uvue
index d0487d6..7e50a6c 100644
--- a/rencaika_merchant/pages/scan/scan.uvue
+++ b/rencaika_merchant/pages/scan/scan.uvue
@@ -111,6 +111,19 @@
 				uni.navigateTo({ url: '/pages/consume-input/consume-input?verificationId=' + encodeURIComponent(result.verificationId) })
 				return
 			}
+			if (result.credentialType == 'FREE_STAY_CODE') {
+				openedConfirmation = true
+				uni.navigateTo({
+					url: '/pages/free-stay-confirm/free-stay-confirm?verificationId=' + encodeURIComponent(result.verificationId)
+						+ '&customerName=' + encodeURIComponent(result.customerName)
+						+ '&customerMobile=' + encodeURIComponent(result.customerMobile)
+						+ '&cardLevel=' + encodeURIComponent(result.cardLevel)
+						+ '&freeStayTotalCount=' + encodeURIComponent('' + result.freeStayTotalCount)
+						+ '&freeStayRemainingCount=' + encodeURIComponent('' + result.freeStayRemainingCount)
+						+ '&benefitText=' + encodeURIComponent(result.benefitText)
+				})
+				return
+			}
 			openedConfirmation = true
 			uni.navigateTo({
 				url: '/pages/verification-confirm/verification-confirm?verificationId=' + encodeURIComponent(result.verificationId)
@@ -119,6 +132,7 @@
 					+ '&customerMobile=' + encodeURIComponent(result.customerMobile)
 					+ '&cardLevel=' + encodeURIComponent(result.cardLevel)
 					+ '&orderNo=' + encodeURIComponent(result.orderNo)
+					+ '&subsidyAmount=' + encodeURIComponent('' + result.subsidyAmount)
 			})
 		}, showVerificationError)
 	}
diff --git a/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue b/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
index 7f357da..61cae9b 100644
--- a/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
+++ b/rencaika_merchant/pages/verification-confirm/verification-confirm.uvue
@@ -113,6 +113,7 @@
 		customerMobile.value = textOption(options, 'customerMobile', '')
 		cardLevel.value = textOption(options, 'cardLevel', '人才卡')
 		orderNo.value = textOption(options, 'orderNo', '')
+		subsidyAmount.value = Number(textOption(options, 'subsidyAmount', '0'))
 		loadOrder()
 	})
 </script>
diff --git a/rencaika_merchant/services/verification.uts b/rencaika_merchant/services/verification.uts
index 2b0358e..2444da5 100644
--- a/rencaika_merchant/services/verification.uts
+++ b/rencaika_merchant/services/verification.uts
@@ -13,6 +13,10 @@
 	customerName: string
 	customerMobile: string
 	cardLevel: string
+	subsidyAmount: number
+	freeStayTotalCount: number
+	freeStayRemainingCount: number
+	benefitText: string
 }
 
 export type VerificationConfirmResult = {
@@ -76,7 +80,11 @@
 			orderNo: data.orderNo != null ? data.orderNo as string : '',
 			customerName: data.customerName != null ? data.customerName as string : '',
 			customerMobile: data.customerMobile != null ? data.customerMobile as string : '',
-			cardLevel: data.cardLevel != null ? data.cardLevel as string : ''
+			cardLevel: data.cardLevel != null ? data.cardLevel as string : '',
+			subsidyAmount: data.subsidyAmount != null ? Number(data.subsidyAmount) : 0,
+			freeStayTotalCount: data.freeStayTotalCount != null ? Number(data.freeStayTotalCount) : 0,
+			freeStayRemainingCount: data.freeStayRemainingCount != null ? Number(data.freeStayRemainingCount) : 0,
+			benefitText: data.benefitText != null ? '' + data.benefitText : ''
 		}
 		onSuccess(result, traceId)
 	}, onError)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
index 8dfdda1..02ed9be 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
@@ -1,5 +1,7 @@
 package com.talentcard.server.business.merchant;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.LinkedHashMap;
@@ -11,6 +13,7 @@
 import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
 import com.talentcard.server.common.BusinessException;
 import com.talentcard.server.common.ErrorCode;
+import org.springframework.dao.DataAccessException;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -94,22 +97,42 @@
         }
         String verificationId = createAttempt(principal, TALENT_CODE, number(code.get("dynamicCodeId")), null, null, code.get("expiresAt"));
         return scanResult(verificationId, TALENT_CODE, "人才码有效,请录入消费金额并发送待支付订单", code.get("talentName"),
-                code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"));
+                code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"), 0L);
     }
 
     private Map<String, Object> createOrderAttempt(MerchantPrincipal principal, Map<String, Object> code) {
         String verificationId = createAttempt(principal, ORDER_CODE, null, number(code.get("orderVerificationCodeId")), null, code.get("expiresAt"));
         String title = value(code.get("productName"), "订单权益");
         return scanResult(verificationId, ORDER_CODE, "订单核销码有效,请确认后完成核销:" + title,
-                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), code.get("orderNo"), code.get("expiresAt"));
+                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), code.get("orderNo"), code.get("expiresAt"), 0L);
     }
 
     private Map<String, Object> createFreeStayAttempt(MerchantPrincipal principal, Map<String, Object> code) {
         Long remaining = number(code.get("totalCount")) - number(code.get("usedCount"));
         String verificationId = createAttempt(principal, FREE_STAY_CODE, null, null,
                 number(code.get("freeStayVerificationCodeId")), code.get("expiresAt"));
-        return scanResult(verificationId, FREE_STAY_CODE, "免费入住券有效,剩余 " + remaining + " 次,请确认后核销",
-                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"));
+        Map<String, Object> result = scanResult(verificationId, FREE_STAY_CODE, "免费入住券有效,剩余 " + remaining + " 次,请确认后核销",
+                code.get("talentName"), code.get("talentMobile"), code.get("cardLevel"), null, code.get("expiresAt"),
+                hotelSubsidyAmountCents());
+        result.put("freeStayTotalCount", number(code.get("totalCount")));
+        result.put("freeStayRemainingCount", Math.max(remaining, 0L));
+        result.put("benefitText", freeStayBenefitText(code, principal));
+        return result;
+    }
+
+    private String freeStayBenefitText(Map<String, Object> code, MerchantPrincipal principal) {
+        try {
+            List<Map<String, Object>> benefits = rows("""
+                    select benefit_text
+                    from merchant_card_benefit
+                    where merchant_id = ? and card_level = ? and status = 1
+                    limit 1
+                    """, principal.merchantId(), value(code.get("cardLevel"), ""));
+            return benefits.isEmpty() ? "" : value(benefits.get(0).get("benefitText"), "");
+        } catch (DataAccessException ignored) {
+            // Older installations may not have the optional display table yet.
+            return "";
+        }
     }
 
     private String createAttempt(MerchantPrincipal principal, String type, Long dynamicCodeId, Long orderCodeId, Long freeStayCodeId, Object expiresAt) {
@@ -217,8 +240,9 @@
         if (codeUpdated == 0 || voucherUpdated == 0) {
             throw new BusinessException(ErrorCode.CONFLICT, "免费入住券已被使用或已失效");
         }
+        Long subsidyAmount = hotelSubsidyAmountCents();
         String verifyNo = insertRecord(principal, null, number(code.get("talentId")), number(code.get("cardId")), null,
-                "HOTEL_FREE", 0L, 0L, "免费入住券");
+                "HOTEL_FREE", 0L, subsidyAmount, "免费入住券(平台线下补贴)");
         finishAttempt(attempt, verifyNo);
         return confirmed(verifyNo, code.get("cardLevel"));
     }
@@ -291,7 +315,8 @@
     }
 
     private Map<String, Object> scanResult(String verificationId, String credentialType, String message,
-                                            Object talentName, Object talentMobile, Object cardLevel, Object orderNo, Object expiresAt) {
+                                            Object talentName, Object talentMobile, Object cardLevel, Object orderNo,
+                                            Object expiresAt, Long subsidyAmount) {
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("verificationId", verificationId);
         result.put("credentialType", credentialType);
@@ -303,10 +328,36 @@
         result.put("customerMobile", maskMobile(value(talentMobile, "")));
         result.put("cardLevel", cardLevel == null ? "人才卡" : String.valueOf(cardLevel));
         result.put("orderNo", orderNo == null ? "" : String.valueOf(orderNo));
+        result.put("subsidyAmount", subsidyAmount == null ? 0L : subsidyAmount);
         result.put("expiresAt", expiresAt);
         return result;
     }
 
+    private Long hotelSubsidyAmountCents() {
+        List<Map<String, Object>> settings = rows("""
+                select setting_value
+                from admin_setting
+                where setting_key = 'hotelSubsidy'
+                limit 1
+                """);
+        if (settings.isEmpty()) {
+            return 0L;
+        }
+        String configuredAmount = value(settings.get(0).get("settingValue"), "").trim();
+        if (configuredAmount.isEmpty()) {
+            return 0L;
+        }
+        try {
+            BigDecimal yuan = new BigDecimal(configuredAmount).setScale(2, RoundingMode.UNNECESSARY);
+            if (yuan.signum() < 0) {
+                throw new ArithmeticException("negative subsidy");
+            }
+            return yuan.movePointRight(2).longValueExact();
+        } catch (ArithmeticException | NumberFormatException exception) {
+            throw new BusinessException(ErrorCode.CONFLICT, "酒店免费入住补贴金额配置不正确,请联系平台管理员");
+        }
+    }
+
     private Map<String, Object> confirmed(Object verifyNo, Object cardLevel) {
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("verified", true);
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java
index 5efbb68..3280bae 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalController.java
@@ -146,9 +146,11 @@
 	public ApiResponse<PageResult<Map<String, Object>>> transportMerchants(
 			@RequestParam(defaultValue = "fuel") String type,
 			@RequestParam(defaultValue = "1") int page,
-			@RequestParam(defaultValue = "4") int size
+			@RequestParam(defaultValue = "4") int size,
+			@RequestParam(required = false) Long talentId,
+			@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization
 	) {
-		return ApiResponse.success(portalService.transportMerchants(type, page, size));
+		return ApiResponse.success(portalService.transportMerchants(type, page, size, sessionTalentId(authorization, talentId)));
 	}
 
 	@GetMapping("/topics/shopping")
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
index f339b61..678b1db 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -525,7 +525,7 @@
 	}
 
 	@Transactional(readOnly = true)
-	public PageResult<Map<String, Object>> transportMerchants(String type, int page, int size) {
+	public PageResult<Map<String, Object>> transportMerchants(String type, int page, int size, Long talentId) {
 		String requestedType = MerchantTypeCatalog.canonicalCode(type);
 		if (!List.of("fuel", "parking", "carWash").contains(requestedType)) {
 			throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的行服务类型");
@@ -539,12 +539,21 @@
 				from merchant
 				""" + merchantWhere + " order by sort_no asc, id asc",
 				"select count(*) from merchant" + merchantWhere, page, size);
+		String cardLevel = "carWash".equals(requestedType) ? activeProductCardLevel(talentId) : null;
+		String sourceAreaType = "carWash".equals(requestedType) ? activeTalentSourceAreaType(talentId) : "";
 		List<Map<String, Object>> records = result.records().stream().map(row -> {
 			Map<String, Object> item = enrichMerchantSearchRow(row);
 			item.put("merchantType", requestedType);
-			item.put("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享")));
 			if ("carWash".equals(requestedType)) {
-				item.put("externalLinkType", carWashExternalLinkType(item.get("merchantRightsConfig")));
+				List<Map<String, Object>> rights = merchantRightsConfigs(item.get("merchantRightsConfig"));
+				String externalLinkType = carWashExternalLinkType(rights);
+				Map<String, Object> right = matchingMerchantRight(rights, cardLevel, sourceAreaType, true);
+				String discountText = carWashListDiscountText(externalLinkType, right);
+				item.put("externalLinkType", externalLinkType);
+				item.put("discountText", discountText);
+				item.put("benefitTagVisible", !discountText.isBlank());
+			} else {
+				item.put("discountText", textOrDefault(item.get("serviceLabel"), textOrDefault(item.get("discountText"), "人才专享")));
 			}
 			item.remove("merchantRightsConfig");
 			return item;
@@ -920,8 +929,8 @@
 		}
 	}
 
-	private String carWashExternalLinkType(Object rightsConfig) {
-		for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) {
+	private String carWashExternalLinkType(List<Map<String, Object>> rights) {
+		for (Map<String, Object> right : rights) {
 			if (right.containsKey("externalLinkType")
 					|| right.containsKey("externalMiniProgramShortLink")
 					|| right.containsKey("externalMiniProgramPath")
@@ -932,6 +941,11 @@
 		return "";
 	}
 
+	private String carWashListDiscountText(String externalLinkType, Map<String, Object> right) {
+		if ("miniProgram".equals(externalLinkType) || right == null) return "";
+		return merchantRightDiscountText(right);
+	}
+
 	private boolean booleanValue(Object value) {
 		if (value instanceof Boolean bool) return bool;
 		if (value instanceof Number number) return number.intValue() != 0;
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapper.java
index 3fb5366..f172ec0 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapper.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapper.java
@@ -27,27 +27,45 @@
             <script>
             select count(*)
             from (
-                select case
-                           when #{periodType} = 'MONTH' then date_format(coalesce(v.verify_time, o.updated_at, o.created_at), '%Y-%m')
-                           else date_format(coalesce(v.verify_time, o.updated_at, o.created_at), '%Y-%m-%d')
-                       end as period_key,
-                       o.merchant_id
-                from benefit_order o
-                join merchant m on m.id = o.merchant_id and m.deleted = 0
-                left join (
-                    select order_id, max(verify_time) as verify_time
-                    from verify_record
-                    where status = 1
-                    group by order_id
-                ) v on v.order_id = o.id
-                where o.deleted = 0
-                  and o.order_status = 1
-                  and o.verify_status = 1
-                  <if test="merchantId != null">and o.merchant_id = #{merchantId}</if>
-                  <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
-                  <if test="startTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &gt;= #{startTime}</if>
-                  <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &lt; #{endTime}</if>
-                group by period_key, o.merchant_id
+                select base.period_key, base.merchant_id
+                from (
+                    select case
+                               when #{periodType} = 'MONTH' then date_format(coalesce(v.verify_time, o.updated_at, o.created_at), '%Y-%m')
+                               else date_format(coalesce(v.verify_time, o.updated_at, o.created_at), '%Y-%m-%d')
+                           end as period_key,
+                           o.merchant_id
+                    from benefit_order o
+                    join merchant m on m.id = o.merchant_id and m.deleted = 0
+                    left join (
+                        select order_id, max(verify_time) as verify_time
+                        from verify_record
+                        where status = 1
+                        group by order_id
+                    ) v on v.order_id = o.id
+                    where o.deleted = 0
+                      and o.order_status = 1
+                      and o.verify_status = 1
+                      <if test="merchantId != null">and o.merchant_id = #{merchantId}</if>
+                      <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
+                      <if test="startTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &gt;= #{startTime}</if>
+                      <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &lt; #{endTime}</if>
+                    union all
+                    select case
+                               when #{periodType} = 'MONTH' then date_format(r.verify_time, '%Y-%m')
+                               else date_format(r.verify_time, '%Y-%m-%d')
+                           end as period_key,
+                           r.merchant_id
+                    from verify_record r
+                    join merchant m on m.id = r.merchant_id and m.deleted = 0
+                    where r.status = 1
+                      and r.order_id is null
+                      and upper(r.verify_type) = 'HOTEL_FREE'
+                      <if test="merchantId != null">and r.merchant_id = #{merchantId}</if>
+                      <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
+                      <if test="startTime != null">and r.verify_time &gt;= #{startTime}</if>
+                      <if test="endTime != null">and r.verify_time &lt; #{endTime}</if>
+                ) base
+                group by base.period_key, base.merchant_id
             ) grouped
             </script>
             """)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java
index ef387db..0f856d6 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java
@@ -185,6 +185,28 @@
                   <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
                   <if test="startTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &gt;= #{startTime}</if>
                   <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &lt; #{endTime}</if>
+                union all
+                select case
+                           when #{periodType} = 'MONTH' then date_format(r.verify_time, '%Y-%m')
+                           else date_format(r.verify_time, '%Y-%m-%d')
+                       end as period_key,
+                       r.verify_time as business_time,
+                       r.merchant_id,
+                       m.merchant_name,
+                       m.merchant_type,
+                       0 as order_amount,
+                       0 as pay_amount,
+                       0 as coupon_amount,
+                       coalesce(r.subsidy_amount, 0) as government_subsidy_amount
+                from verify_record r
+                join merchant m on m.id = r.merchant_id and m.deleted = 0
+                where r.status = 1
+                  and r.order_id is null
+                  and upper(r.verify_type) = 'HOTEL_FREE'
+                  <if test="merchantId != null">and r.merchant_id = #{merchantId}</if>
+                  <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
+                  <if test="startTime != null">and r.verify_time &gt;= #{startTime}</if>
+                  <if test="endTime != null">and r.verify_time &lt; #{endTime}</if>
             ) base
             group by base.period_key, base.merchant_id, base.merchant_name, base.merchant_type
             order by base.period_key desc, base.merchant_id desc
@@ -234,6 +256,24 @@
                   <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
                   <if test="startTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &gt;= #{startTime}</if>
                   <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &lt; #{endTime}</if>
+                union all
+                select r.verify_time as business_time,
+                       r.merchant_id,
+                       m.merchant_name,
+                       m.merchant_type,
+                       0 as order_amount,
+                       0 as pay_amount,
+                       0 as coupon_amount,
+                       coalesce(r.subsidy_amount, 0) as government_subsidy_amount
+                from verify_record r
+                join merchant m on m.id = r.merchant_id and m.deleted = 0
+                where r.status = 1
+                  and r.order_id is null
+                  and upper(r.verify_type) = 'HOTEL_FREE'
+                  <if test="merchantId != null">and r.merchant_id = #{merchantId}</if>
+                  <if test="merchantType != null and merchantType != ''">and m.merchant_type_code = #{merchantType}</if>
+                  <if test="startTime != null">and r.verify_time &gt;= #{startTime}</if>
+                  <if test="endTime != null">and r.verify_time &lt; #{endTime}</if>
             ) base
             group by base.merchant_id
             order by settlementAmountCents desc, base.merchant_id desc
@@ -244,6 +284,8 @@
     public String selectSettlementExportDetails() {
         return sql("""
             <script>
+            select details.*
+            from (
             select o.id as orderId,
                    o.order_no as orderNo,
                    o.order_type as orderType,
@@ -268,7 +310,8 @@
                    o.created_at as createdAt,
                    o.updated_at as paidAt,
                    v.verify_time as verifyTime,
-                   v.verify_operator as verifyOperator
+                   v.verify_operator as verifyOperator,
+                   coalesce(v.verify_time, o.updated_at, o.created_at) as businessTime
             from benefit_order o
             join merchant m on m.id = o.merchant_id and m.deleted = 0
             left join merchant_store s on s.id = o.store_id and s.deleted = 0
@@ -288,7 +331,48 @@
               <if test="verifyStatus != null">and o.verify_status = #{verifyStatus}</if>
               <if test="startTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &gt;= #{startTime}</if>
               <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) &lt; #{endTime}</if>
-            order by coalesce(v.verify_time, o.updated_at, o.created_at) desc, o.id desc
+            union all
+            select r.id as orderId,
+                   r.verify_no as orderNo,
+                   'HOTEL_FREE' as orderType,
+                   r.merchant_id as merchantId,
+                   m.merchant_name as merchantName,
+                   m.merchant_type as merchantType,
+                   r.store_id as storeId,
+                   s.store_name as storeName,
+                   r.talent_id as talentId,
+                   t.name as talentName,
+                   t.mobile as talentMobile,
+                   tc.card_no as cardNo,
+                   tc.card_level as cardLevel,
+                   '酒店免费入住券' as productName,
+                   0 as originalAmountCents,
+                   0 as transactionAmountCents,
+                   coalesce(r.subsidy_amount, 0) as subsidyAmountCents,
+                   coalesce(r.subsidy_amount, 0) as settlementAmountCents,
+                   0 as couponAmountCents,
+                   1 as orderStatus,
+                   1 as verifyStatus,
+                   r.verify_time as createdAt,
+                   null as paidAt,
+                   r.verify_time as verifyTime,
+                   r.staff_id as verifyOperator,
+                   r.verify_time as businessTime
+            from verify_record r
+            join merchant m on m.id = r.merchant_id and m.deleted = 0
+            left join merchant_store s on s.id = r.store_id and s.deleted = 0
+            left join talent_user t on t.id = r.talent_id and t.deleted = 0
+            left join talent_card tc on tc.id = r.card_id and tc.deleted = 0
+            where r.status = 1
+              and r.order_id is null
+              and upper(r.verify_type) = 'HOTEL_FREE'
+              and r.merchant_id = #{merchantId}
+              <if test="orderStatus != null">and #{orderStatus} = 1</if>
+              <if test="verifyStatus != null">and #{verifyStatus} = 1</if>
+              <if test="startTime != null">and r.verify_time &gt;= #{startTime}</if>
+              <if test="endTime != null">and r.verify_time &lt; #{endTime}</if>
+            ) details
+            order by details.businessTime desc, details.orderId desc
             limit #{limit}
             </script>
             """);
@@ -329,12 +413,24 @@
                     where o.deleted = 0
                       and o.order_status = 1
                       and o.merchant_id = #{merchantId}
+                ), 0)
+                +
+                coalesce((
+                    select sum(r.subsidy_amount)
+                    from verify_record r
+                    where r.status = 1
+                      and r.order_id is null
+                      and upper(r.verify_type) = 'HOTEL_FREE'
+                      and r.merchant_id = #{merchantId}
                 ), 0) as subsidyAmountCents
             """);
     }
 
     private static String sql(String template) {
-        return template.replace("/*AMOUNT*/", GovernmentSubsidySql.AMOUNT_CENTS)
+        String amount = template.contains("<script>")
+                ? GovernmentSubsidySql.AMOUNT_CENTS.replace("<", "&lt;")
+                : GovernmentSubsidySql.AMOUNT_CENTS;
+        return template.replace("/*AMOUNT*/", amount)
                 .replace("/*ORDER_TIME*/", GovernmentSubsidySql.ORDER_TIME);
     }
 }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java b/talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java
index 59782e9..0dc4d4e 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java
@@ -130,7 +130,7 @@
         result.put("subsidyAmountCents", subsidyAmount);
         result.put("subsidyAmount", yuan(subsidyAmount));
         result.put("hasData", transactionAmount > 0 || todayVerificationCount > 0 || weekVerificationCount > 0 || subsidyAmount > 0);
-        result.put("statisticsBasis", "交易总额统计已支付订单实付金额;补贴总额仅统计政府承担补贴的商品补贴和政府补贴券金额,商家自行让利不进入政府补贴结算");
+        result.put("statisticsBasis", "交易总额统计已支付订单实付金额;补贴总额统计政府承担的订单补贴、政府补贴券及酒店免费入住线下补贴,商家自行让利不进入政府补贴结算");
         return result;
     }
 
@@ -278,7 +278,7 @@
         result.put("governmentSubsidyOrderCount", longValue(row, "governmentSubsidyOrderCount"));
         result.put("periodStartTime", value(row, "periodStartTime", "startTime"));
         result.put("periodEndTime", value(row, "periodEndTime", "endTime"));
-        result.put("statisticsBasis", "已支付且已核销订单;补贴额仅统计政府承担补贴的商品补贴和政府补贴券金额,商家自行让利不进入政府补贴结算");
+        result.put("statisticsBasis", "已支付且已核销订单及酒店免费入住核销记录;补贴额统计政府承担的订单补贴、政府补贴券及酒店免费入住线下补贴,商家自行让利不进入政府补贴结算");
         return result;
     }
 
@@ -422,6 +422,7 @@
             case "STORE_CONSUME" -> "到店消费";
             case "GROUP_BUY" -> "团购订单";
             case "HOTEL_RESERVATION" -> "酒店预约";
+            case "HOTEL_FREE" -> "酒店免费入住";
             default -> clean(orderType);
         };
     }
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java
index 5c340ef..686e757 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java
@@ -59,15 +59,65 @@
                 create table free_stay_verification_code (
                     id bigint primary key,
                     voucher_id bigint not null,
+                    code_token varchar(128),
                     expires_at timestamp not null,
                     status tinyint not null
+                )
+                """);
+        jdbc.execute("""
+                create table talent_user (
+                    id bigint primary key,
+                    name varchar(64),
+                    mobile varchar(32),
+                    status tinyint not null default 1,
+                    source_area_type varchar(32),
+                    deleted tinyint not null default 0
                 )
                 """);
         jdbc.execute("""
                 create table talent_card (
                     id bigint primary key,
                     card_level varchar(64),
+                    card_no varchar(64),
+                    status tinyint not null default 1,
+                    valid_end date,
                     deleted tinyint not null default 0
+                )
+                """);
+        jdbc.execute("""
+                create table talent_dynamic_code (
+                    id bigint primary key,
+                    expected_merchant_id bigint,
+                    expires_at timestamp,
+                    card_id bigint,
+                    talent_id bigint,
+                    code_token varchar(128),
+                    status tinyint not null default 0
+                )
+                """);
+        jdbc.execute("""
+                create table benefit_order (
+                    id bigint primary key,
+                    order_no varchar(64),
+                    order_status tinyint,
+                    verify_status tinyint,
+                    merchant_id bigint,
+                    store_id bigint,
+                    product_name_snapshot varchar(255),
+                    benefit_id bigint,
+                    talent_id bigint,
+                    card_id bigint,
+                    deleted tinyint not null default 0
+                )
+                """);
+        jdbc.execute("create table benefit (id bigint primary key, benefit_name varchar(255))");
+        jdbc.execute("""
+                create table order_dynamic_verification_code (
+                    id bigint primary key,
+                    order_id bigint,
+                    code_token varchar(128),
+                    expires_at timestamp,
+                    status tinyint not null default 0
                 )
                 """);
         jdbc.execute("""
@@ -87,6 +137,9 @@
                     remark varchar(255)
                 )
                 """);
+        jdbc.execute("create table admin_setting (setting_key varchar(64) primary key, setting_value varchar(1000))");
+        jdbc.update("insert into admin_setting(setting_key, setting_value) values ('hotelSubsidy', '268.50')");
+        jdbc.execute("create table payment_transaction (id bigint primary key)");
         service = new MerchantVerificationService(jdbc, new ManualConsumptionService(jdbc));
         principal = new MerchantPrincipal(9L, 88L, 7L, "operator");
     }
@@ -103,6 +156,28 @@
         assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class,
                 "free-stay-expired-code")).isEqualTo(1);
         assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'HOTEL_FREE'", Integer.class)).isEqualTo(1);
+        assertThat(jdbc.queryForObject("select subsidy_amount from verify_record where verify_type = 'HOTEL_FREE'", Long.class)).isEqualTo(26850L);
+        assertThat(jdbc.queryForObject("select count(*) from benefit_order", Integer.class)).isZero();
+        assertThat(jdbc.queryForObject("select count(*) from payment_transaction", Integer.class)).isZero();
+    }
+
+    @Test
+    void scanReturnsConfiguredHotelSubsidyAmount() {
+        jdbc.update("insert into talent_card(id, card_level, deleted) values (10, '卓越人才卡', 0)");
+        jdbc.update("insert into talent_user(id, name, mobile, deleted) values (1015, '测试人才', '13800000000', 0)");
+        jdbc.update("""
+                insert into talent_free_stay_voucher(id, talent_id, merchant_id, card_id, total_count, used_count, valid_end, status)
+                values (20, 1015, 88, 10, 2, 0, ?, 1)
+                """, LocalDate.now().plusDays(1));
+        jdbc.update("""
+                insert into free_stay_verification_code(id, voucher_id, code_token, expires_at, status)
+                values (30, 20, 'free-stay-scan-code', ?, 0)
+                """, Timestamp.valueOf(LocalDateTime.now().plusMinutes(1)));
+
+        java.util.Map<String, Object> scanned = service.scan(principal, new VerificationScanRequest("free-stay-scan-code"));
+
+        assertThat(scanned.get("credentialType")).isEqualTo("FREE_STAY_CODE");
+        assertThat(scanned.get("subsidyAmount")).isEqualTo(26850L);
     }
 
     @Test
@@ -121,9 +196,9 @@
                 values (20, 1015, 88, 10, 2, 0, ?, 1)
                 """, voucherValidEnd);
         jdbc.update("""
-                insert into free_stay_verification_code(id, voucher_id, expires_at, status)
-                values (30, 20, ?, 2)
-                """, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1)));
+                insert into free_stay_verification_code(id, voucher_id, code_token, expires_at, status)
+                values (30, 20, ?, ?, 2)
+                """, verificationId, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1)));
         jdbc.update("""
                 insert into merchant_verification_attempt(verification_id, credential_type, free_stay_verification_code_id,
                                                           merchant_id, store_id, operator_id, expires_at, status)
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalControllerTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalControllerTests.java
index d4e7fa5..2da839a 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalControllerTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalControllerTests.java
@@ -47,4 +47,16 @@
 
 		verify(portalService).stayMerchants("hotel", 1, 4, 7L);
 	}
+
+	@Test
+	void transportMerchantsUsesTalentIdFromAuthenticatedSession() {
+		when(talentAuthService.authenticate("Bearer session-token"))
+				.thenReturn(new TalentSessionPrincipal(42L));
+		when(portalService.transportMerchants("carWash", 1, 4, 42L))
+				.thenReturn(new PageResult<>(List.of(), 1, 4, 0));
+
+		controller.transportMerchants("carWash", 1, 4, 7L, "Bearer session-token");
+
+		verify(portalService).transportMerchants("carWash", 1, 4, 42L);
+	}
 }
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java
index 9d92a8f..ea75ae9 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/portal/PortalServiceTests.java
@@ -59,6 +59,20 @@
 	}
 
 	@Test
+	void carWashListHidesMiniProgramTagAndShowsNormalConfiguredDiscount() throws Exception {
+		Method method = PortalService.class.getDeclaredMethod("carWashListDiscountText", String.class, Map.class);
+		method.setAccessible(true);
+		Map<String, Object> right = Map.of(
+				"enabled", true,
+				"storeConsume", true,
+				"consumeEnabled", true,
+				"discount", "8.50");
+
+		assertThat(method.invoke(service, "miniProgram", right)).isEqualTo("");
+		assertThat(method.invoke(service, "normal", right)).isEqualTo("8.5折");
+	}
+
+	@Test
 	@SuppressWarnings("unchecked")
 	void recommendedMerchantTagsFollowCategoryListRules() throws Exception {
 		Method method = PortalService.class.getDeclaredMethod(
diff --git a/talent-card-server/src/test/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapperTests.java b/talent-card-server/src/test/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapperTests.java
new file mode 100644
index 0000000..9e950da
--- /dev/null
+++ b/talent-card-server/src/test/java/com/talentcard/server/mapper/MerchantSettlementStatisticsMapperTests.java
@@ -0,0 +1,160 @@
+package com.talentcard.server.mapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+public class MerchantSettlementStatisticsMapperTests {
+
+    private SqlSessionFactory sessionFactory;
+
+    @BeforeEach
+    void setUp() {
+        DriverManagerDataSource dataSource = new DriverManagerDataSource(
+                "jdbc:h2:mem:merchant-settlement-statistics-" + System.nanoTime()
+                        + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
+        createSchema(dataSource);
+        insertFixture(dataSource);
+
+        Environment environment = new Environment("test", new JdbcTransactionFactory(), dataSource);
+        Configuration configuration = new Configuration(environment);
+        configuration.addMapper(MerchantSettlementStatisticsMapper.class);
+        sessionFactory = new SqlSessionFactoryBuilder().build(configuration);
+    }
+
+    @Test
+    void includesHotelFreeOfflineSubsidyWithoutIncreasingTransactionAmount() {
+        LocalDateTime start = LocalDate.now().atStartOfDay();
+        LocalDateTime end = LocalDate.now().plusDays(1).atStartOfDay();
+
+        try (SqlSession session = sessionFactory.openSession()) {
+            MerchantSettlementStatisticsMapper mapper = session.getMapper(MerchantSettlementStatisticsMapper.class);
+
+            List<Map<String, Object>> summaries = mapper.selectPeriodSummaries(
+                    88L, "HOTEL", "DAY", start, end, 20, 0);
+            assertThat(summaries).hasSize(1);
+            assertTotals(summaries.get(0));
+            assertThat(mapper.countPeriodSummaries(88L, "HOTEL", "DAY", start, end)).isEqualTo(1L);
+
+            List<Map<String, Object>> overview = mapper.selectMerchantOverviews(88L, "HOTEL", start, end);
+            assertThat(overview).hasSize(1);
+            assertTotals(overview.get(0));
+
+            Map<String, Object> cards = mapper.selectDashboardCards(88L, start, end, start, end);
+            assertThat(longValue(cards, "transactionAmountCents")).isEqualTo(10_000L);
+            assertThat(longValue(cards, "subsidyAmountCents")).isEqualTo(28_850L);
+            assertThat(longValue(cards, "todayVerificationCount")).isEqualTo(2L);
+
+            List<Map<String, Object>> details = mapper.selectSettlementExportDetails(88L, 1, 1, start, end, 20);
+            assertThat(details).hasSize(2);
+            Map<String, Object> freeStay = details.stream()
+                    .filter(row -> "HOTEL_FREE".equals(String.valueOf(value(row, "orderType"))))
+                    .findFirst().orElseThrow();
+            assertThat(longValue(freeStay, "transactionAmountCents")).isZero();
+            assertThat(longValue(freeStay, "subsidyAmountCents")).isEqualTo(26_850L);
+            assertThat(longValue(freeStay, "settlementAmountCents")).isEqualTo(26_850L);
+        }
+    }
+
+    private void assertTotals(Map<String, Object> row) {
+        assertThat(longValue(row, "orderCount")).isEqualTo(2L);
+        assertThat(longValue(row, "transactionAmountCents")).isEqualTo(10_000L);
+        assertThat(longValue(row, "subsidyAmountCents")).isEqualTo(28_850L);
+        assertThat(longValue(row, "settlementAmountCents")).isEqualTo(38_850L);
+    }
+
+    private long longValue(Map<String, Object> row, String key) {
+        Object value = value(row, key);
+        return value instanceof Number number ? number.longValue() : 0L;
+    }
+
+    private Object value(Map<String, Object> row, String key) {
+        for (Map.Entry<String, Object> entry : row.entrySet()) {
+            if (entry.getKey().equalsIgnoreCase(key)) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    private void createSchema(DataSource dataSource) {
+        JdbcTemplate jdbc = new JdbcTemplate(dataSource);
+        jdbc.execute("create alias if not exists date_format for 'com.talentcard.server.mapper.MerchantSettlementStatisticsMapperTests.dateFormat'");
+        jdbc.execute("""
+                create table merchant (
+                    id bigint primary key, merchant_name varchar(128), merchant_type varchar(64),
+                    merchant_type_code varchar(64), deleted tinyint not null default 0
+                )
+                """);
+        jdbc.execute("""
+                create table benefit_order (
+                    id bigint primary key, order_no varchar(64), order_type varchar(64), merchant_id bigint,
+                    store_id bigint, talent_id bigint, card_id bigint, benefit_id bigint, product_id bigint,
+                    coupon_id bigint, product_name_snapshot varchar(255), order_amount bigint, pay_amount bigint,
+                    coupon_amount bigint, subsidy_amount bigint, order_status tinyint, verify_status tinyint,
+                    created_at timestamp, updated_at timestamp, deleted tinyint not null default 0
+                )
+                """);
+        jdbc.execute("""
+                create table verify_record (
+                    id bigint primary key, verify_no varchar(64), order_id bigint, talent_id bigint, card_id bigint,
+                    merchant_id bigint, store_id bigint, staff_id bigint, verify_type varchar(64), verify_amount bigint,
+                    subsidy_amount bigint, verify_time timestamp, status tinyint not null default 1
+                )
+                """);
+        jdbc.execute("create table merchant_product (id bigint primary key, product_name varchar(255), is_government_subsidy tinyint, deleted tinyint)");
+        jdbc.execute("create table merchant_coupon (id bigint primary key, government_subsidy tinyint, deleted tinyint)");
+        jdbc.execute("create table merchant_store (id bigint primary key, store_name varchar(128), deleted tinyint)");
+        jdbc.execute("create table talent_user (id bigint primary key, name varchar(64), mobile varchar(32), deleted tinyint)");
+        jdbc.execute("create table talent_card (id bigint primary key, card_no varchar(64), card_level varchar(64), deleted tinyint)");
+    }
+
+    private void insertFixture(DataSource dataSource) {
+        JdbcTemplate jdbc = new JdbcTemplate(dataSource);
+        LocalDateTime now = LocalDateTime.now();
+        jdbc.update("insert into merchant(id, merchant_name, merchant_type, merchant_type_code, deleted) values (88, '测试酒店', '酒店服务', 'HOTEL', 0)");
+        jdbc.update("insert into merchant_store(id, store_name, deleted) values (7, '酒店门店', 0)");
+        jdbc.update("insert into talent_user(id, name, mobile, deleted) values (1015, '测试人才', '13800000000', 0)");
+        jdbc.update("insert into talent_card(id, card_no, card_level, deleted) values (10, 'TC001', '卓越人才卡', 0)");
+        jdbc.update("""
+                insert into benefit_order(id, order_no, order_type, merchant_id, store_id, talent_id, card_id,
+                                          product_name_snapshot, order_amount, pay_amount, coupon_amount, subsidy_amount,
+                                          order_status, verify_status, created_at, updated_at, deleted)
+                values (1, 'O001', 'GROUP_BUY', 88, 7, 1015, 10, '测试团单', 12000, 10000, 0, 2000, 1, 1, ?, ?, 0)
+                """, now, now);
+        jdbc.update("""
+                insert into verify_record(id, verify_no, order_id, talent_id, card_id, merchant_id, store_id, staff_id,
+                                          verify_type, verify_amount, subsidy_amount, verify_time, status)
+                values (1, 'V001', 1, 1015, 10, 88, 7, 9, 'ORDER_CODE', 10000, 2000, ?, 1)
+                """, now);
+        jdbc.update("""
+                insert into verify_record(id, verify_no, order_id, talent_id, card_id, merchant_id, store_id, staff_id,
+                                          verify_type, verify_amount, subsidy_amount, verify_time, status)
+                values (2, 'V002', null, 1015, 10, 88, 7, 9, 'HOTEL_FREE', 0, 26850, ?, 1)
+                """, now);
+    }
+
+    public static String dateFormat(java.sql.Timestamp value, String pattern) {
+        DateTimeFormatter formatter = "%Y-%m".equals(pattern)
+                ? DateTimeFormatter.ofPattern("yyyy-MM")
+                : DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        return value.toLocalDateTime().format(formatter);
+    }
+}

--
Gitblit v1.7.1