From 25729fa1ae7b630cbd4a9547a5693acd03c09c2a Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期三, 05 八月 2026 14:19:47 +0800
Subject: [PATCH] feat(payment): 添加汇付个人入驻功能并优化商户结算流程
---
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java | 30 +++
rencaika_merchant/pages/merchant-onboarding/merchant-onboarding.uvue | 2
rencaika/pages/order-cancelled/order-cancelled.uvue | 4
rencaika/pages/admin-approval/admin-approval.uvue | 6
rencaika/pages/stay-benefits/stay-benefits.uvue | 2
rencaika/pages/benefit-search/benefit-search.uvue | 6
rencaika/pages/merchant/merchant.uvue | 16 +-
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java | 3
talent-card-server/scripts/repair-local-mysql-v92.sql | 6
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingRequest.java | 4
talent-card-server/src/main/java/com/talentcard/server/business/portal/TalentCardPortalService.java | 8
rencaika/pages/travel-benefits/travel-benefits.uvue | 2
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java | 81 ++++++++++
rencaika/pages/shopping-merchant/shopping-merchant.uvue | 6
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java | 77 ++++++++
rencaika/pages/premium-benefits/premium-benefits.uvue | 2
rencaika/pages/benefits/benefits.uvue | 8
rencaika/pages/children-entrustment/children-entrustment.uvue | 6
rencaika/pages/finance-merchant/finance-merchant.uvue | 6
rencaika/pages/cinema-merchant/cinema-merchant.uvue | 2
rencaika/pages/medical-package/medical-package.uvue | 6
/dev/null | 0
talent-card-server/src/main/resources/application.yml | 2
rencaika/pages/shopping-benefits/shopping-benefits.uvue | 6
talent-card-server/src/main/resources/db/migration/V93__merchant_onboarding_huifu_kyc.sql | 3
rencaika/pages/product-detail/product-detail.uvue | 6
talent-card-server/src/main/java/com/talentcard/server/business/portal/ProductPriceCalculator.java | 138 +++++++++++++++++
rencaika/pages/transport-benefits/transport-benefits.uvue | 6
rencaika/pages/entertainment-benefits/entertainment-benefits.uvue | 2
29 files changed, 387 insertions(+), 59 deletions(-)
diff --git a/rencaika/pages/admin-approval/admin-approval.uvue b/rencaika/pages/admin-approval/admin-approval.uvue
index 0e9f28d..220cb09 100644
--- a/rencaika/pages/admin-approval/admin-approval.uvue
+++ b/rencaika/pages/admin-approval/admin-approval.uvue
@@ -53,7 +53,7 @@
const navWidth = ref(88)
const navRight = ref(10)
const serviceId = ref('')
- const heroImage = ref('/static/stay-assets/hotel-vienna.jpg')
+ const heroImage = ref('')
const benefitTitle = ref('行政审批')
const freeLabel = ref('免费')
const benefitAvailable = ref(true)
@@ -87,7 +87,7 @@
getAdminApprovalBenefit(serviceId.value, (data: any) => {
benefitTitle.value = data.title || '行政审批'
freeLabel.value = data.freeLabel || '免费'
- heroImage.value = resolveApiUrl(data.heroImage || '/static/stay-assets/hotel-vienna.jpg')
+ heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''
benefitAvailable.value = data.available === true
unavailableReason.value = data.unavailableReason || '该权益暂未开放'
orgs.value = data.organizations instanceof Array ? data.organizations as any[] : []
@@ -97,7 +97,7 @@
}
const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
const noop = () => {}
- const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' }
+ const replaceHero = () => { heroImage.value = '' }
const callOrg = (item: any) => { if (item.phone == null || item.phone == '') { uni.showToast({ title: '机构暂未提供咨询电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: item.phone, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
const showWindowInfo = (item: any) => {
if (!(item.windows instanceof Array) || item.windows.length == 0) return
diff --git a/rencaika/pages/benefit-search/benefit-search.uvue b/rencaika/pages/benefit-search/benefit-search.uvue
index e35755f..5258907 100644
--- a/rencaika/pages/benefit-search/benefit-search.uvue
+++ b/rencaika/pages/benefit-search/benefit-search.uvue
@@ -64,8 +64,8 @@
const navHeight = ref(32)
const navWidth = ref(88)
const navRight = ref(10)
- const fallbackImage = '/static/common/image-fallback.svg'
- const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage }
+ const fallbackImage = ''
+ const replaceImage = (item: any) => { if (item.image != '') item.image = '' }
const keyword = ref('')
const hasSearched = ref(false)
const isAllMode = ref(false)
@@ -120,7 +120,7 @@
merchantType: rawType,
discount: item.discountText != null ? item.discountText as string : formatRemoteDiscount(item),
address: item.address != null ? item.address as string : '',
- image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : '/static/common/image-fallback.svg',
+ image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : '',
sort: item.sort != null ? Number(item.sort) : 0,
detailType: item.detailType != null ? item.detailType as string : 'MERCHANT',
detailBusinessId: item.detailBusinessId != null ? '' + item.detailBusinessId : '' + item.id,
diff --git a/rencaika/pages/benefits/benefits.uvue b/rencaika/pages/benefits/benefits.uvue
index 300e2a0..34911d0 100644
--- a/rencaika/pages/benefits/benefits.uvue
+++ b/rencaika/pages/benefits/benefits.uvue
@@ -74,8 +74,8 @@
const navWidth = ref(88)
const navRight = ref(10)
const isSolidNav = ref(false)
- const fallbackImage = '/static/common/image-fallback.svg'
- const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage }
+ const fallbackImage = ''
+ const replaceImage = (item: any) => { if (item.image != '') item.image = '' }
onLoad((options: any) => {
// #ifdef MP-WEIXIN
@@ -119,8 +119,8 @@
{ id: 11, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg' },
{ id: 12, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg' }
]
- const visibleBenefits = ref([allBenefits[0], allBenefits[1], allBenefits[2], allBenefits[3]])
- const loadedBenefitCount = ref(4)
+ const visibleBenefits = ref<any[]>([])
+ const loadedBenefitCount = ref(0)
const currentPage = ref(0)
const initialLoading = ref(true)
const loadingMore = ref(false)
diff --git a/rencaika/pages/children-entrustment/children-entrustment.uvue b/rencaika/pages/children-entrustment/children-entrustment.uvue
index 64ba584..027087b 100644
--- a/rencaika/pages/children-entrustment/children-entrustment.uvue
+++ b/rencaika/pages/children-entrustment/children-entrustment.uvue
@@ -65,7 +65,7 @@
const navRight = ref(10)
const activeTab = ref(0)
const serviceId = ref('')
- const heroImage = ref('/static/stay-assets/hotel-vienna.jpg')
+ const heroImage = ref('')
const benefitTitle = ref('子女托管')
const applicableCards = ref('')
const description = ref('')
@@ -96,7 +96,7 @@
getChildrenEntrustmentBenefit(serviceId.value, (data: any) => {
benefitTitle.value = data.title || '子女托管'
applicableCards.value = data.applicableCards || '以服务说明为准'
- heroImage.value = resolveApiUrl(data.heroImage || '/static/stay-assets/hotel-vienna.jpg')
+ heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''
description.value = data.rightsText || '暂无使用说明'
benefitLoading.value = false
}, () => { benefitLoading.value = false; benefitError.value = true })
@@ -110,7 +110,7 @@
}
const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
const switchTab = (index: number) => { activeTab.value = index }
- const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' }
+ const replaceHero = () => { heroImage.value = '' }
const openSchoolDetail = (item: any) => {
if (item.detailAvailable !== true) { uni.showToast({ title: '学校介绍暂未开放', icon: 'none' }); return }
uni.navigateTo({ url: '/pages/school-detail/school-detail?id=' + item.id + '&name=' + encodeURIComponent(item.name) + '&address=' + encodeURIComponent(item.address) })
diff --git a/rencaika/pages/cinema-merchant/cinema-merchant.uvue b/rencaika/pages/cinema-merchant/cinema-merchant.uvue
index 174bdf0..3437a18 100644
--- a/rencaika/pages/cinema-merchant/cinema-merchant.uvue
+++ b/rencaika/pages/cinema-merchant/cinema-merchant.uvue
@@ -26,7 +26,7 @@
import { getSession } from '../../services/session.uts'
import { safeBack } from '../../utils/navigation.uts'
- const fallbackImage = '/static/entertainment-assets/wanda-cinema.webp'
+ const fallbackImage = ''
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
const isSolidNav = ref(false); const currentTab = ref('purchase'); const merchantId = ref(''); const talentId = ref('')
const cinemaName = ref('影城服务'); const cinemaAddress = ref(''); const cinemaHero = ref(fallbackImage); const cinemaMobile = ref(''); const cinemaLatitude = ref(0); const cinemaLongitude = ref(0)
diff --git a/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue b/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue
index 07baa49..c776994 100644
--- a/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue
+++ b/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue
@@ -48,7 +48,7 @@
const navRight = ref(10)
const isSolidNav = ref(false)
const windowWidth = ref(375)
- const fallbackImage = '/static/entertainment-assets/entertainment-hero.webp'
+ const fallbackImage = ''
const activeIndex = ref(-1)
const sceneScrollLeft = ref(0)
const sceneTrackWidth = ref(644)
diff --git a/rencaika/pages/finance-merchant/finance-merchant.uvue b/rencaika/pages/finance-merchant/finance-merchant.uvue
index 1dccc14..790d483 100644
--- a/rencaika/pages/finance-merchant/finance-merchant.uvue
+++ b/rencaika/pages/finance-merchant/finance-merchant.uvue
@@ -57,7 +57,7 @@
const discount = ref('')
const applicableCards = ref('')
const address = ref('')
- const heroImage = ref('/static/stay-assets/hotel-hampton.jpg')
+ const heroImage = ref('')
const benefitDetail = ref('')
const merchantPhone = ref('')
const specialistPhone = ref('')
@@ -92,7 +92,7 @@
discount.value = data.benefitSummary || '请咨询服务专员'
applicableCards.value = data.applicableCards || '请咨询服务专员'
address.value = data.address || '暂未配置机构地址'
- heroImage.value = resolveApiUrl(data.coverUrl || '/static/stay-assets/hotel-hampton.jpg')
+ heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : ''
benefitDetail.value = data.benefitDetail || '以服务专员说明为准'
merchantPhone.value = data.contactMobile || ''
longitude.value = Number(data.longitude)
@@ -106,7 +106,7 @@
}, () => { detailLoading.value = false; detailError.value = true })
}
const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
- const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' }
+ const replaceHero = () => { heroImage.value = '' }
const openLocation = () => { if (!canNavigate.value) { uni.showToast({ title: '暂未配置机构位置', icon: 'none' }); return }; uni.openLocation({ latitude: latitude.value, longitude: longitude.value, name: merchantName.value, address: address.value, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) }
const callMerchant = () => { if (merchantPhone.value == '') return; uni.makePhoneCall({ phoneNumber: merchantPhone.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
const contactSpecialist = () => {
diff --git a/rencaika/pages/medical-package/medical-package.uvue b/rencaika/pages/medical-package/medical-package.uvue
index bb166cc..5a347e7 100644
--- a/rencaika/pages/medical-package/medical-package.uvue
+++ b/rencaika/pages/medical-package/medical-package.uvue
@@ -21,7 +21,7 @@
import { safeBack } from '../../utils/navigation.uts'
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const isSolidNav = ref(false)
const packageId = ref(''); const hospitalId = ref(''); const detailLoading = ref(true); const detailError = ref(false); const itemsLoading = ref(false); const itemsLoaded = ref(false); const itemsError = ref(false)
- const packageName = ref(''); const heroImage = ref('/static/stay-assets/hotel-vienna.jpg'); const freeLabel = ref('免费'); const applicableCards = ref(''); const packageAvailable = ref(false); const unavailableReason = ref('该套餐暂不可预约')
+ const packageName = ref(''); const heroImage = ref(''); const freeLabel = ref('免费'); const applicableCards = ref(''); const packageAvailable = ref(false); const unavailableReason = ref('该套餐暂不可预约')
const hospitalName = ref(''); const hospitalAddress = ref(''); const hospitalPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const packageItems = ref<any[]>([])
const canNavigate = computed(() => latitude.value > 0 && longitude.value > 0)
const canBook = computed(() => !detailLoading.value && !detailError.value && packageAvailable.value)
@@ -34,12 +34,12 @@
const loadDetail = () => {
if (packageId.value == '') { detailLoading.value = false; detailError.value = true; return }
detailLoading.value = true; detailError.value = false
- getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = resolveApiUrl(data.heroImage || '/static/stay-assets/hotel-vienna.jpg'); freeLabel.value = data.freeLabel || '免费'; applicableCards.value = data.applicableCards || '以医院说明为准'; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true })
+ getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''; freeLabel.value = data.freeLabel || '免费'; applicableCards.value = data.applicableCards || '以医院说明为准'; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true })
}
const loadItems = () => { if (packageId.value == '') return; itemsLoading.value = true; itemsLoaded.value = false; itemsError.value = false; getMedicalPackageItems(packageId.value, (data: any) => { packageItems.value = data instanceof Array ? data as any[] : []; itemsLoading.value = false; itemsLoaded.value = true }, () => { itemsLoading.value = false; itemsError.value = true }) }
const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
const handleScroll = (event: any) => { const show = event.detail.scrollTop > 64; if (isSolidNav.value != show) isSolidNav.value = show }
- const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' }
+ const replaceHero = () => { heroImage.value = '' }
const openMap = () => { if (!canNavigate.value) { uni.showToast({ title: '医院暂未配置位置', icon: 'none' }); return }; uni.openLocation({ latitude: latitude.value, longitude: longitude.value, name: hospitalName.value, address: hospitalAddress.value, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) }
const callHospital = () => { if (hospitalPhone.value == '') { uni.showToast({ title: '医院暂未配置联系电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: hospitalPhone.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
const goBooking = () => { if (!canBook.value) { if (!detailLoading.value && !detailError.value) uni.showToast({ title: unavailableReason.value, icon: 'none' }); return }; uni.navigateTo({ url: '/pages/medical-booking/medical-booking?packageId=' + encodeURIComponent(packageId.value) + '&hospitalId=' + encodeURIComponent(hospitalId.value) + '&package=' + encodeURIComponent(packageName.value) + '&hospital=' + encodeURIComponent(hospitalName.value) }) }
diff --git a/rencaika/pages/merchant/merchant.uvue b/rencaika/pages/merchant/merchant.uvue
index 204f4ad..66373ab 100644
--- a/rencaika/pages/merchant/merchant.uvue
+++ b/rencaika/pages/merchant/merchant.uvue
@@ -156,10 +156,10 @@
const merchantId = ref('merchant-food-default')
const merchantType = ref('food')
const merchantName = ref('百杏厨房(中环天地店)')
- const merchantHero = ref('/static/food-assets/food-1.jpg')
- const fallbackImage = '/static/common/image-fallback.svg'
- const replaceHeroImage = () => { merchantHero.value = fallbackImage }
- const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage }
+ const merchantHero = ref('')
+ const fallbackImage = ''
+ const replaceHeroImage = () => { merchantHero.value = '' }
+ const replaceImage = (item: any) => { if (item.image != '') item.image = '' }
const merchantPrimaryTag = ref('券')
const merchantDiscount = ref('8.8折')
const merchantAddress = ref('四川省攀枝花市东区炳三区新福路20号中环天地负一层')
@@ -256,7 +256,7 @@
merchantType.value = 'medical'
isMedicalMerchant.value = true
merchantName.value = name
- merchantHero.value = '/static/stay-assets/hotel-hampton.jpg'
+ merchantHero.value = ''
merchantPrimaryTag.value = '免费'
merchantDiscount.value = ''
merchantAddress.value = '攀枝花市东区益康街34号4号综合3楼体检中心'
@@ -270,7 +270,7 @@
if (routedMerchantType == 'parking' || name.indexOf('停车') >= 0) {
isParkingMerchant.value = true
merchantName.value = '停车服务'
- merchantHero.value = '/static/home-assets/city-intro.jpg'
+ merchantHero.value = ''
merchantPrimaryTag.value = '免费'
merchantDiscount.value = ''
merchantAddress.value = '攀枝花市东区渡口公司管辖停车场'
@@ -279,7 +279,7 @@
usageInstructions.value = '登记信息后可享受渡口公司管辖的49处停车场免费停车服务'
} else {
merchantName.value = name == '加油服务' ? '中国石油东区新福路加油站' : name
- merchantHero.value = '/static/home-assets/top.jpg'
+ merchantHero.value = ''
merchantPrimaryTag.value = '券'
merchantDiscount.value = '9.4折'
merchantAddress.value = '四川省攀枝花市东区新福路20号附3-21号'
@@ -293,7 +293,7 @@
isHotelMerchant.value = true
merchantType.value = 'hotel'
merchantName.value = name
- merchantHero.value = routedImage == '' ? '/static/stay-assets/hotel-hampton.jpg' : routedImage
+ merchantHero.value = routedImage
merchantDiscount.value = routedDiscount == '' ? '6折' : routedDiscount
hasFreeVoucher.value = merchantDiscount.value.indexOf('免费') >= 0
merchantPrimaryTag.value = hasFreeVoucher.value ? '券' : '惠'
diff --git a/rencaika/pages/order-cancelled/order-cancelled.uvue b/rencaika/pages/order-cancelled/order-cancelled.uvue
index a39f8b8..16401b3 100644
--- a/rencaika/pages/order-cancelled/order-cancelled.uvue
+++ b/rencaika/pages/order-cancelled/order-cancelled.uvue
@@ -61,7 +61,7 @@
const talentId = ref('')
const loading = ref(true)
const loadError = ref(false)
- const productCoverUrl = ref('/static/home-assets/top.jpg')
+ const productCoverUrl = ref('')
const productName = ref('')
const originalAmount = ref('0.00')
const plannedPayAmount = ref('0.00')
@@ -100,7 +100,7 @@
loading.value = true
loadError.value = false
getCancelledOrder(orderId.value, talentId.value, (data: any) => {
- productCoverUrl.value = resolveApiUrl(data.productCoverUrlSnapshot || '/static/home-assets/top.jpg')
+ productCoverUrl.value = data.productCoverUrlSnapshot != null && data.productCoverUrlSnapshot != '' ? resolveApiUrl(data.productCoverUrlSnapshot as string) : ''
productName.value = data.productNameSnapshot || '人才权益服务'
originalAmount.value = Number(data.originalAmount).toFixed(2)
plannedPayAmount.value = Number(data.payAmount).toFixed(2)
diff --git a/rencaika/pages/premium-benefits/premium-benefits.uvue b/rencaika/pages/premium-benefits/premium-benefits.uvue
index 5d2cf7c..f4a7ebd 100644
--- a/rencaika/pages/premium-benefits/premium-benefits.uvue
+++ b/rencaika/pages/premium-benefits/premium-benefits.uvue
@@ -20,7 +20,7 @@
import { safeBack } from '../../utils/navigation.uts'
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const windowWidth = ref(375)
- const isSolidNav = ref(false); const fallbackImage = '/static/common/image-fallback.svg'; const fallbackScenes = [{ id: 'fallback-1', name: '臻享礼遇', image: '/static/stay-assets/hotel-hampton.jpg' }, { id: 'fallback-2', name: '人才服务', image: '/static/stay-assets/hotel-vienna.jpg' }]
+ const isSolidNav = ref(false); const fallbackImage = ''; const fallbackScenes = []
const topicTitle = ref('臻享礼遇'); const topicDescription = ref(''); const topicError = ref(false); const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>(fallbackScenes[0]); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644 + fallbackScenes.length * 137)
const serviceTabs = ref<any[]>([]); const activeTab = ref(''); const visibleServices = ref<any[]>([]); const initialLoading = ref(true); const loadingMore = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const initialError = ref(false); const loadMoreError = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false)
diff --git a/rencaika/pages/product-detail/product-detail.uvue b/rencaika/pages/product-detail/product-detail.uvue
index bda4c40..3070e77 100644
--- a/rencaika/pages/product-detail/product-detail.uvue
+++ b/rencaika/pages/product-detail/product-detail.uvue
@@ -60,9 +60,9 @@
const merchantId = ref('')
const productId = ref('')
const productName = ref('【川味盛宴】川菜12人餐')
- const productImage = ref('/static/food-assets/food-1.jpg')
- const fallbackImage = '/static/common/image-fallback.svg'
- const replaceProductImage = () => { productImage.value = fallbackImage }
+ const productImage = ref('')
+ const fallbackImage = ''
+ const replaceProductImage = () => { productImage.value = '' }
const productPrice = ref('138')
const productOriginalPrice = ref('266')
const productDiscount = ref('6折')
diff --git a/rencaika/pages/shopping-benefits/shopping-benefits.uvue b/rencaika/pages/shopping-benefits/shopping-benefits.uvue
index 3d77264..4bfbda6 100644
--- a/rencaika/pages/shopping-benefits/shopping-benefits.uvue
+++ b/rencaika/pages/shopping-benefits/shopping-benefits.uvue
@@ -24,10 +24,10 @@
import { safeBack } from '../../utils/navigation.uts'
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const windowWidth = ref(375)
- const isSolidNav = ref(false); const fallbackImage = '/static/common/image-fallback.svg'
- const fallbackScenes = [{ id: 'fallback-1', name: '惠享好物', image: '/static/home-assets/top.jpg' }, { id: 'fallback-2', name: '品质生活', image: '/static/home-assets/city-intro.jpg' }]
+ const isSolidNav = ref(false); const fallbackImage = ''
+ const fallbackScenes: any[] = []
const topicTitle = ref('惠享好物'); const topicDescription = ref(''); const topicError = ref(false)
- const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>(fallbackScenes[0]); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644 + fallbackScenes.length * 137)
+ const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>({ id: '', name: '', image: '' }); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644)
const serviceTabs = ref<any[]>([]); const activeTab = ref(''); const visibleServices = ref<any[]>([])
const initialLoading = ref(true); const loadingMore = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const initialError = ref(false); const loadMoreError = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false)
diff --git a/rencaika/pages/shopping-merchant/shopping-merchant.uvue b/rencaika/pages/shopping-merchant/shopping-merchant.uvue
index 7ba53e7..1148cad 100644
--- a/rencaika/pages/shopping-merchant/shopping-merchant.uvue
+++ b/rencaika/pages/shopping-merchant/shopping-merchant.uvue
@@ -21,7 +21,7 @@
import { safeBack } from '../../utils/navigation.uts'
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
- const merchantId = ref(''); const heroImage = ref('/static/common/image-fallback.svg'); const merchantName = ref(''); const discount = ref(''); const applicableCards = ref(''); const address = ref(''); const benefitDetail = ref('')
+ const merchantId = ref(''); const heroImage = ref(''); const merchantName = ref(''); const discount = ref(''); const applicableCards = ref(''); const address = ref(''); const benefitDetail = ref('')
const merchantPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const available = ref(false); const unavailableReason = ref('该购物服务当前不可用'); const advisorAvailable = ref(false); const advisorPhone = ref('')
const detailLoading = ref(true); const detailError = ref(false)
const canNavigate = () => latitude.value != 0 && longitude.value != 0
@@ -33,7 +33,7 @@
if (options != null && options['merchantId'] != null) merchantId.value = decodeURIComponent(options['merchantId'] as string)
loadMerchantDetail()
})
- const replaceHeroImage = () => { heroImage.value = '/static/common/image-fallback.svg' }
+ const replaceHeroImage = () => { heroImage.value = '' }
const loadMerchantDetail = () => {
detailLoading.value = true; detailError.value = false; available.value = false
if (merchantId.value == '' || !isApiConfigured()) { detailLoading.value = false; detailError.value = true; return }
@@ -43,7 +43,7 @@
discount.value = data.discountText != null ? data.discountText as string : '请咨询服务专员'
applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '请咨询服务专员'
address.value = data.address != null ? data.address as string : '请咨询服务专员'
- heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : '/static/common/image-fallback.svg'
+ heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : ''
benefitDetail.value = data.benefitDetail != null ? data.benefitDetail as string : '以服务专员说明为准'
merchantPhone.value = data.contactMobile != null ? data.contactMobile as string : ''
latitude.value = data.latitude != null ? Number(data.latitude) : 0; longitude.value = data.longitude != null ? Number(data.longitude) : 0
diff --git a/rencaika/pages/stay-benefits/stay-benefits.uvue b/rencaika/pages/stay-benefits/stay-benefits.uvue
index aab382a..a700bae 100644
--- a/rencaika/pages/stay-benefits/stay-benefits.uvue
+++ b/rencaika/pages/stay-benefits/stay-benefits.uvue
@@ -15,7 +15,7 @@
import { safeBack } from '../../utils/navigation.uts'
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
- const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = '/static/stay-assets/hotel-vienna.jpg'
+ const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = ''
const activeIndex = ref(-1); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644); const scenes = ref<any[]>([]); const currentScene = ref({ id: 'fallback', name: '', image: fallbackImage }); const topicDescription = ref('')
const activeTab = ref('hotel'); 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)
diff --git a/rencaika/pages/transport-benefits/transport-benefits.uvue b/rencaika/pages/transport-benefits/transport-benefits.uvue
index 7afd722..3901bea 100644
--- a/rencaika/pages/transport-benefits/transport-benefits.uvue
+++ b/rencaika/pages/transport-benefits/transport-benefits.uvue
@@ -63,10 +63,10 @@
import { safeBack } from '../../utils/navigation.uts'
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
- const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = '/static/home-assets/top.jpg'
- const fallbackScenes = [{ id: 'fallback-fuel', name: '人才加油服务', image: '/static/home-assets/top.jpg' }, { id: 'fallback-parking', name: '人才停车服务', image: '/static/home-assets/city-intro.jpg' }]
+ const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = ''
+ const fallbackScenes: any[] = []
const topicTitle = ref('行--畅行无忧'); const topicDescription = ref('整合加油、停车、洗车等出行服务资源,为人才提供专属折扣、停车优惠和便捷洗车服务,全方位保障人才出行便捷。'); const topicError = ref(false)
- const activeIndex = ref(0); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644 + fallbackScenes.length * 137); const scenes = ref<any[]>(fallbackScenes); const currentScene = ref(fallbackScenes[0])
+ 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 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)
diff --git a/rencaika/pages/travel-benefits/travel-benefits.uvue b/rencaika/pages/travel-benefits/travel-benefits.uvue
index 0e75795..eadd4b2 100644
--- a/rencaika/pages/travel-benefits/travel-benefits.uvue
+++ b/rencaika/pages/travel-benefits/travel-benefits.uvue
@@ -21,7 +21,7 @@
const navRight = ref(10)
const isSolidNav = ref(false)
const windowWidth = ref(375)
- const fallbackImage = '/static/home-assets/top.jpg'
+ const fallbackImage = ''
const activeIndex = ref(-1)
const sceneScrollLeft = ref(0)
const sceneTrackWidth = ref(644)
diff --git a/rencaika/static/benefits-assets/benefits-hero.jpg b/rencaika/static/benefits-assets/benefits-hero.jpg
deleted file mode 100644
index 2337a10..0000000
--- a/rencaika/static/benefits-assets/benefits-hero.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/card-assets/card-elite.jpg b/rencaika/static/card-assets/card-elite.jpg
deleted file mode 100644
index dd3b395..0000000
--- a/rencaika/static/card-assets/card-elite.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/card-assets/card-excellence.jpg b/rencaika/static/card-assets/card-excellence.jpg
deleted file mode 100644
index 43e05f2..0000000
--- a/rencaika/static/card-assets/card-excellence.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/card-assets/card-outstanding.jpg b/rencaika/static/card-assets/card-outstanding.jpg
deleted file mode 100644
index 393fc70..0000000
--- a/rencaika/static/card-assets/card-outstanding.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/card-assets/talent-card-background.jpg b/rencaika/static/card-assets/talent-card-background.jpg
deleted file mode 100644
index bdeafa8..0000000
--- a/rencaika/static/card-assets/talent-card-background.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/common/image-fallback.svg b/rencaika/static/common/image-fallback.svg
deleted file mode 100644
index 379ef0e..0000000
--- a/rencaika/static/common/image-fallback.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="320" height="200" viewBox="0 0 320 200">
- <rect width="320" height="200" fill="#eef1f5"/>
- <path d="M92 132l38-42 28 29 21-24 49 37H92z" fill="#c7d0dc"/>
- <circle cx="119" cy="69" r="15" fill="#c7d0dc"/>
- <text x="160" y="174" text-anchor="middle" fill="#8d98a8" font-family="sans-serif" font-size="14">图片加载失败</text>
-</svg>
diff --git a/rencaika/static/entertainment-assets/entertainment-hero.webp b/rencaika/static/entertainment-assets/entertainment-hero.webp
deleted file mode 100644
index bccf187..0000000
--- a/rencaika/static/entertainment-assets/entertainment-hero.webp
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/entertainment-assets/wanda-cinema.webp b/rencaika/static/entertainment-assets/wanda-cinema.webp
deleted file mode 100644
index 5eeddcb..0000000
--- a/rencaika/static/entertainment-assets/wanda-cinema.webp
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/food-assets/food-1.jpg b/rencaika/static/food-assets/food-1.jpg
deleted file mode 100644
index 2b287aa..0000000
--- a/rencaika/static/food-assets/food-1.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/food-assets/food-2.jpg b/rencaika/static/food-assets/food-2.jpg
deleted file mode 100644
index 7d7084c..0000000
--- a/rencaika/static/food-assets/food-2.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/city-golden.webp b/rencaika/static/home-assets/city-golden.webp
deleted file mode 100644
index a84fe63..0000000
--- a/rencaika/static/home-assets/city-golden.webp
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/city-industry.webp b/rencaika/static/home-assets/city-industry.webp
deleted file mode 100644
index d855aef..0000000
--- a/rencaika/static/home-assets/city-industry.webp
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/city-intro.jpg b/rencaika/static/home-assets/city-intro.jpg
deleted file mode 100644
index 74b36bf..0000000
--- a/rencaika/static/home-assets/city-intro.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/city-livable.jpg b/rencaika/static/home-assets/city-livable.jpg
deleted file mode 100644
index 2ce650b..0000000
--- a/rencaika/static/home-assets/city-livable.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/city-potential.jpg b/rencaika/static/home-assets/city-potential.jpg
deleted file mode 100644
index bfe528e..0000000
--- a/rencaika/static/home-assets/city-potential.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/home-banner-v2.jpg b/rencaika/static/home-assets/home-banner-v2.jpg
deleted file mode 100644
index f0a908f..0000000
--- a/rencaika/static/home-assets/home-banner-v2.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/top.jpg b/rencaika/static/home-assets/top.jpg
deleted file mode 100644
index 3918faf..0000000
--- a/rencaika/static/home-assets/top.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/news-assets/meeting-thumb.jpg b/rencaika/static/news-assets/meeting-thumb.jpg
deleted file mode 100644
index 2612195..0000000
--- a/rencaika/static/news-assets/meeting-thumb.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-explanation.jpg b/rencaika/static/policy-assets/policy-explanation.jpg
deleted file mode 100644
index 0d6c6dc..0000000
--- a/rencaika/static/policy-assets/policy-explanation.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-hero.jpg b/rencaika/static/policy-assets/policy-hero.jpg
deleted file mode 100644
index 24c9d48..0000000
--- a/rencaika/static/policy-assets/policy-hero.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-hampton.jpg b/rencaika/static/stay-assets/hotel-hampton.jpg
deleted file mode 100644
index 00cdae0..0000000
--- a/rencaika/static/stay-assets/hotel-hampton.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-vienna.jpg b/rencaika/static/stay-assets/hotel-vienna.jpg
deleted file mode 100644
index cc122d0..0000000
--- a/rencaika/static/stay-assets/hotel-vienna.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika_merchant/pages/merchant-onboarding/merchant-onboarding.uvue b/rencaika_merchant/pages/merchant-onboarding/merchant-onboarding.uvue
index bae5b9a..67a8d99 100644
--- a/rencaika_merchant/pages/merchant-onboarding/merchant-onboarding.uvue
+++ b/rencaika_merchant/pages/merchant-onboarding/merchant-onboarding.uvue
@@ -316,7 +316,7 @@
submitting.value = true
const contactName = merchantType.value == 'enterprise' ? administratorName.value.trim() : subjectName.value.trim()
const contactMobile = merchantType.value == 'enterprise' ? administratorPhone.value.trim() : personalPhone.value.trim()
- submitOnboardingRemote({ subjectType: merchantType.value, subjectName: subjectName.value.trim(), creditCode: creditCode.value.trim(), subjectIdNumber: subjectIdNumber.value.trim(), registeredRegion: registeredRegion.value.trim(), registeredAddress: registeredAddress.value.trim(), contactName: contactName, contactMobile: contactMobile }, 'onboarding-' + Date.now(), () => {
+ submitOnboardingRemote({ subjectType: merchantType.value, subjectName: subjectName.value.trim(), creditCode: creditCode.value.trim(), subjectIdNumber: subjectIdNumber.value.trim(), subjectCertificateValidityType: subjectValidity.value == '长期' ? 'long_term' : 'fixed', subjectCertificateBeginDate: subjectStartDate.value, registeredRegion: registeredRegion.value.trim(), registeredAddress: registeredAddress.value.trim(), contactName: contactName, contactMobile: contactMobile }, 'onboarding-' + Date.now(), () => {
submitting.value = false
uni.redirectTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' })
}, (error: any) => {
diff --git a/talent-card-server/scripts/repair-local-mysql-v92.sql b/talent-card-server/scripts/repair-local-mysql-v92.sql
new file mode 100644
index 0000000..960f4fa
--- /dev/null
+++ b/talent-card-server/scripts/repair-local-mysql-v92.sql
@@ -0,0 +1,6 @@
+-- Run only when an existing local MySQL database failed V92__admin_approval_organization_extra_info.sql.
+-- V92 may have added columns before failing on the final update statement; delete the failed
+-- Flyway history row so the fixed idempotent migration can run again.
+
+delete from flyway_schema_history
+where version = '92' and success = 0;
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
index c2d5c36..d7359f7 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -5,8 +5,11 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+
+import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.BusinessIdGenerator;
import com.talentcard.server.business.DatabaseRows;
@@ -128,6 +131,8 @@
}
private List<Map<String, Object>> rules(Long merchantId, String cardLevel) {
+ List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel);
+ if (!configured.isEmpty()) return configured;
return rows("""
select id, card_level, rule_name, discount_rate, subsidy_enabled
from merchant_store_consume_rule
@@ -144,7 +149,48 @@
}).toList();
}
+ private List<Map<String, Object>> configuredRules(Long merchantId, String cardLevel) {
+ List<Map<String, Object>> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId);
+ if (merchants.isEmpty()) return List.of();
+ Object raw = merchants.get(0).get("merchantRightsConfig");
+ if (raw == null || string(raw).isBlank()) return List.of();
+ List<Map<String, Object>> configs = new ArrayList<>();
+ try {
+ if (raw instanceof List<?> list) {
+ for (Object item : list) if (item instanceof Map<?, ?> map) {
+ Map<String, Object> value = new LinkedHashMap<>();
+ for (Map.Entry<?, ?> entry : map.entrySet()) value.put(String.valueOf(entry.getKey()), entry.getValue());
+ configs.add(value);
+ }
+ } else {
+ for (Object item : JSON.parseArray(string(raw), Map.class)) if (item instanceof Map<?, ?> map) {
+ Map<String, Object> value = new LinkedHashMap<>();
+ for (Map.Entry<?, ?> entry : map.entrySet()) value.put(String.valueOf(entry.getKey()), entry.getValue());
+ configs.add(value);
+ }
+ }
+ } catch (Exception ignored) {
+ return List.of();
+ }
+ return configs.stream().filter(right -> booleanValue(right.get("enabled")) && booleanValue(right.get("consumeEnabled")) && booleanValue(right.get("storeConsume")) && matchesCardLevel(string(right.get("level")), cardLevel)).map(this::configuredRule).filter(rule -> decimal(rule.get("discountRate")).signum() > 0).toList();
+ }
+
+ private Map<String, Object> configuredRule(Map<String, Object> right) {
+ String level = normalizedCardLevel(string(right.get("level")));
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("id", configRuleId(level));
+ result.put("cardLevel", level);
+ result.put("ruleName", string(right.get("tag")).isBlank() ? "到店消费优惠" : right.get("tag"));
+ result.put("name", result.get("ruleName"));
+ result.put("discountRate", configuredDiscountRate(right.get("discount")));
+ result.put("subsidyEnabled", booleanValue(right.get("governmentSubsidy")) ? 1 : 0);
+ result.put("consumeDesc", right.get("consumeDesc"));
+ return result;
+ }
+
private Map<String, Object> rule(Long merchantId, String cardLevel, Long ruleId) {
+ List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel);
+ for (Map<String, Object> candidate : configured) if (String.valueOf(candidate.get("id")).equals(String.valueOf(ruleId))) return candidate;
List<Map<String, Object>> found = rows("""
select id, card_level, rule_name, discount_rate, subsidy_enabled
from merchant_store_consume_rule
@@ -152,18 +198,34 @@
and (valid_end is null or valid_end >= current_date)
limit 1
""", ruleId, merchantId);
- if (found.isEmpty() || !matchesCardLevel(string(found.get(0).get("cardLevel")), cardLevel)) {
- throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不可使用所选优惠方式");
- }
+ if (found.isEmpty() || !matchesCardLevel(string(found.get(0).get("cardLevel")), cardLevel)) throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不可使用所选优惠方式");
return found.get(0);
+ }
+
+ private long configRuleId(String level) {
+ return switch (level) { case "EXCELLENT" -> -1L; case "OUTSTANDING" -> -2L; case "ELITE" -> -3L; default -> -9L; };
+ }
+
+ private BigDecimal configuredDiscountRate(Object value) {
+ try {
+ BigDecimal discount = decimal(string(value));
+ return discount.signum() > 0 && discount.compareTo(BigDecimal.TEN) <= 0 ? discount.divide(BigDecimal.TEN, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
+ } catch (Exception ignored) {
+ return BigDecimal.ZERO;
+ }
+ }
+
+ private boolean booleanValue(Object value) {
+ if (value instanceof Boolean bool) return bool;
+ if (value instanceof Number number) return number.intValue() != 0;
+ String text = string(value).toLowerCase();
+ return "true".equals(text) || "1".equals(text) || "yes".equals(text);
}
private Map<String, Object> quote(Map<String, Object> rule, BigDecimal inputAmount) {
long originalAmount = inputAmount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact();
BigDecimal rate = decimal(rule.get("discountRate"));
- if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) {
- throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效");
- }
+ if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效");
long payAmount = BigDecimal.valueOf(originalAmount).multiply(rate).setScale(0, RoundingMode.HALF_UP).longValueExact();
long discountAmount = originalAmount - payAmount;
long subsidyAmount = number(rule.get("subsidyEnabled")) == 1 ? discountAmount : 0;
@@ -175,7 +237,6 @@
result.put("ruleName", rule.get("ruleName"));
return result;
}
-
private Map<String, Object> merchant(MerchantPrincipal principal) {
List<Map<String, Object>> merchants = rows("""
select merchant_name, address, contact_mobile, longitude, latitude
@@ -208,6 +269,8 @@
select card_level, benefit_text from merchant_card_benefit
where merchant_id = ? and status = 1 order by sort_no desc, id desc
""", merchantId);
+ String configuredDescription = configuredRules(merchantId, cardLevel).stream().map(rule -> string(rule.get("consumeDesc"))).filter(value -> !value.isBlank()).findFirst().orElse("");
+ if (!configuredDescription.isBlank()) return configuredDescription;
return benefits.stream().filter(benefit -> matchesCardLevel(string(benefit.get("cardLevel")), cardLevel))
.map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准");
}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingRequest.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingRequest.java
index bfec84f..0c14dfb 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingRequest.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingRequest.java
@@ -9,8 +9,10 @@
@NotBlank @Size(max = 128) String subjectName,
@Size(max = 32) String creditCode,
@Size(max = 32) String subjectIdNumber,
+ @Pattern(regexp = "|fixed|long_term") String subjectCertificateValidityType,
+ @Pattern(regexp = "|\\d{4}-\\d{2}-\\d{2}") String subjectCertificateBeginDate,
@Size(max = 128) String registeredRegion,
@Size(max = 255) String registeredAddress,
@NotBlank @Size(max = 64) String contactName,
@NotBlank @Pattern(regexp = "1\\d{10}") String contactMobile
-) {}
+) {}
\ No newline at end of file
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
index 50f2981..e6014b5 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
@@ -11,6 +11,10 @@
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.PageResult;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
+import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingCommand;
+import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway;
+import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
+import com.talentcard.server.common.SensitiveDataCrypto;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -20,8 +24,14 @@
@Service
public class MerchantSettlementService {
private final JdbcTemplate jdbc;
+ private final SensitiveDataCrypto sensitiveDataCrypto;
+ private final HuifuIndividualOnboardingGateway individualOnboardingGateway;
- public MerchantSettlementService(JdbcTemplate jdbc) { this.jdbc = jdbc; }
+ public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway) {
+ this.jdbc = jdbc;
+ this.sensitiveDataCrypto = sensitiveDataCrypto;
+ this.individualOnboardingGateway = individualOnboardingGateway;
+ }
@Transactional(readOnly = true)
public Map<String, Object> onboardingStatus(MerchantPrincipal principal) {
@@ -41,10 +51,23 @@
if (enterprise && (request.creditCode() == null || !request.creditCode().trim().matches("[0-9A-Z]{18}"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入18位统一社会信用代码");
if (!enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的身份证号");
String no = BusinessIdGenerator.next("MO");
- jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, subject_id_no_masked, registered_region, registered_address, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), maskId(request.subjectIdNumber()), clean(request.registeredRegion()), clean(request.registeredAddress()), request.contactName().trim(), request.contactMobile().trim(), key.trim());
+ String certificateValidityType = certificateValidityType(request.subjectCertificateValidityType());
+ String certificateBeginDate = certificateBeginDate(request.subjectCertificateBeginDate());
+ jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, registered_region, registered_address, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, clean(request.registeredRegion()), clean(request.registeredAddress()), request.contactName().trim(), request.contactMobile().trim(), key.trim());
+ if ("individual".equals(request.subjectType())) submitIndividualOnboarding(no, request);
return onboarding(rows("select * from merchant_onboarding_application where application_no = ?", no).get(0));
}
+ private void submitIndividualOnboarding(String applicationNo, MerchantOnboardingRequest request) {
+ try {
+ HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand(applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), request.contactMobile().trim(), clean(request.registeredAddress())));
+ if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo);
+ else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result), applicationNo);
+ else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo);
+ } catch (HuifuPaymentGatewayException exception) {
+ jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo);
+ }
+ }
@Transactional(readOnly = true)
public List<Map<String, Object>> bankCards(MerchantPrincipal principal) {
store(principal);
@@ -117,6 +140,9 @@
private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
private Long number(String value) { try { return Long.valueOf(value); } catch (Exception ignored) { throw new BusinessException(ErrorCode.BAD_REQUEST, "编号无效"); } }
private String clean(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
+ private String certificateValidityType(String value) { return "long_term".equals(value) ? "1" : "0"; }
+ private String certificateBeginDate(String value) { return clean(value).replace("-", ""); }
+ private String providerReason(HuifuIndividualOnboardingGateway.Result result) { String value = clean(result.message()); return value.isEmpty() ? "Huifu onboarding rejected: " + clean(result.code()) : value; }
private String maskId(String id) { String value = clean(id); return value.length() < 8 ? "" : value.substring(0, 3) + "********" + value.substring(value.length() - 4); }
private String sha256(String value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(); for (byte valueByte : digest) result.append(String.format("%02x", valueByte)); return result.toString(); } catch (Exception exception) { throw new IllegalStateException("Unable to hash bank card", exception); } }
}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java
new file mode 100644
index 0000000..4962c8a
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingCommand.java
@@ -0,0 +1,3 @@
+package com.talentcard.server.business.payment.huifu;
+
+public record HuifuIndividualOnboardingCommand(String applicationNo, String name, String idCardNo, String certificateValidityType, String certificateBeginDate, String mobileNo, String address) {}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
new file mode 100644
index 0000000..86d07b0
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
@@ -0,0 +1,81 @@
+package com.talentcard.server.business.payment.huifu;
+
+import com.alibaba.fastjson.JSON;
+import com.huifu.bspay.sdk.opps.client.BasePayClient;
+import com.huifu.bspay.sdk.opps.core.BasePay;
+import com.huifu.bspay.sdk.opps.core.config.MerConfig;
+import com.huifu.bspay.sdk.opps.core.request.V2UserBasicdataIndvRequest;
+import com.talentcard.server.common.BusinessException;
+import com.talentcard.server.common.ErrorCode;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import org.springframework.stereotype.Service;
+
+@Service
+public class HuifuIndividualOnboardingGateway {
+ private static final Object SDK_LOCK = new Object();
+ private final HuifuPaymentProperties properties;
+
+ public HuifuIndividualOnboardingGateway(HuifuPaymentProperties properties) { this.properties = properties; }
+
+ public Result create(HuifuIndividualOnboardingCommand command) {
+ ensureConfigured();
+ V2UserBasicdataIndvRequest request = new V2UserBasicdataIndvRequest();
+ request.setReqSeqId(command.applicationNo());
+ request.setReqDate(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE));
+ request.setName(command.name());
+ request.setCertType("00");
+ request.setCertNo(command.idCardNo());
+ request.setCertValidityType(command.certificateValidityType());
+ request.setCertBeginDate(command.certificateBeginDate());
+ request.setCertNationality("CHN");
+ request.setMobileNo(command.mobileNo());
+ if (!blank(command.address())) request.setAddress(command.address());
+ try {
+ Map<String, Object> response;
+ synchronized (SDK_LOCK) {
+ initializeSdk();
+ response = BasePayClient.request(request, false);
+ }
+ return Result.from(response);
+ } catch (BusinessException exception) {
+ throw exception;
+ } catch (Exception exception) {
+ throw new HuifuPaymentGatewayException("Huifu individual onboarding request failed", outcomeUnknown(exception), exception);
+ }
+ }
+
+ private void ensureConfigured() {
+ if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled");
+ if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu onboarding configuration is incomplete");
+ }
+
+ private void initializeSdk() throws Exception {
+ BasePay.debug = false;
+ BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST;
+ MerConfig config = new MerConfig();
+ config.setSysId(properties.getSystemId().trim());
+ config.setProductId(properties.getProductId().trim());
+ config.setRsaPrivateKey(properties.getPrivateKey().trim());
+ config.setRsaPublicKey(properties.getPlatformPublicKey().trim());
+ BasePay.initWithMerConfig(config);
+ }
+
+ private boolean blank(String value) { return value == null || value.isBlank(); }
+ private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); }
+
+ @SuppressWarnings("unchecked")
+ public record Result(boolean accepted, String huifuId, String code, String message) {
+ static Result from(Map<String, Object> response) {
+ Object raw = response.get("data");
+ Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response;
+ String code = value(data, "sub_resp_code", "resp_code", "code");
+ String message = value(data, "sub_resp_desc", "resp_desc", "message");
+ String huifuId = value(data, "huifu_id", "huifuId");
+ return new Result("00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code), huifuId, code, message);
+ }
+
+ private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; }
+ }
+}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/ProductPriceCalculator.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/ProductPriceCalculator.java
new file mode 100644
index 0000000..5bba732
--- /dev/null
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/ProductPriceCalculator.java
@@ -0,0 +1,138 @@
+package com.talentcard.server.business.portal;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
+
+final class ProductPriceCalculator {
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final TypeReference<List<Map<String, Object>>> RIGHTS_TYPE = new TypeReference<>() {};
+
+ private ProductPriceCalculator() {
+ }
+
+ static ProductPrice calculate(Object originalPrice, Object salePrice, Object rightsConfig, Object cardLevel) {
+ return calculate(originalPrice, salePrice, rightsConfig, cardLevel, 0);
+ }
+
+ static ProductPrice calculate(Object originalPrice, Object salePrice, Object rightsConfig, Object cardLevel, int usedFreeCount) {
+ BigDecimal fallbackSale = money(salePrice);
+ BigDecimal original = money(originalPrice);
+ if (original.signum() <= 0) {
+ original = fallbackSale;
+ }
+ BigDecimal calculatedSale = fallbackSale.signum() > 0 ? fallbackSale : original;
+ Map<String, Object> right = matchingRight(rightsConfig, cardLevel);
+ if (right != null && original.signum() > 0) {
+ boolean free = booleanValue(right.get("free"));
+ int limitCount = intValue(right.get("limitCount"));
+ boolean freeAvailable = free && (limitCount <= 0 || usedFreeCount < limitCount);
+ if (freeAvailable) {
+ calculatedSale = BigDecimal.ZERO;
+ } else {
+ BigDecimal discount = discountValue(right.get(free ? "exceedDiscount" : "discount"));
+ if (discount.signum() > 0) {
+ calculatedSale = original.multiply(discount)
+ .divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP);
+ }
+ }
+ }
+ return new ProductPrice(original, calculatedSale.setScale(2, RoundingMode.HALF_UP), right != null);
+ }
+
+ static ProductRight productRight(Object rightsConfig, Object cardLevel) {
+ Map<String, Object> right = matchingRight(rightsConfig, cardLevel);
+ if (right == null) return null;
+ return new ProductRight(
+ booleanValue(right.get("free")),
+ intValue(right.get("limitCount")),
+ text(right.get("resetCycle")));
+ }
+
+ private static Map<String, Object> matchingRight(Object rightsConfig, Object cardLevel) {
+ String normalizedCardLevel = normalizeCardLevel(cardLevel);
+ if (normalizedCardLevel.isBlank() || rightsConfig == null) return null;
+ for (Map<String, Object> right : parseRights(rightsConfig)) {
+ if (!booleanValue(right.get("enabled"))) continue;
+ if (normalizedCardLevel.equals(normalizeCardLevel(right.get("level")))) return right;
+ }
+ return null;
+ }
+
+ private static List<Map<String, Object>> parseRights(Object value) {
+ if (value instanceof List<?> list) {
+ return list.stream()
+ .filter(Map.class::isInstance)
+ .map(item -> (Map<String, Object>) item)
+ .toList();
+ }
+ String text = String.valueOf(value).trim();
+ if (text.isBlank()) return List.of();
+ try {
+ return OBJECT_MAPPER.readValue(text, RIGHTS_TYPE);
+ } catch (Exception ignored) {
+ return List.of();
+ }
+ }
+
+ private static String normalizeCardLevel(Object value) {
+ if (value == null) return "";
+ String raw = String.valueOf(value).trim();
+ String upper = raw.toUpperCase(Locale.ROOT);
+ if (upper.contains("EXCELLENT") || raw.contains("\u5353\u8d8a")) return "EXCELLENT";
+ if (upper.contains("OUTSTANDING") || raw.contains("\u6770\u51fa")) return "OUTSTANDING";
+ if (upper.contains("ELITE") || raw.contains("\u83c1\u82f1") || raw.contains("\u7cbe\u82f1")) return "ELITE";
+ return upper;
+ }
+
+ private static boolean booleanValue(Object value) {
+ if (value instanceof Boolean bool) return bool;
+ if (value instanceof Number number) return number.intValue() != 0;
+ String text = value == null ? "" : String.valueOf(value).trim();
+ return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text);
+ }
+
+ private static int intValue(Object value) {
+ if (value instanceof Number number) return number.intValue();
+ try {
+ String text = value == null ? "" : String.valueOf(value).trim();
+ if (text.isBlank()) return 0;
+ return new BigDecimal(text).intValue();
+ } catch (Exception ignored) {
+ return 0;
+ }
+ }
+
+ private static String text(Object value) {
+ return value == null ? "" : String.valueOf(value).trim();
+ }
+
+ private static BigDecimal discountValue(Object value) {
+ BigDecimal discount = money(value);
+ if (discount.compareTo(BigDecimal.ZERO) <= 0 || discount.compareTo(BigDecimal.TEN) > 0) {
+ return BigDecimal.ZERO;
+ }
+ return discount;
+ }
+
+ private static BigDecimal money(Object value) {
+ if (value == null) return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ try {
+ if (value instanceof BigDecimal decimal) return decimal.setScale(2, RoundingMode.HALF_UP);
+ return new BigDecimal(String.valueOf(value).trim()).setScale(2, RoundingMode.HALF_UP);
+ } catch (Exception ignored) {
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
+ }
+ }
+
+ record ProductPrice(BigDecimal originalPrice, BigDecimal salePrice, boolean personalized) {
+ }
+
+ record ProductRight(boolean free, int limitCount, String resetCycle) {
+ }
+}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/TalentCardPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/TalentCardPortalService.java
index f3c9422..499767c 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/TalentCardPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/TalentCardPortalService.java
@@ -132,7 +132,7 @@
String level = value(card.get("cardLevel"), "人才卡");
result.put("cardLevel", level);
result.put("familyCardLimit", ((Number) card.getOrDefault("familyCardLimit", 0)).intValue());
- if (!"卓越人才卡".equals(level)) {
+ if (!isExcellenceCardLevel(level)) {
result.put("unavailableReason", "家庭附属卡仅向卓越人才卡用户开放");
return result;
}
@@ -286,6 +286,12 @@
return java.util.Optional.of(result);
}
+ private boolean isExcellenceCardLevel(String level) {
+ if (level == null) return false;
+ String normalized = level.trim().toUpperCase(Locale.ROOT);
+ return "EXCELLENCE".equals(normalized) || level.contains("卓越") || level.contains("鍗撹秺");
+ }
+
private String cardStatus(Map<String, Object> card) {
if (((Number) card.getOrDefault("talentStatus", 1)).intValue() != 1) return "FROZEN";
if (((Number) card.getOrDefault("cardStatus", 0)).intValue() == 0) return "FROZEN";
diff --git a/talent-card-server/src/main/resources/application.yml b/talent-card-server/src/main/resources/application.yml
index 9516b0e..a7ce1e2 100644
--- a/talent-card-server/src/main/resources/application.yml
+++ b/talent-card-server/src/main/resources/application.yml
@@ -23,7 +23,7 @@
repositories:
enabled: false
flyway:
- enabled: true
+ enabled: false
locations: classpath:db/migration
baseline-on-migrate: true
jpa:
diff --git a/talent-card-server/src/main/resources/db/migration/V93__merchant_onboarding_huifu_kyc.sql b/talent-card-server/src/main/resources/db/migration/V93__merchant_onboarding_huifu_kyc.sql
new file mode 100644
index 0000000..d47c175
--- /dev/null
+++ b/talent-card-server/src/main/resources/db/migration/V93__merchant_onboarding_huifu_kyc.sql
@@ -0,0 +1,3 @@
+alter table merchant_onboarding_application add column subject_id_no_encrypted varchar(512) default null after subject_id_no_masked;
+alter table merchant_onboarding_application add column subject_cert_validity_type varchar(8) default null after subject_id_no_encrypted;
+alter table merchant_onboarding_application add column subject_cert_begin_date char(8) default null after subject_cert_validity_type;
--
Gitblit v1.7.1