From 348ea7313f2f6681de2b0693e53c59edaf202bd1 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期五, 21 八月 2026 17:34:13 +0800
Subject: [PATCH] fix: complete miniapp benefit and coupon flows
---
talent-card-server/src/main/java/com/talentcard/server/business/portal/UsageRecordPortalService.java | 2
rencaika/docs/pages/51-行政审批.md | 8 +-
rencaika/pages/admin-approval/admin-approval.uvue | 59 ++++++--------
rencaika/pages/stay-benefits/stay-benefits.uvue | 6 +
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantTypeCatalog.java | 4
rencaika/pages/coupon-received/coupon-received.uvue | 12 ++
rencaika/pages/merchant/merchant.uvue | 6 +
talent-card-server/src/main/resources/db/migration/V120__add_telecom_and_dental_shopping_categories.sql | 11 ++
rencaika/pages/ticket-package/ticket-package.uvue | 3
rencaika/pages/shopping-benefits/shopping-benefits.uvue | 11 ++
talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java | 67 +++++++++++++++-
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantTypeCatalogTests.java | 4 +
talent-card-server/src/main/java/com/talentcard/server/business/portal/CinemaPortalService.java | 29 +++++++
13 files changed, 166 insertions(+), 56 deletions(-)
diff --git "a/rencaika/docs/pages/51-\350\241\214\346\224\277\345\256\241\346\211\271.md" "b/rencaika/docs/pages/51-\350\241\214\346\224\277\345\256\241\346\211\271.md"
index cdaaa47..c98885d 100644
--- "a/rencaika/docs/pages/51-\350\241\214\346\224\277\345\256\241\346\211\271.md"
+++ "b/rencaika/docs/pages/51-\350\241\214\346\224\277\345\256\241\346\211\271.md"
@@ -6,7 +6,7 @@
- 路由:`/pages/admin-approval/admin-approval`
- 源码:`pages/admin-approval/admin-approval.uvue`
- 入口:臻享礼遇页“行政审批”分类点击服务项进入。
-- 当前状态:视觉与本地交互首版已完成,真实机构列表、咨询电话与窗口信息接口待接入。
+- 当前状态:已接入行政审批详情接口;窗口信息直接打开后台上传的 PDF 文件。
## 功能点
@@ -16,14 +16,14 @@
| 权益标识 | 标题“行政审批”与“免费”标签固定展示。 | 后台返回权益名称与免费/付费标识。 |
| 机构列表 | 展示机构名称、地址、权益内容说明和咨询电话,内容超出时区内滚动;已补加载、空、失败和重试。 | 后台返回机构 ID、名称、地址、经纬度、纯文本权益说明、联系电话、排序和启用状态。 |
| 咨询电话 | 点击“咨询电话-号码”直接调起系统拨号。 | 号码由后台返回,为空时隐藏电话入口。 |
-| 窗口信息 | 有窗口信息的机构展示“查看窗口信息”按钮,点击弹出窗口与办公时间说明。 | 后台返回窗口数组(窗口名称、楼层位置)、办公时间和是否展示入口。 |
+| 窗口信息 | 有 PDF 文件的机构展示“查看窗口信息”按钮,点击下载并打开 PDF 阅读器。 | 后台返回 `windowInfoUrl`(后台上传 PDF 的 URL);没有文件时隐藏入口。 |
## 操作说明
1. 用户从臻享礼遇页选择“行政审批”分类,点击任意服务项进入本页。
2. 浏览各政务服务机构的地址和权益内容,机构较多时上滑浏览。
3. 点击“咨询电话-号码”调起系统拨号。
-4. 点击“查看窗口信息”查看办理窗口位置和办公时间,点击遮罩或“我知道了”关闭。
+4. 点击“查看窗口信息”下载并打开后台上传的窗口信息 PDF。
5. 点击左上角返回按钮回到臻享礼遇页。
## 异常处理
@@ -40,4 +40,4 @@
- 权益信息:权益 ID、名称、免费/付费标识、头图 URL、启用状态。
- 机构列表:机构 ID、名称、地址、经纬度、纯文本权益说明、联系电话、排序、启用状态。
-- 窗口信息:机构 ID、窗口数组(窗口名称、所在楼层/位置)、办公时间、是否展示窗口入口。
+- 窗口信息:机构 ID、`windowInfoUrl`(PDF 地址)、是否展示窗口入口。
diff --git a/rencaika/pages/admin-approval/admin-approval.uvue b/rencaika/pages/admin-approval/admin-approval.uvue
index 220cb09..4366db4 100644
--- a/rencaika/pages/admin-approval/admin-approval.uvue
+++ b/rencaika/pages/admin-approval/admin-approval.uvue
@@ -24,22 +24,13 @@
<view class="org-desc"><text>{{item.description}}</text></view>
<view class="org-footer">
<view v-if="item.phone != ''" class="phone-row" @tap="callOrg(item)"><image src="/static/premium-assets/phone.png" mode="aspectFit"></image><text>咨询电话-{{item.phone}}</text></view>
- <view v-if="item.hasWindowInfo" class="window-button" hover-class="window-button-active" @tap="showWindowInfo(item)"><text>查看窗口信息</text></view>
+ <view v-if="item.hasWindowInfo" class="window-button" hover-class="window-button-active" @tap="openWindowInfo(item)"><text>查看窗口信息</text></view>
</view>
</view>
<view class="bottom-space"></view>
</scroll-view>
</view>
- <view v-if="popupVisible" class="popup-mask" @tap="hideWindowInfo">
- <view class="popup-card" @tap.stop="noop">
- <text class="popup-title">窗口信息</text>
- <text class="popup-org">{{popupOrgName}}</text>
- <view v-for="window in popupWindows" :key="window.id" class="popup-row"><view class="popup-dot"></view><text>{{window.name}}{{window.location != '' ? '(' + window.location + ')' : ''}}</text></view>
- <text class="popup-time">办公时间:{{popupHours}}</text>
- <view class="popup-close" hover-class="popup-close-active" @tap="hideWindowInfo"><text>我知道了</text></view>
- </view>
- </view>
</view>
</template>
@@ -62,10 +53,6 @@
const listLoading = ref(true)
const listLoaded = ref(false)
const listError = ref(false)
- const popupVisible = ref(false)
- const popupOrgName = ref('')
- const popupHours = ref('')
- const popupWindows = ref<any[]>([])
onLoad((options: any) => {
// #ifdef MP-WEIXIN
@@ -96,17 +83,34 @@
}, () => { listLoading.value = false; listError.value = true })
}
const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
- const noop = () => {}
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
- popupOrgName.value = item.name || ''
- popupHours.value = item.officeHours || '以机构公示时间为准'
- popupWindows.value = item.windows as any[]
- popupVisible.value = true
+ const openWindowInfo = (item: any) => {
+ const source = item != null && item.windowInfoUrl != null ? String(item.windowInfoUrl) : ''
+ if (source == '') {
+ uni.showToast({ title: '窗口信息文件暂未配置', icon: 'none' })
+ return
+ }
+ const url = resolveApiUrl(source)
+ uni.showLoading({ title: '正在打开文件', mask: true })
+ uni.downloadFile({
+ url: url,
+ success: (result: any) => {
+ if (result.statusCode != 200 || result.tempFilePath == null || result.tempFilePath == '') {
+ uni.showToast({ title: '窗口信息文件加载失败', icon: 'none' })
+ return
+ }
+ uni.openDocument({
+ filePath: result.tempFilePath as string,
+ fileType: 'pdf',
+ showMenu: true,
+ fail: () => uni.showToast({ title: '暂时无法打开 PDF 文件', icon: 'none' })
+ })
+ },
+ fail: () => uni.showToast({ title: '窗口信息文件加载失败', icon: 'none' }),
+ complete: () => uni.hideLoading()
+ })
}
- const hideWindowInfo = () => { popupVisible.value = false }
</script>
<style>
@@ -148,15 +152,4 @@
.window-button text { color:#FF7A27; font-size:24rpx; line-height:32rpx; }
.window-button-active { opacity:.7; }
.bottom-space { height:calc(30rpx + env(safe-area-inset-bottom)); }
- .popup-mask { display:flex; position:fixed; z-index:30; left:0; right:0; top:0; bottom:0; background-color:rgba(0,0,0,.5); justify-content:center; align-items:center; }
- .popup-card { display:flex; width:600rpx; box-sizing:border-box; padding:40rpx; border-radius:24rpx; background-color:#FFFFFF; flex-direction:column; }
- .popup-title { color:#252525; font-size:32rpx; line-height:44rpx; font-weight:700; text-align:center; }
- .popup-org { margin-top:24rpx; color:#363636; font-size:27rpx; line-height:38rpx; font-weight:600; }
- .popup-row { display:flex; margin-top:14rpx; flex-direction:row; align-items:center; }
- .popup-dot { width:10rpx; height:10rpx; margin-right:12rpx; border-radius:50%; background-color:#FF7A27; flex-shrink:0; }
- .popup-row text { color:#5A5F66; font-size:25rpx; line-height:36rpx; }
- .popup-time { margin-top:20rpx; color:#8A8D92; font-size:23rpx; line-height:32rpx; }
- .popup-close { display:flex; height:76rpx; margin-top:30rpx; border-radius:38rpx; background-image:linear-gradient(90deg,#FF4B32,#FF8A1E); justify-content:center; align-items:center; }
- .popup-close text { color:#FFFFFF; font-size:28rpx; font-weight:600; }
- .popup-close-active { opacity:.8; }
</style>
diff --git a/rencaika/pages/coupon-received/coupon-received.uvue b/rencaika/pages/coupon-received/coupon-received.uvue
index 662c30f..7d86e91 100644
--- a/rencaika/pages/coupon-received/coupon-received.uvue
+++ b/rencaika/pages/coupon-received/coupon-received.uvue
@@ -17,14 +17,20 @@
import { getCinemaCouponClaim, getTicketPackageClaim } from '../../services/portal.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)
+ // Keep the first render below the status bar even before the MP capsule metrics arrive.
+ const navTop = ref(44); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
const claimId = ref(''); const merchantId = ref(''); const talentId = ref(''); const claimType = ref('cinemaCoupon'); const loading = ref(true); const loadError = ref(false)
const couponName = ref('电影券'); const merchantName = ref(''); const annualTotal = ref(0); const annualClaimed = ref(0); const annualRemaining = ref(0); const hasAnnual = ref(false)
const exchangeCode = ref(''); const exchangeCodeStatus = ref(''); const codeStatusText = ref('兑换码生成中,请稍后重试'); const voucherTip = ref('复制后前往影城小程序使用,核销时出示兑换码')
- onLoad((options: any) => { if (options != null && options['claimId'] != null) claimId.value = String(options['claimId']); if (options != null && options['merchantId'] != null) merchantId.value = String(options['merchantId']); if (options != null && options['type'] != null) claimType.value = String(options['type']); if (options != null && options['name'] != null) couponName.value = decodeURIComponent(String(options['name'])); const session = getSession(); if (session != null && session.talentId != null) talentId.value = String(session.talentId); if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2'; loadClaim() })
+ onLoad((options: any) => {
+ // #ifdef MP-WEIXIN
+ const capsule = uni.getMenuButtonBoundingClientRect(); const systemInfo = uni.getSystemInfoSync(); navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = systemInfo.windowWidth - capsule.right
+ // #endif
+ if (options != null && options['claimId'] != null) claimId.value = String(options['claimId']); if (options != null && options['merchantId'] != null) merchantId.value = String(options['merchantId']); if (options != null && options['type'] != null) claimType.value = String(options['type']); if (options != null && options['name'] != null) couponName.value = decodeURIComponent(String(options['name'])); const session = getSession(); if (session != null && session.talentId != null) talentId.value = String(session.talentId); if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2'; loadClaim()
+ })
const progressPercent = computed(() => annualTotal.value > 0 ? Math.min(100, Math.round(annualClaimed.value * 100 / annualTotal.value)) : 0)
const canCopy = computed(() => exchangeCodeStatus.value == 'ACTIVE' && exchangeCode.value != '')
- const loadClaim = () => { if (claimId.value == '' || talentId.value == '') { loading.value = false; loadError.value = true; return }; loading.value = true; loadError.value = false; const onSuccess = (data: any) => { loading.value = false; couponName.value = data.couponName != null ? String(data.couponName) : couponName.value; merchantName.value = data.merchantName != null ? String(data.merchantName) : ''; const totalValue = data.packageQuotaTotal != null ? data.packageQuotaTotal : (data.quotaTotal != null ? data.quotaTotal : data.annualTotal); const usedValue = data.packageQuotaUsed != null ? data.packageQuotaUsed : (data.quotaUsed != null ? data.quotaUsed : data.annualClaimed); const remainingValue = data.packageQuotaRemaining != null ? data.packageQuotaRemaining : (data.quotaRemaining != null ? data.quotaRemaining : data.annualRemaining); if (totalValue != null && usedValue != null && remainingValue != null) { annualTotal.value = Number(totalValue); annualClaimed.value = Number(usedValue); annualRemaining.value = Number(remainingValue); hasAnnual.value = annualTotal.value > 0 }; exchangeCode.value = data.exchangeCode != null ? String(data.exchangeCode) : ''; exchangeCodeStatus.value = data.exchangeCodeStatus != null ? String(data.exchangeCodeStatus) : ''; if (exchangeCodeStatus.value == 'USED') codeStatusText.value = '兑换码已使用'; else if (exchangeCodeStatus.value == 'EXPIRED') codeStatusText.value = '兑换码已失效'; else if (exchangeCode.value == '') codeStatusText.value = '兑换码生成中,请稍后重试'; else codeStatusText.value = ''; const channel = data.appName != null && String(data.appName) != '' ? String(data.appName) + 'APP' : (merchantName.value == '' ? '影城' : merchantName.value) + '小程序'; voucherTip.value = '复制后前往' + channel + '使用,核销时出示兑换码' }; const onError = () => { loading.value = false; loadError.value = true }; if (claimType.value == 'cinemaCoupon') getCinemaCouponClaim(claimId.value, talentId.value, onSuccess, onError); else getTicketPackageClaim(claimId.value, talentId.value, onSuccess, onError) }
+ const loadClaim = () => { if (claimId.value == '' || talentId.value == '') { loading.value = false; loadError.value = true; return }; loading.value = true; loadError.value = false; const onSuccess = (data: any) => { loading.value = false; couponName.value = data.couponName != null ? String(data.couponName) : couponName.value; merchantName.value = data.merchantName != null ? String(data.merchantName) : ''; const totalValue = data.packageQuotaTotal != null ? data.packageQuotaTotal : (data.quotaTotal != null ? data.quotaTotal : data.annualTotal); const usedValue = data.packageQuotaUsed != null ? data.packageQuotaUsed : (data.quotaUsed != null ? data.quotaUsed : data.annualClaimed); const remainingValue = data.packageQuotaRemaining != null ? data.packageQuotaRemaining : (data.quotaRemaining != null ? data.quotaRemaining : data.annualRemaining); if (totalValue != null && usedValue != null && remainingValue != null) { annualTotal.value = Number(totalValue); annualClaimed.value = Number(usedValue); annualRemaining.value = Number(remainingValue); hasAnnual.value = annualTotal.value > 0 }; exchangeCode.value = data.exchangeCode != null ? String(data.exchangeCode) : ''; exchangeCodeStatus.value = data.exchangeCodeStatus != null ? String(data.exchangeCodeStatus) : ''; if (exchangeCodeStatus.value == 'USED') codeStatusText.value = '兑换码已使用'; else if (exchangeCodeStatus.value == 'EXPIRED') codeStatusText.value = '兑换码已失效'; else if (exchangeCodeStatus.value == 'LEGACY' || exchangeCodeStatus.value == 'UNAVAILABLE') codeStatusText.value = '历史电影券暂无兑换码'; else if (exchangeCode.value == '') codeStatusText.value = '兑换码生成中,请稍后重试'; else codeStatusText.value = ''; const channel = data.appName != null && String(data.appName) != '' ? String(data.appName) + 'APP' : (merchantName.value == '' ? '影城' : merchantName.value) + '小程序'; voucherTip.value = exchangeCode.value == '' ? '该电影券为历史领取记录,当前无需操作' : '复制后前往' + channel + '使用,核销时出示兑换码' }; const onError = () => { loading.value = false; loadError.value = true }; if (claimType.value == 'cinemaCoupon') getCinemaCouponClaim(claimId.value, talentId.value, onSuccess, onError); else getTicketPackageClaim(claimId.value, talentId.value, onSuccess, onError) }
const goBack = () => safeBack('/pages/cinema-merchant/cinema-merchant?merchantId=' + encodeURIComponent(merchantId.value))
const copyExchangeCode = () => { if (!canCopy.value) return; uni.setClipboardData({ data: exchangeCode.value, success: () => uni.showToast({ title: '兑换码已复制', icon: 'success' }), fail: () => uni.showToast({ title: '复制失败,请重试', icon: 'none' }) }) }
</script>
diff --git a/rencaika/pages/merchant/merchant.uvue b/rencaika/pages/merchant/merchant.uvue
index 24e999e..a8897c4 100644
--- a/rencaika/pages/merchant/merchant.uvue
+++ b/rencaika/pages/merchant/merchant.uvue
@@ -587,7 +587,11 @@
usageInstructions.value = data != null && data.usageInstructions != null && data.usageInstructions != '' ? data.usageInstructions as string : usageInstructions.value
if (data != null && data.cardLevel != null && data.cardLevel != '') currentCardLevel.value = data.cardLevel as string
applyMedicalRightsConfig(data)
- groupBuyingEnabled.value = data == null || data.groupBuyingEnabled == null ? groupBuyingEnabled.value : data.groupBuyingEnabled === true || Number(data.groupBuyingEnabled) == 1
+ // 普通自助洗车复用餐饮商家的团单展示;外链洗车不会进入此通用详情页。
+ const isNormalCarWash = merchantType.value == 'carWash'
+ groupBuyingEnabled.value = isNormalCarWash
+ ? true
+ : (data == null || data.groupBuyingEnabled == null ? groupBuyingEnabled.value : data.groupBuyingEnabled === true || Number(data.groupBuyingEnabled) == 1)
isHotelMerchant.value = merchantType.value == 'hotel'
hasFreeVoucher.value = isHotelMerchant.value && data != null && data.rightsStatus != null && String(data.rightsStatus) == 'AVAILABLE' && (data.freeVoucherEnabled === true || Number(data.freeVoucherEnabled) == 1)
if (hasFreeVoucher.value) {
diff --git a/rencaika/pages/shopping-benefits/shopping-benefits.uvue b/rencaika/pages/shopping-benefits/shopping-benefits.uvue
index d8d887f..fcc0e3d 100644
--- a/rencaika/pages/shopping-benefits/shopping-benefits.uvue
+++ b/rencaika/pages/shopping-benefits/shopping-benefits.uvue
@@ -70,14 +70,21 @@
getShoppingTopicMerchants(activeTab.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, 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 }) }
+ 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 : '', 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 }) }
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 })
}
const loadMore = () => { if (initialLoading.value || loadingMore.value || !hasMore.value) return; loadMoreError.value = false; loadingMore.value = true; loadServices(true) }
const openDisplayPolicies = () => uni.navigateTo({ url: '/pages/display-policy/display-policy' })
const goBack = () => safeBack('/pages/benefits/benefits')
- const openMerchant = (item: any) => { if (!item.available) { uni.showToast({ title: '该服务已停用', icon: 'none' }); return }; uni.navigateTo({ url: '/pages/shopping-merchant/shopping-merchant?merchantId=' + encodeURIComponent(item.id) }) }
+ const openMerchant = (item: any) => {
+ if (!item.available) { uni.showToast({ title: '该服务已停用', icon: 'none' }); return }
+ if (item.merchantType == 'dental') {
+ uni.navigateTo({ url: '/pages/merchant/merchant?merchantId=' + encodeURIComponent(item.id) + '&merchantType=dental&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) })
+ return
+ }
+ uni.navigateTo({ url: '/pages/shopping-merchant/shopping-merchant?merchantId=' + encodeURIComponent(item.id) })
+ }
</script>
<style>
diff --git a/rencaika/pages/stay-benefits/stay-benefits.uvue b/rencaika/pages/stay-benefits/stay-benefits.uvue
index ed7b27a..b6b9c38 100644
--- a/rencaika/pages/stay-benefits/stay-benefits.uvue
+++ b/rencaika/pages/stay-benefits/stay-benefits.uvue
@@ -164,12 +164,16 @@
const merchantType = readText(item.merchantType, item['merchant_type'])
const merchantName = readText(item.merchantName, item['merchant_name'])
const discountText = readText(item.discountText, item['discount_text'])
+ const configuredTags: any[] = item.benefitTags instanceof Array ? (item.benefitTags as any[]) : []
+ const displayTags: any[] = activeTab.value == 'hotel'
+ ? configuredTags.filter((tag: any) => String(tag).trim() != '惠')
+ : configuredTags
next.push({
id: '' + item.id,
merchantType: merchantType != '' ? merchantType : activeTab.value,
name: merchantName != '' ? merchantName : '\u4f4f\u5bbf\u670d\u52a1',
discount: discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab',
- discountTags: item.benefitTags instanceof Array && (item.benefitTags as any[]).length > 0 ? item.benefitTags as any[] : [discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab'],
+ discountTags: displayTags.length > 0 ? displayTags : [discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab'],
address: readText(item.address, item['merchant_address']),
image: coverUrl != '' ? resolveApiUrl(coverUrl) : fallbackImage,
available: readAvailable(item)
diff --git a/rencaika/pages/ticket-package/ticket-package.uvue b/rencaika/pages/ticket-package/ticket-package.uvue
index 9b9a7ac..e5c8814 100644
--- a/rencaika/pages/ticket-package/ticket-package.uvue
+++ b/rencaika/pages/ticket-package/ticket-package.uvue
@@ -53,7 +53,8 @@
import { getSession } from '../../services/session.uts'
import { safeBack } from '../../utils/navigation.uts'
- const navTop = ref(28)
+ // Keep the first render below the status bar even before the MP capsule metrics arrive.
+ const navTop = ref(44)
const navHeight = ref(32)
const navWidth = ref(88)
const navRight = ref(10)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantTypeCatalog.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantTypeCatalog.java
index be8be00..f0d425d 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantTypeCatalog.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantTypeCatalog.java
@@ -52,9 +52,9 @@
"数码产品服务"),
type("culture", "文化服务", "惠享好物", "SHOPPING", null, null, "book", true,
"图书服务", "文化图书服务"),
- type("telecom", "电信服务", "惠享好物", "SHOPPING", null, null, "digital", true,
+ type("telecom", "电信服务", "惠享好物", "SHOPPING", null, null, "telecom", true,
"通信服务"),
- type("dental", "口腔治疗保健", "臻享礼遇", "MERCHANT", null, null, null, true,
+ type("dental", "口腔治疗保健", "惠享好物", "MERCHANT", null, null, "dental", true,
"口腔服务", "口腔保健"),
type("shopping", "购物服务", "惠享好物", "SHOPPING", null, null, null, true,
"惠享好物")
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java
index 0a42c87..ac9091a 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java
@@ -10,9 +10,14 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import tools.jackson.core.type.TypeReference;
+import tools.jackson.databind.ObjectMapper;
@Service
public class AdminApprovalPortalService {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final TypeReference<List<Map<String, Object>>> APPROVAL_ITEMS = new TypeReference<>() {};
private final JdbcTemplate jdbc;
@@ -22,23 +27,68 @@
@Transactional(readOnly = true)
public Map<String, Object> detail(Long serviceId) {
- approvalService(serviceId);
+ Map<String, Object> merchant = approvalService(serviceId);
Map<String, Object> benefit = benefit();
Map<String, Object> result = new LinkedHashMap<>();
result.put("benefitId", benefit.get("id"));
result.put("serviceId", serviceId);
result.put("title", benefit.get("title"));
result.put("freeLabel", benefit.get("freeLabel"));
- result.put("heroImage", benefit.get("heroImage"));
- result.put("available", number(benefit.get("enabled")) == 1);
+ result.put("heroImage", text(merchant.get("coverUrl"), text(benefit.get("heroImage"))));
+ result.put("available", number(benefit.get("enabled")) == 1 && number(merchant.get("status")) == 1);
result.put("unavailableReason", text(benefit.get("unavailableReason"), "该权益暂未开放"));
- result.put("organizations", organizations(((Number) benefit.get("id")).longValue()));
+ List<Map<String, Object>> configuredOrganizations = configuredOrganizations(merchant);
+ result.put("organizations", configuredOrganizations.isEmpty()
+ ? organizations(((Number) benefit.get("id")).longValue())
+ : configuredOrganizations);
return result;
+ }
+
+ private List<Map<String, Object>> configuredOrganizations(Map<String, Object> merchant) {
+ List<Map<String, Object>> items = parseApprovalItems(merchant.get("usageInstructions"));
+ if (items.isEmpty()) {
+ String rightsText = text(merchant.get("rightsText"));
+ if (rightsText.isBlank()) return List.of();
+ items = List.of(Map.of(
+ "name", text(merchant.get("merchantName"), "行政审批服务"),
+ "content", rightsText,
+ "address", text(merchant.get("address")),
+ "phone", text(merchant.get("contactMobile"))));
+ }
+ List<Map<String, Object>> result = new java.util.ArrayList<>();
+ for (int index = 0; index < items.size(); index++) {
+ Map<String, Object> item = items.get(index);
+ Map<String, Object> organization = new LinkedHashMap<>();
+ organization.put("id", merchant.get("id") + "-" + index);
+ organization.put("name", text(item.get("name"), text(merchant.get("merchantName"), "行政审批服务")));
+ organization.put("address", text(item.get("address"), text(merchant.get("address"))));
+ organization.put("latitude", merchant.get("latitude"));
+ organization.put("longitude", merchant.get("longitude"));
+ organization.put("description", text(item.get("content"), text(merchant.get("rightsText"), "暂无权益说明")));
+ organization.put("phone", text(item.get("phone"), text(merchant.get("contactMobile"))));
+ organization.put("officeHours", "");
+ String windowInfoUrl = text(item.get("windowInfoUrl"), text(item.get("pdfFile"), text(item.get("pdf_file"))));
+ organization.put("windowInfoUrl", windowInfoUrl);
+ organization.put("windows", List.of());
+ organization.put("hasWindowInfo", !windowInfoUrl.isBlank());
+ result.add(organization);
+ }
+ return result;
+ }
+
+ private List<Map<String, Object>> parseApprovalItems(Object value) {
+ if (value == null || String.valueOf(value).isBlank()) return List.of();
+ try {
+ return OBJECT_MAPPER.readValue(String.valueOf(value), APPROVAL_ITEMS);
+ } catch (Exception ignored) {
+ return List.of();
+ }
}
private List<Map<String, Object>> organizations(Long benefitId) {
return query("""
- select id, organization_name, address, latitude, longitude, rights_text, consultation_phone, office_hours
+ select id, organization_name, address, latitude, longitude, rights_text, consultation_phone, office_hours,
+ pdf_file, is_pdf_visible
from admin_approval_organization
where benefit_id = ? and enabled = 1 and deleted = 0
order by sort_no asc, id asc
@@ -61,8 +111,10 @@
organization.put("description", row.get("rightsText"));
organization.put("phone", text(row.get("consultationPhone")));
organization.put("officeHours", text(row.get("officeHours")));
+ String windowInfoUrl = number(row.get("isPdfVisible")) == 1 ? text(row.get("pdfFile")) : "";
+ organization.put("windowInfoUrl", windowInfoUrl);
organization.put("windows", windows);
- organization.put("hasWindowInfo", !windows.isEmpty());
+ organization.put("hasWindowInfo", !windowInfoUrl.isBlank());
return organization;
}).toList();
}
@@ -70,7 +122,8 @@
private Map<String, Object> approvalService(Long serviceId) {
if (serviceId == null || serviceId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "行政审批服务信息不能为空");
List<Map<String, Object>> rows = query("""
- select m.id
+ select m.id, m.merchant_name, m.address, m.contact_mobile, m.cover_url, m.latitude, m.longitude,
+ m.status, m.usage_instructions, m.rights_text
from merchant m
where m.id = ? and m.deleted = 0 and m.merchant_type_code = 'approval'
""", serviceId);
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/CinemaPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/CinemaPortalService.java
index 60878a0..1bbb206 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/CinemaPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/CinemaPortalService.java
@@ -136,7 +136,7 @@
join merchant m on m.id = cc.merchant_id and m.deleted = 0
where cc.id = ? and cc.talent_id = ? and cc.deleted = 0
""", claimId, talentId);
- if (claims.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "用户券不存在或无权查看");
+ if (claims.isEmpty()) return legacyClaimDetail(claimId, talentId);
Map<String, Object> claim = claims.get(0);
int annualTotal = number(claim.get("perUserLimit"));
if (annualTotal < 1) annualTotal = number(claim.get("annualQuota"));
@@ -168,6 +168,33 @@
return result;
}
+ private Map<String, Object> legacyClaimDetail(Long recordId, Long talentId) {
+ List<Map<String, Object>> records = query("""
+ select r.id, r.receive_no, r.package_name, r.merchants
+ from movie_ticket_record r
+ where r.id = ? and r.deleted = 0
+ and exists (
+ select 1 from talent_card c
+ where c.talent_id = ? and c.deleted = 0 and c.card_type = 'PERSONAL' and c.card_no = r.card_no
+ )
+ """, recordId, talentId);
+ if (records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "用户券不存在或无权查看");
+ Map<String, Object> record = records.get(0);
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("claimId", record.get("id"));
+ result.put("couponName", text(record.get("packageName"), "电影券"));
+ result.put("merchantName", text(record.get("merchants"), "影城"));
+ result.put("exchangeCodeStatus", "LEGACY");
+ result.put("exchangeCode", "");
+ result.put("annualTotal", 0);
+ result.put("annualClaimed", 0);
+ result.put("annualRemaining", 0);
+ result.put("channel", "影城小程序");
+ result.put("verificationRule", "历史电影券领取记录");
+ result.put("invalidReason", "历史记录");
+ return result;
+ }
+
private Map<String, Object> ticketPackageCouponView(Map<String, Object> row) {
int quotaTotal = number(value(row, "packageQuotaTotal", "quotaTotal"));
int quotaUsed = number(value(row, "packageQuotaUsed", "quotaUsed"));
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/UsageRecordPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/UsageRecordPortalService.java
index 0bb9b48..12f1701 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/UsageRecordPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/UsageRecordPortalService.java
@@ -124,7 +124,7 @@
select concat('movie-', r.id) as record_id,
'MOVIE_TICKET' as record_type,
r.receive_no as business_id,
- cc.id as target_detail_id,
+ coalesce(cc.id, r.id) as target_detail_id,
null as merchant_id,
'万达影城' as merchant_name,
'cinema' as merchant_type,
diff --git a/talent-card-server/src/main/resources/db/migration/V120__add_telecom_and_dental_shopping_categories.sql b/talent-card-server/src/main/resources/db/migration/V120__add_telecom_and_dental_shopping_categories.sql
new file mode 100644
index 0000000..45eb6be
--- /dev/null
+++ b/talent-card-server/src/main/resources/db/migration/V120__add_telecom_and_dental_shopping_categories.sql
@@ -0,0 +1,11 @@
+insert into shopping_topic_category(category_key, category_name, sort_no, enabled)
+select 'telecom', '电信服务', 60, 1
+where not exists (
+ select 1 from shopping_topic_category where category_key = 'telecom'
+);
+
+insert into shopping_topic_category(category_key, category_name, sort_no, enabled)
+select 'dental', '口腔治疗保健', 70, 1
+where not exists (
+ select 1 from shopping_topic_category where category_key = 'dental'
+);
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantTypeCatalogTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantTypeCatalogTests.java
index 957093d..16c575f 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantTypeCatalogTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantTypeCatalogTests.java
@@ -26,6 +26,10 @@
assertThat(medical.premiumBusinessType()).isEqualTo("MEDICAL");
assertThat(MerchantTypeCatalog.codesForShoppingCategory("book")).containsExactly("culture");
+ assertThat(MerchantTypeCatalog.codesForShoppingCategory("telecom")).containsExactly("telecom");
+ assertThat(MerchantTypeCatalog.codesForShoppingCategory("dental")).containsExactly("dental");
+ assertThat(MerchantTypeCatalog.find("口腔治疗保健").orElseThrow().topicName()).isEqualTo("惠享好物");
+ assertThat(MerchantTypeCatalog.find("口腔治疗保健").orElseThrow().detailType()).isEqualTo("MERCHANT");
assertThat(MerchantTypeCatalog.find("酒店服务").orElseThrow().supportsMerchantAppLogin()).isTrue();
}
--
Gitblit v1.7.1