fix: complete miniapp benefit and coupon flows
| | |
| | | - 路由:`/pages/admin-approval/admin-approval` |
| | | - 源码:`pages/admin-approval/admin-approval.uvue` |
| | | - 入口:臻享礼遇页“行政审批”分类点击服务项进入。 |
| | | - 当前状态:视觉与本地交互首版已完成,真实机构列表、咨询电话与窗口信息接口待接入。 |
| | | - 当前状态:已接入行政审批详情接口;窗口信息直接打开后台上传的 PDF 文件。 |
| | | |
| | | ## 功能点 |
| | | |
| | |
| | | | 权益标识 | 标题“行政审批”与“免费”标签固定展示。 | 后台返回权益名称与免费/付费标识。 | |
| | | | 机构列表 | 展示机构名称、地址、权益内容说明和咨询电话,内容超出时区内滚动;已补加载、空、失败和重试。 | 后台返回机构 ID、名称、地址、经纬度、纯文本权益说明、联系电话、排序和启用状态。 | |
| | | | 咨询电话 | 点击“咨询电话-号码”直接调起系统拨号。 | 号码由后台返回,为空时隐藏电话入口。 | |
| | | | 窗口信息 | 有窗口信息的机构展示“查看窗口信息”按钮,点击弹出窗口与办公时间说明。 | 后台返回窗口数组(窗口名称、楼层位置)、办公时间和是否展示入口。 | |
| | | | 窗口信息 | 有 PDF 文件的机构展示“查看窗口信息”按钮,点击下载并打开 PDF 阅读器。 | 后台返回 `windowInfoUrl`(后台上传 PDF 的 URL);没有文件时隐藏入口。 | |
| | | |
| | | ## 操作说明 |
| | | |
| | | 1. 用户从臻享礼遇页选择“行政审批”分类,点击任意服务项进入本页。 |
| | | 2. 浏览各政务服务机构的地址和权益内容,机构较多时上滑浏览。 |
| | | 3. 点击“咨询电话-号码”调起系统拨号。 |
| | | 4. 点击“查看窗口信息”查看办理窗口位置和办公时间,点击遮罩或“我知道了”关闭。 |
| | | 4. 点击“查看窗口信息”下载并打开后台上传的窗口信息 PDF。 |
| | | 5. 点击左上角返回按钮回到臻享礼遇页。 |
| | | |
| | | ## 异常处理 |
| | |
| | | |
| | | - 权益信息:权益 ID、名称、免费/付费标识、头图 URL、启用状态。 |
| | | - 机构列表:机构 ID、名称、地址、经纬度、纯文本权益说明、联系电话、排序、启用状态。 |
| | | - 窗口信息:机构 ID、窗口数组(窗口名称、所在楼层/位置)、办公时间、是否展示窗口入口。 |
| | | - 窗口信息:机构 ID、`windowInfoUrl`(PDF 地址)、是否展示窗口入口。 |
| | |
| | | <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> |
| | | |
| | |
| | | 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 |
| | |
| | | }, () => { 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> |
| | |
| | | .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> |
| | |
| | | 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> |
| | |
| | | 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) { |
| | |
| | | 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> |
| | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | "数码产品服务"), |
| | | 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, |
| | | "惠享好物") |
| | |
| | | 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; |
| | | |
| | |
| | | |
| | | @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 |
| | |
| | | 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(); |
| | | } |
| | |
| | | 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); |
| | |
| | | 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")); |
| | |
| | | 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")); |
| | |
| | | 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, |
| New file |
| | |
| | | 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' |
| | | ); |
| | |
| | | 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(); |
| | | } |
| | | |