shinnosuke
2026-08-13 f7b915a08e3407b54a589ad0999fdd4c69118557
fix(config): 更新开发环境基础URL并优化界面显示

- 将开发环境基础URL从 http://127.0.0.1:8080 更新为 http://221.182.45.100:11451
- 优化银行卡选择页面导航栏样式,调整标题居中和返回按钮位置
- 修复搜索页面景点图片路径错误,统一使用默认占位图
- 修正人才卡页面文字描述,将"一卡三类"改为"一二三类"
- 调整对话框确认按钮尺寸,宽度从240rpx改为200rpx
- 优化申请条件页面条件行处理逻辑,新增编号条件样式
- 更新首页界面元素,移除视频播放功能和相关图片资源
- 修正首页新闻资讯数据结构,增加封面图片URL字段
- 删除商户端提现功能入口,简化设置卡片布局
11个文件已修改
220 ■■■■■ 已修改文件
rencaika/pages/card/card.uvue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/benefits/benefits.uvue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/order-detail/order-detail.uvue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/withdrawal-detail/withdrawal-detail.uvue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/withdrawal/withdrawal.uvue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantManagementService.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantWorkServiceTests.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/card/card.uvue
@@ -117,8 +117,8 @@
    let codeRefreshTimer: number | null = null
    const guestCardTypes = ref([
        { title: '卓越人才卡', image: '/static/card-assets/card-excellence.jpg' },
        { title: '杰出人才卡', image: '/static/card-assets/card-outstanding.jpg' },
        { title: '菁英人才卡', image: '/static/card-assets/card-elite.jpg' }
        { title: '杰出人才卡', image: '/static/card-assets/card-elite.jpg' },
        { title: '菁英人才卡', image: '/static/card-assets/card-outstanding.jpg' }
    ])
    onLoad((options: any) => {
@@ -140,8 +140,8 @@
    onUnload(() => clearCodeTimer())
    const setCardBackground = (theme: string) => {
        if (theme == 'OUTSTANDING') { cardBackground.value = '/static/card-assets/card-outstanding.jpg'; return }
        if (theme == 'ELITE') { cardBackground.value = '/static/card-assets/card-elite.jpg'; return }
        if (theme == 'OUTSTANDING') { cardBackground.value = '/static/card-assets/card-elite.jpg'; return }
        if (theme == 'ELITE') { cardBackground.value = '/static/card-assets/card-outstanding.jpg'; return }
        cardBackground.value = '/static/card-assets/talent-card-background.jpg'
    }
    const loadTalentCard = () => {
rencaika_merchant/pages/benefits/benefits.uvue
@@ -85,19 +85,19 @@
    .hero-title { color: #FFFFFF; font-size: 42rpx; line-height: 59rpx; font-weight: 700; }
    .hero-subtitle { color: rgba(255, 255, 255, 0.90); font-size: 25rpx; line-height: 35rpx; margin-top: 4rpx; }
    .content { min-height: 1060rpx; margin-top: -52rpx; padding: 38rpx 34rpx 0; border-radius: 44rpx 44rpx 0 0; background-color: #F4F6F9; position: relative; box-sizing: border-box; }
    .benefit-card { min-height: 206rpx; padding: 28rpx 34rpx; margin-bottom: 26rpx; overflow: hidden; border-radius: 34rpx; box-sizing: border-box; position: relative; }
    .content { min-height: 1060rpx; margin-top: -52rpx; padding: 32rpx 34rpx 0; border-radius: 44rpx 44rpx 0 0; background-color: #F4F6F9; position: relative; box-sizing: border-box; }
    .benefit-card { min-height: 182rpx; padding: 24rpx 30rpx; margin-bottom: 22rpx; overflow: hidden; border-radius: 28rpx; box-sizing: border-box; position: relative; }
    .excellent-card { background-image: linear-gradient(135deg, #D5A52B 0%, #F8ECAF 54%, #E9BF4D 100%); }
    .outstanding-card { min-height: 346rpx; background-image: linear-gradient(135deg, #B4BCC7 0%, #E7EBF0 56%, #BAC2CD 100%); }
    .outstanding-card { min-height: 224rpx; background-image: linear-gradient(135deg, #AAB4C1 0%, #DDE2E8 56%, #B5BFCC 100%); }
    .elite-card { background-image: linear-gradient(135deg, #78BCE7 0%, #D9F1FF 55%, #8FCBEC 100%); }
    .card-circle { width: 176rpx; height: 176rpx; border-width: 1rpx; border-style: solid; border-color: rgba(255, 255, 255, 0.30); border-radius: 88rpx; position: absolute; }
    .circle-one { top: -92rpx; right: -8rpx; }
    .circle-two { right: 52rpx; bottom: -116rpx; }
    .card-heading { display: flex; flex-direction: row; align-items: center; justify-content: space-between; position: relative; }
    .card-title { color: #FFFFFF; font-size: 40rpx; line-height: 56rpx; font-weight: 700; }
    .level-tag { padding: 11rpx 20rpx; border-radius: 27rpx; color: #FFFFFF; font-size: 22rpx; line-height: 31rpx; font-weight: 600; background-color: rgba(109, 91, 43, 0.28); }
    .card-title { color: #FFFFFF; font-size: 36rpx; line-height: 50rpx; font-weight: 700; }
    .level-tag { padding: 9rpx 18rpx; border-radius: 24rpx; color: #FFFFFF; font-size: 20rpx; line-height: 28rpx; font-weight: 600; background-color: rgba(109, 91, 43, 0.28); }
    .outstanding-card .level-tag { background-color: rgba(74, 82, 96, 0.28); }
    .elite-card .level-tag { background-color: rgba(42, 125, 177, 0.32); }
    .benefit-copy { padding: 20rpx 22rpx; margin-top: 24rpx; border-radius: 24rpx; color: #FFFFFF; font-size: 27rpx; line-height: 39rpx; font-weight: 600; background-color: rgba(255, 255, 255, 0.27); box-sizing: border-box; position: relative; }
    .benefit-copy { padding: 16rpx 20rpx; margin-top: 18rpx; border-radius: 20rpx; color: #FFFFFF; font-size: 24rpx; line-height: 35rpx; font-weight: 600; background-color: rgba(255, 255, 255, 0.27); box-sizing: border-box; position: relative; }
    .bottom-space { height: 86rpx; }
</style>
rencaika_merchant/pages/order-detail/order-detail.uvue
@@ -63,6 +63,8 @@
    const formatCents = (value: number): string => '¥' + (value / 100).toFixed(2)
    const applyRemoteOrder = (detail: MerchantOrderDetail) => {
        // 订单列表仅以订单号跳转,撤销接口需要后端返回的真实订单 ID。
        orderId.value = detail.id
        orderNo.value = detail.orderNo
        productAmount.value = formatCents(detail.orderAmount)
        talentDiscount.value = '-¥' + (detail.talentDiscountAmount / 100).toFixed(2)
@@ -75,7 +77,8 @@
        customerPhone.value = detail.customerMobile
        cardLevel.value = detail.cardLevel.length > 0 ? detail.cardLevel : '人才卡'
        benefitName.value = detail.benefitName.length > 0 ? detail.benefitName : '以当前订单权益规则为准'
        isCompleted.value = detail.status == 'completed'
        // 核销时间是已完成餐饮订单的必要数据,避免历史脏状态显示“已完成”却仍是“待核销”。
        isCompleted.value = detail.status == 'completed' && detail.verifyTime.length > 0
        isStoreCompleted.value = detail.category == 'store' && detail.status == 'completed'
        canRevokeVerification.value = detail.canRevoke
        revokeExpiredMessage.value = detail.revokeReason.length > 0 ? detail.revokeReason : '已过可撤销时间'
@@ -113,7 +116,10 @@
    }
    const submitRevokeVerification = () => {
        if (revokeSubmitting.value) return
        if (revokeSubmitting.value || orderId.value.length == 0) {
            if (orderId.value.length == 0) uni.showToast({ title: '订单信息加载失败,请刷新后重试', icon: 'none' })
            return
        }
        revokeSubmitting.value = true
        const idempotencyKey = 'revoke-verification-' + orderId.value + '-' + Date.now()
        revokeOrderVerification(orderId.value, idempotencyKey, (result: any, traceId: string) => {
rencaika_merchant/pages/withdrawal-detail/withdrawal-detail.uvue
@@ -2,8 +2,8 @@
    <view class="page">
        <view class="navigation-bar"><view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="page-title">商户提现</text></view>
        <scroll-view class="form-scroll" scroll-y="true" :show-scrollbar="false" :enhanced="false">
            <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view>
            <view class="form-card">
                <view class="available-row"><text class="available-label">当前可提</text><text class="available-value">¥{{availableText}}</text></view>
                <view class="field-block">
                    <view class="field-row">
                        <text class="field-label">提现金额</text>
@@ -78,4 +78,5 @@
<style>
    page { background-color: #F4F6F9; }.page { display: flex; flex: 1; width: 100%; height: 100vh; flex-direction: column; overflow: hidden; background-color: #F4F6F9; }.navigation-bar { display: flex; height: 174rpx; padding: 68rpx 38rpx 0; flex: none; flex-direction: row; align-items: center; position: relative; box-sizing: border-box; }.back-button { display: flex; width: 64rpx; height: 64rpx; justify-content: center; align-items: center; position: absolute; left: 38rpx; top: 89rpx; z-index: 2; }.back-icon { width: 38rpx; height: 38rpx; transform: rotate(180deg); }.page-title { width: 100%; color: #202B3E; font-size: 34rpx; line-height: 48rpx; text-align: center; font-weight: 700; }.form-scroll { flex: 1; height: 0; padding: 0 34rpx; box-sizing: border-box; }.available-row { display: flex; height: 72rpx; padding: 0 12rpx; align-items: center; }.available-label { color: #7D8797; font-size: 25rpx; }.available-value { margin-left: 12rpx; color: #2C3748; font-size: 27rpx; font-weight: 700; }.form-card { padding: 6rpx 28rpx; border-radius: 16rpx; background-color: #FFFFFF; box-shadow: 0 8rpx 24rpx rgba(49,55,66,.05); }.field-block { display: flex; flex-direction: column; }.bank-field-block { margin-top: 0; }.field-row { display: flex; min-height: 104rpx; align-items: center; }.bank-field-row { align-items: center; }.field-label { width: 178rpx; padding-top: 0; flex: none; color: #5F6978; font-size: 27rpx; font-weight: 400; }.amount-input-shell { display: flex; height: 70rpx; padding: 0 18rpx; flex: 1; align-items: center; border: 1rpx solid #E3E7ED; border-radius: 8rpx; background-color: #FFFFFF; box-sizing: border-box; }.amount-prefix { color: #8B94A2; font-size: 27rpx; font-weight: 400; }.amount-input { height: 68rpx; margin-left: 8rpx; flex: 1; color: #2C3748; font-size: 27rpx; }.input-placeholder { color: #B5BDC9; font-size: 25rpx; }.field-invalid { border-color: #F0746D; background-color: #FFF9F8; }.field-error { margin: 0 0 14rpx 178rpx; color: #E95B55; font-size: 22rpx; }.field-divider { height: 1rpx; margin-left: 178rpx; background-color: #EEF0F3; }.bank-value { display: flex; min-width: 0; flex: 1; }.bank-placeholder { color: #B5BDC9; font-size: 25rpx; }.selected-bank-copy { display: flex; min-width: 0; flex: 1; flex-direction: column; }.selected-bank-name { color: #2C3748; font-size: 25rpx; }.selected-card-number { margin-top: 4rpx; color: #8A93A1; font-size: 22rpx; }.select-bank-button { display: flex; width: 84rpx; height: 52rpx; padding: 0; margin: 0 0 0 14rpx; flex: none; align-items: center; justify-content: center; border: 1rpx solid #58A5F8; border-radius: 8rpx; color: #318CF0; background-color: #FFFFFF; font-size: 23rpx; font-weight: 400; }.submit-button { display: flex; width: 360rpx; height: 72rpx; margin: 34rpx auto 0; align-items: center; justify-content: center; border-radius: 8rpx; color: #FFFFFF; background-color: #409EFF; font-size: 27rpx; font-weight: 500; box-shadow: none; }.submit-disabled { opacity: .58; }.safe-space { height: 70rpx; }
    .back-icon { transform: none; }
    .submit-button { width: 100%; margin-left: 0; margin-right: 0; }
</style>
rencaika_merchant/pages/withdrawal/withdrawal.uvue
@@ -1,15 +1,25 @@
<template>
  <view class="page"><view class="nav"><view class="nav-back" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text>商户提现</text><view></view></view><view class="balance"><text>当前可提</text><text class="amount">¥{{amount}}</text><view class="button" @tap="startWithdrawal">提现</view></view><view class="heading"><text>提现记录</text><text>共 {{records.length}} 条</text></view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载</view><view v-else-if="records.length == 0" class="state">暂无提现记录</view><view v-for="item in records" :key="item.id" class="record"><view><text>提现</text><text class="money">¥{{format(item.amountCents)}}</text></view><view class="record-bottom"><text>{{displayTime(item)}} {{item.cardMasked}}</text><text :class="'status ' + item.status">{{statusText(item.status)}}</text></view><text v-if="item.reason.length > 0" class="reason">{{item.reason}}</text></view></scroll-view></view>
  <view class="page"><view class="nav" :style="'height:' + navBarHeight + 'px'"><view class="nav-back" :style="'top:' + navTop + 'px;height:' + navHeight + 'px'" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="nav-title" :style="'top:' + navTop + 'px;height:' + navHeight + 'px'">商户提现</text></view><view class="balance"><text>当前可提</text><text class="amount">¥{{amount}}</text><view class="button" @tap="startWithdrawal">提现</view></view><view class="heading"><text>提现记录</text><text>共 {{records.length}} 条</text></view><scroll-view class="scroll" scroll-y="true"><view v-if="loading" class="state">正在加载</view><view v-else-if="records.length == 0" class="state">暂无提现记录</view><view v-for="item in records" :key="item.id" class="record"><view><text>提现</text><text class="money">¥{{format(item.amountCents)}}</text></view><view class="record-bottom"><text>{{displayTime(item)}} {{item.cardMasked}}</text><text :class="'status ' + item.status">{{statusText(item.status)}}</text></view><text v-if="item.reason.length > 0" class="reason">{{item.reason}}</text></view></scroll-view></view>
</template>
<script setup lang="uts">
import { loadOnboardingStatus, loadWithdrawalSummary, loadWithdrawals, SettlementWithdrawal } from '../../services/settlement.uts'
const loading = ref(true); const available = ref(0); const onboardingStatus = ref('unsubmitted'); const records = ref<Array<SettlementWithdrawal>>([])
const navTop = ref(28); const navHeight = ref(32); const navBarHeight = ref(76)
const amount = computed(() => (available.value / 100).toFixed(2)); const format = (cents: number): string => (cents / 100).toFixed(2); const formatTime = (value: string): string => value.replace('T', ' ')
const statusText = (value: string): string => value == 'paid' ? '已到账' : value == 'processing' ? '到账处理中' : value == 'rejected' ? '已驳回' : value == 'failed' ? '提现失败' : '待审核'
const displayTime = (item: SettlementWithdrawal): string => item.status == 'paid' && item.paidAt.length > 0 ? '到账 ' + formatTime(item.paidAt) : formatTime(item.createdAt)
const goBack = () => uni.navigateBack()
const startWithdrawal = () => { if (onboardingStatus.value == 'approved') { uni.navigateTo({ url: '/pages/withdrawal-detail/withdrawal-detail' }); return }; if (onboardingStatus.value == 'pending') { uni.navigateTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' }); return }; uni.navigateTo({ url: '/pages/merchant-onboarding/merchant-onboarding' }) }
const load = () => { loading.value = true; loadOnboardingStatus((item) => { onboardingStatus.value = item.status }, () => {}); loadWithdrawalSummary((data: any) => { available.value = Number(data.availableAmountCents || 0) }, () => {}); loadWithdrawals(1, 50, (data) => { records.value = data.records; loading.value = false }, (error: any) => { loading.value = false; uni.showToast({ title: error.message || '提现记录加载失败', icon: 'none' }) }) }
onLoad(() => {
    // Align the custom back control with the native mini-program capsule.
    // #ifdef MP-WEIXIN
    const capsule = uni.getMenuButtonBoundingClientRect()
    navTop.value = capsule.top
    navHeight.value = capsule.height
    navBarHeight.value = capsule.top + capsule.height + 16
    // #endif
})
onShow(() => load())
</script>
<style>.page{height:100vh;background:#f4f6f9}.nav{height:174rpx;padding:68rpx 38rpx 0;display:flex;justify-content:space-between;align-items:center;box-sizing:border-box;color:#202b3e;font-size:32rpx;font-weight:700}.nav view{width:80rpx;font-size:25rpx}.nav-back{display:flex;align-items:center;justify-content:center}.back-icon{width:44rpx;height:44rpx}.balance{margin:0 34rpx;padding:38rpx 32rpx;background:#fff;border-radius:34rpx;text-align:center;color:#7d8797}.amount{display:block;margin:15rpx 0;color:#273345;font-size:58rpx;font-weight:700}.button{height:88rpx;border-radius:44rpx;background:#ff7625;color:#fff;line-height:88rpx;font-weight:700}.heading{display:flex;justify-content:space-between;padding:35rpx 42rpx 18rpx;color:#273345;font-size:30rpx;font-weight:700}.heading text:last-child{color:#a0a9b7;font-size:24rpx}.scroll{height:calc(100vh - 555rpx);padding:0 34rpx;box-sizing:border-box}.record{padding:26rpx 28rpx;margin-bottom:1rpx;background:#fff}.record view{display:flex;justify-content:space-between;color:#2c3748;font-size:28rpx;font-weight:700}.record-bottom{margin-top:18rpx!important;color:#7e8999!important;font-size:23rpx!important;font-weight:400!important}.money{font-weight:700}.status{font-weight:700}.pending,.processing{color:#e58b28}.paid{color:#249765}.rejected,.failed,.reason{color:#e95b5b}.reason{display:block;margin-top:8rpx;font-size:22rpx}.state{padding:120rpx;text-align:center;color:#8993a1}</style>
<style>.page{height:100vh;background:#f4f6f9}.nav{position:relative;width:100%;box-sizing:border-box;color:#202b3e;font-size:32rpx;font-weight:700}.nav-back{position:absolute;left:16px;display:flex;width:48px;justify-content:flex-start;align-items:center;z-index:2}.nav-title{position:absolute;left:112rpx;right:112rpx;display:flex;justify-content:center;align-items:center;color:#202b3e;font-size:34rpx;font-weight:700;line-height:1;white-space:nowrap}.back-icon{width:44rpx;height:44rpx}.balance{margin:0 34rpx;padding:38rpx 32rpx;background:#fff;border-radius:34rpx;text-align:center;color:#7d8797}.amount{display:block;margin:15rpx 0;color:#273345;font-size:58rpx;font-weight:700}.button{height:88rpx;border-radius:44rpx;background:#ff7625;color:#fff;line-height:88rpx;font-weight:700}.heading{display:flex;justify-content:space-between;padding:35rpx 42rpx 18rpx;color:#273345;font-size:30rpx;font-weight:700}.heading text:last-child{color:#a0a9b7;font-size:24rpx}.scroll{height:calc(100vh - 555rpx);padding:0 34rpx;box-sizing:border-box}.record{padding:26rpx 28rpx;margin-bottom:1rpx;background:#fff}.record view{display:flex;justify-content:space-between;color:#2c3748;font-size:28rpx;font-weight:700}.record-bottom{margin-top:18rpx!important;color:#7e8999!important;font-size:23rpx!important;font-weight:400!important}.money{font-weight:700}.status{font-weight:700}.pending,.processing{color:#e58b28}.paid{color:#249765}.rejected,.failed,.reason{color:#e95b5b}.reason{display:block;margin-top:8rpx;font-size:22rpx}.state{padding:120rpx;text-align:center;color:#8993a1}</style>
talent-card-admin/src/router/index.ts
@@ -135,7 +135,7 @@
          meta: { title: '订单列表' },
        },
        {
          path: 'orders/:id',
          path: 'orders/:orderNo',
          name: 'order-detail',
          component: () => import('../views/OrderDetailView.vue'),
          meta: { title: '订单详情' },
@@ -276,7 +276,7 @@
          path: 'talent-policy-management',
          name: 'talent-policy-management',
          component: () => import('../views/TalentArticleManagementView.vue'),
          meta: { title: '人才政策管理' },
          meta: { title: '人才政策管理', businessKind: 'talentPolicyHome' },
        },
        {
          path: 'talent-policies',
talent-card-admin/src/style.css
@@ -577,11 +577,12 @@
}
.merchant-form-row.address-row {
  grid-template-columns: 58px minmax(300px, 420px) 72px;
  grid-template-columns: 58px minmax(0, 1fr) 72px;
}
.merchant-form-row.address-row .el-input {
  width: 420px;
  width: 100%;
  min-width: 0;
}
.merchant-form-row.address-row .el-button,
@@ -589,6 +590,11 @@
  width: 72px;
}
.merchant-form-row.address-row .el-button {
  flex: 0 0 72px;
  white-space: nowrap;
}
.merchant-cover-row {
  grid-template-columns: 58px 268px 92px;
  align-items: end;
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -182,6 +182,7 @@
        result.put("discountRate", configuredDiscountRate(right.get("discount")));
        result.put("subsidyEnabled", booleanValue(right.get("governmentSubsidy")) ? 1 : 0);
        result.put("consumeDesc", right.get("consumeDesc"));
        result.put("benefitText", configuredBenefitText(right));
        return result;
    }
@@ -324,12 +325,37 @@
                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("");
        String configuredDescription = configuredRules(merchantId, cardLevel).stream()
                .map(rule -> string(rule.get("benefitText")))
                .filter(value -> !value.isBlank())
                .findFirst()
                .orElse("");
        if (configuredDescription.isBlank()) {
            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("以当前商户到店消费规则为准");
    }
    /** Build the displayed benefit from the same structured fields used for pricing. */
    private String configuredBenefitText(Map<String, Object> right) {
        List<String> parts = new ArrayList<>();
        String consumeType = string(right.get("consumeType"));
        if ("优惠".equals(consumeType)) {
            String amount = string(right.get("consumeCoupon"));
            if (!amount.isBlank()) parts.add("到店消费立减" + amount + "元");
        } else {
            String discount = "折扣".equals(consumeType) ? string(right.get("consumeDiscount")) : string(right.get("discount"));
            if (!discount.isBlank()) parts.add("到店消费" + discount + "折");
        }
        return String.join(",", parts);
    }
    String benefitTextForMerchant(Long merchantId, String cardLevel) {
        return benefitText(merchantId, cardLevel);
    }
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantManagementService.java
@@ -9,6 +9,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.PageResult;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
@@ -154,6 +155,8 @@
    @Transactional(readOnly = true)
    public List<Map<String, Object>> benefits(MerchantPrincipal principal) {
        store(principal);
        List<Map<String, Object>> configured = configuredBenefits(principal.merchantId());
        if (!configured.isEmpty()) return configured;
        return rows("""
                select card_level, card_name, benefit_text, theme
                from merchant_card_benefit
@@ -167,6 +170,92 @@
        )).toList();
    }
    private List<Map<String, Object>> configuredBenefits(Long merchantId) {
        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 || value(raw, "").isBlank()) return List.of();
        List<Map<String, Object>> rights = new ArrayList<>();
        try {
            if (raw instanceof List<?> list) {
                for (Object item : list) if (item instanceof Map<?, ?> map) rights.add(copyMap(map));
            } else {
                for (Object item : JSON.parseArray(value(raw, ""), Map.class)) if (item instanceof Map<?, ?> map) rights.add(copyMap(map));
            }
        } catch (Exception ignored) {
            return List.of();
        }
        Map<String, Map<String, Object>> byLevel = new LinkedHashMap<>();
        for (Map<String, Object> right : rights) {
            if (!booleanValue(right.get("enabled"))) continue;
            String level = normalizeLevel(value(right.get("level"), ""));
            if (!List.of("EXCELLENT", "OUTSTANDING", "ELITE").contains(level)) continue;
            byLevel.putIfAbsent(level, right);
        }
        return List.of("EXCELLENT", "OUTSTANDING", "ELITE").stream()
                .filter(byLevel::containsKey).map(level -> toBenefit(byLevel.get(level)))
                .filter(item -> !value(item.get("benefitText"), "").isBlank()).toList();
    }
    private Map<String, Object> toBenefit(Map<String, Object> right) {
        String level = value(right.get("level"), "");
        String normalized = normalizeLevel(level);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("cardLevel", normalized);
        result.put("cardName", value(right.get("cardName"), cardName(normalized, level)));
        String text = value(right.get("detail"), "");
        if (text.isBlank()) text = value(right.get("consumeDesc"), "");
        if (text.isBlank()) text = configuredConsumeText(right);
        result.put("benefitText", text);
        result.put("theme", theme(normalized));
        return result;
    }
    private String configuredConsumeText(Map<String, Object> right) {
        String type = value(right.get("consumeType"), "");
        if ("优惠".equals(type) && !value(right.get("consumeCoupon"), "").isBlank()) return "到店消费立减" + right.get("consumeCoupon") + "元";
        String discount = "折扣".equals(type) ? value(right.get("consumeDiscount"), "") : value(right.get("discount"), "");
        return discount.isBlank() ? "" : "到店消费" + discount + "折";
    }
    private String normalizeLevel(String value) {
        String upper = value.toUpperCase();
        if (upper.contains("EXCELLENT") || value.contains("卓越")) return "EXCELLENT";
        if (upper.contains("OUTSTANDING") || value.contains("杰出")) return "OUTSTANDING";
        if (upper.contains("ELITE") || value.contains("菁英") || value.contains("精英")) return "ELITE";
        return upper;
    }
    private String cardName(String normalized, String fallback) {
        return switch (normalized) {
            case "EXCELLENT" -> "卓越人才卡";
            case "OUTSTANDING" -> "杰出人才卡";
            case "ELITE" -> "菁英人才卡";
            default -> fallback;
        };
    }
    private String theme(String normalized) {
        return switch (normalized) {
            case "OUTSTANDING" -> "outstanding";
            case "ELITE" -> "elite";
            default -> "excellent";
        };
    }
    private boolean booleanValue(Object value) {
        if (value instanceof Boolean bool) return bool;
        if (value instanceof Number number) return number.intValue() != 0;
        String text = value(value, "");
        return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text);
    }
    private Map<String, Object> copyMap(Map<?, ?> source) {
        Map<String, Object> result = new LinkedHashMap<>();
        for (Map.Entry<?, ?> entry : source.entrySet()) result.put(String.valueOf(entry.getKey()), entry.getValue());
        return result;
    }
    private Map<String, Object> merchant(Long merchantId) {
        List<Map<String, Object>> merchants = rows("""
                select id, merchant_name, address, cover_url, business_status, status, updated_at
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
@@ -208,7 +208,11 @@
        String category = "GROUP_BUY".equals(orderType) ? "group" : ("STORE_CONSUME".equals(orderType) ? "store" : "stay");
        Long orderStatus = number(order.get("orderStatus"));
        Long verifyStatus = number(order.get("verifyStatus"));
        String status = orderStatus != null && orderStatus == 0 ? "pending_payment" : (verifyStatus != null && verifyStatus == 1 ? "completed" : "pending_verification");
        LocalDateTime verifyTime = time(order.get("verifyTime"));
        // 核销状态必须同时有有效核销记录和核销时间。历史数据可能只更新了
        // benefit_order.verify_status,不能据此把订单展示为已完成。
        boolean verified = verifyStatus != null && verifyStatus == 1 && verifyTime != null;
        String status = orderStatus != null && orderStatus == 0 ? "pending_payment" : (verified ? "completed" : "pending_verification");
        result.put("category", category);
        result.put("status", status);
        result.put("statusText", "pending_payment".equals(status) ? "待支付" : ("completed".equals(status) ? "已完成" : "待核销"));
@@ -217,11 +221,24 @@
        result.put("title", orderTitle(order, category));
        result.put("benefitName", currentBenefitText(order, category));
        long orderAmount = number(order.get("orderAmount")) == null ? 0 : number(order.get("orderAmount"));
        long payAmount = number(order.get("payAmount")) == null ? 0 : number(order.get("payAmount"));
        long basePayAmount = number(order.get("basePayAmount")) == null ? orderAmount : number(order.get("basePayAmount"));
        long couponAmount = number(order.get("couponAmount")) == null ? 0 : number(order.get("couponAmount"));
        result.put("talentDiscountAmount", Math.max(0, orderAmount - basePayAmount));
        result.put("couponAmount", Math.max(0, couponAmount));
        LocalDateTime verifyTime = time(order.get("verifyTime"));
        long totalDiscount = Math.max(0, orderAmount - payAmount);
        long talentDiscountAmount = Math.max(0, orderAmount - basePayAmount);
        if ("GROUP_BUY".equals(orderType)) {
            couponAmount = Math.max(0, Math.min(couponAmount, totalDiscount));
            // Older group-buy orders were created before all price snapshots were
            // consistently persisted. Reconcile the displayed breakdown with the
            // actual paid amount while preserving any valid coupon snapshot.
            if (talentDiscountAmount + couponAmount != totalDiscount) {
                talentDiscountAmount = Math.max(0, totalDiscount - couponAmount);
            }
            result.put("subsidyAmount", totalDiscount);
        }
        result.put("talentDiscountAmount", talentDiscountAmount);
        result.put("couponAmount", couponAmount);
        result.put("subsidyAmount", totalDiscount);
        boolean canRevoke = "GROUP_BUY".equals(orderType) && verifyTime != null && Duration.between(verifyTime, LocalDateTime.now()).toHours() < REVOCATION_HOURS && verifyStatus != null && verifyStatus == 1;
        result.put("canRevoke", canRevoke);
        result.put("revokeDeadline", verifyTime == null ? null : verifyTime.plusHours(REVOCATION_HOURS).toString());
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantWorkServiceTests.java
@@ -90,6 +90,23 @@
        assertThat(jdbc.queryForObject("select status from verify_record where order_id = ?", Integer.class, orderId)).isEqualTo(2);
    }
    @Test
    void derivesConsistentDiscountBreakdownForLegacyOrderSnapshots() {
        String orderNo = "LEGACY-" + System.nanoTime();
        jdbc.update("""
                insert into benefit_order(order_no, talent_id, merchant_id, store_id, order_type, order_amount,
                                          subsidy_amount, base_pay_amount, coupon_amount, pay_amount,
                                          order_status, verify_status, deleted)
                values (?, ?, ?, ?, 'GROUP_BUY', 14800, 0, 14800, 0, 1, 1, 0, 0)
                """, orderNo, talentId, merchantId, storeId);
        Map<String, Object> detail = merchantWorkService.order(orderNo, merchantId, storeId);
        assertThat(detail).containsEntry("talentDiscountAmount", 14799L)
                .containsEntry("couponAmount", 0L)
                .containsEntry("subsidyAmount", 14799L);
    }
    private String insertOrder(String type, int orderStatus, int verifyStatus, LocalDateTime createdAt) {
        String orderNo = "ORDER-" + System.nanoTime();
        jdbc.update("""