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) >= #{startTime}</if>
- <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) < #{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) >= #{startTime}</if>
+ <if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) < #{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 >= #{startTime}</if>
+ <if test="endTime != null">and r.verify_time < #{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) >= #{startTime}</if>
<if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) < #{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 >= #{startTime}</if>
+ <if test="endTime != null">and r.verify_time < #{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) >= #{startTime}</if>
<if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) < #{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 >= #{startTime}</if>
+ <if test="endTime != null">and r.verify_time < #{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) >= #{startTime}</if>
<if test="endTime != null">and coalesce(v.verify_time, o.updated_at, o.created_at) < #{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 >= #{startTime}</if>
+ <if test="endTime != null">and r.verify_time < #{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("<", "<")
+ : 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