shinnosuke
2026-08-13 dbfab7f57fbd0189b31f5f16854b4d725ee79a12
又又又更新
1个文件已添加
10个文件已修改
348 ■■■■■ 已修改文件
rencaika/manifest.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/manifest.json 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/consume-input/consume-input.uvue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/verification-success/verification-success.uvue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/project.config.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentService.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMiniProgramPaymentGateway.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/resources/db/migration/V109__sync_premium_topic_merchant_mappings.sql 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/manifest.json
@@ -9,7 +9,7 @@
    "quickapp": {},
    /* 小程序特有相关 */
    "mp-weixin": {
        "appid": "wx6ae2cadfea28fce1",
        "appid": "wxcfee477acb54d179",
        "setting": {
            "urlCheck": false
        },
rencaika_merchant/manifest.json
@@ -1,6 +1,6 @@
{
    "name": "人才卡商户端",
    "appid": "wx6ae2cadfea28fce1",
    "appid": "wx550e520ad19458ea",
    "description": "",
    "versionName": "1.0.0",
    "versionCode": "100",
@@ -9,7 +9,7 @@
    "quickapp": {},
    /* 小程序特有相关 */
    "mp-weixin": {
        "appid": "wxe91f1af7638aa5dd",
        "appid": "wx550e520ad19458ea",
        "setting": {
            "urlCheck": false
        },
rencaika_merchant/pages/consume-input/consume-input.uvue
@@ -5,7 +5,7 @@
        <view v-else-if="loadState == 'error'" class="page-state"><text>{{loadError}}</text><text class="retry-text" @tap="loadDetail">重新加载</text></view>
        <scroll-view v-else class="content" scroll-y="true">
            <view class="talent-card"><view class="talent-card-glow"></view><view class="talent-card-head"><text class="talent-name">{{customerName}}</text><text class="talent-level">{{cardLevel}}</text></view><text class="talent-mobile">{{displayMobile(customerMobile)}}</text><view class="talent-benefit-line"><text class="talent-benefit-label">当前权益</text><text class="talent-benefit">{{benefitText}}</text></view></view>
            <view class="amount-card"><text class="card-label">消费金额</text><view class="amount-input-row"><text class="currency">¥</text><input class="amount-input" type="digit" placeholder="0.00" :value="amountText" @input="onAmountInput" @blur="quoteCurrentAmount" /></view><text v-if="amountError.length > 0" class="field-error">{{amountError}}</text></view>
            <view class="amount-card"><text class="card-label">消费金额</text><view class="amount-input-row"><text class="currency">¥</text><input class="amount-input" type="digit" placeholder="" :value="amountText" @input="onAmountInput" @blur="quoteCurrentAmount" /></view><text v-if="amountError.length > 0" class="field-error">{{amountError}}</text></view>
            <view class="method-card"><text class="card-label">优惠方式</text><view v-if="selectedRuleId.length > 0" class="benefit-tag-list"><text class="benefit-tag benefit-rule-tag">{{selectedRuleName()}}</text><text class="benefit-tag benefit-card-tag">{{cardLevel}}</text></view><text v-else class="field-error">当前人才卡暂未配置可用优惠</text></view>
            <view class="summary-card"><view class="summary-row"><text>原价</text><text>¥{{formatMoney(quote.originalAmount)}}</text></view><view class="summary-row"><text>优惠金额</text><text>- ¥{{formatMoney(quote.discountAmount)}}</text></view><view class="summary-row strong-row"><text>用户实付</text><text class="pay-amount">¥{{formatMoney(quote.payAmount)}}</text></view><view class="summary-row"><text>补贴金额</text><text>¥{{formatMoney(quote.subsidyAmount)}}</text></view></view>
            <view v-if="coupons.length > 0" class="method-card"><text class="card-label">可用优惠券</text><view v-for="coupon in coupons" :key="coupon.id" class="rule-item" :class="coupon.id == selectedCouponId ? 'rule-selected' : ''" @tap="selectCoupon(coupon.id)"><view><text class="rule-name">{{coupon.name}}</text><text class="rule-desc">立减 ¥{{formatMoney(coupon.couponAmount)}}</text></view><text class="rule-check">{{coupon.id == selectedCouponId ? '✓' : ''}}</text></view><view class="rule-item" @tap="selectCoupon('')"><text class="rule-name">不使用优惠券</text></view></view>
@@ -149,48 +149,50 @@
</script>
<style>
    page { background-color: #F7F7F7; }
    .page { display: flex; width: 100%; height: 100vh; flex-direction: column; background-color: #F7F7F7; overflow: hidden; }
    .navigation-bar { display: flex; height: 176rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 26rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; }
    .navigation-title { color: #293344; font-size: 34rpx; line-height: 48rpx; font-weight: 700; }
    .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; left: 24rpx; bottom: 14rpx; }
    .back-icon { width: 38rpx; height: 38rpx; }
    .content { flex: 1; height: 0; padding: 32rpx 30rpx 0; box-sizing: border-box; }
    .talent-card, .amount-card, .method-card, .summary-card { border-radius: 14rpx; background-color: #FFFFFF; box-shadow: 0 4rpx 18rpx rgba(49, 62, 83, 0.06); box-sizing: border-box; }
    .talent-card { display: flex; min-height: 204rpx; padding: 28rpx 30rpx; flex-direction: column; border-width: 1rpx; border-style: solid; border-color: #E4BE59; background-image: linear-gradient(135deg, #C99D32 0%, #F7E69D 55%, #E5BD4E 100%); position: relative; overflow: hidden; }
    .talent-card-glow { width: 180rpx; height: 180rpx; border-radius: 90rpx; position: absolute; top: -72rpx; right: -15rpx; background-color: rgba(255, 255, 255, 0.18); }
    page { background-color: #F5F6F8; }
    .page { display: flex; width: 100%; height: 100vh; flex-direction: column; background-color: #F5F6F8; overflow: hidden; }
    .navigation-bar { display: flex; height: 164rpx; flex: none; justify-content: center; align-items:flex-end; padding-bottom: 24rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; }
    .navigation-title { position: absolute; right: 0; bottom: 24rpx; left: 0; color: #273348; font-size: 34rpx; line-height: 48rpx; font-weight: 700; text-align: center; }
    .back-button { display: flex; width: 70rpx; height: 70rpx; justify-content: center; align-items: center; position: absolute; z-index: 2; left: 20rpx; bottom: 13rpx; }
    .back-icon { width: 40rpx; height: 40rpx; }
    .content { flex: 1; height: 0; padding: 25rpx 30rpx 0; box-sizing: border-box; }
    .talent-card, .amount-card, .method-card, .summary-card { border-radius: 26rpx; background-color: #FFFFFF; box-sizing: border-box; }
    .talent-card { display: flex; min-height: 226rpx; padding: 30rpx 30rpx 25rpx; flex-direction: column; border-width: 1rpx; border-style: solid; border-color: #E1B94D; background-image: linear-gradient(135deg, #C4942C 0%, #F8E99B 57%, #E1B74B 100%); box-shadow: 0 13rpx 26rpx rgba(174, 126, 26, .16); position: relative; overflow: hidden; }
    .talent-card-glow { width: 190rpx; height: 190rpx; border-width: 1rpx; border-style: solid; border-color: rgba(255,255,255,.28); border-radius: 100rpx; position: absolute; top: -56rpx; right: -17rpx; background-color: rgba(255,255,255,.12); }
    .talent-card-head { display: flex; flex-direction: row; justify-content: space-between; align-items: center; position: relative; }
    .talent-name { color: #FFFFFF; font-size: 38rpx; line-height: 53rpx; font-weight: 700; }
    .talent-level { padding: 8rpx 18rpx; border-radius: 22rpx; color: #FFFFFF; font-size: 21rpx; line-height: 30rpx; font-weight: 600; background-color: rgba(149, 104, 22, 0.46); }
    .talent-mobile { display: block; margin-top: 14rpx; color: #FFFFFF; font-size: 27rpx; line-height: 38rpx; font-weight: 600; position: relative; }
    .talent-benefit-line { display: flex; margin-top: 14rpx; flex-direction: row; align-items: center; position: relative; }
    .talent-benefit-label { color: rgba(255, 255, 255, 0.78); font-size: 22rpx; line-height: 31rpx; }
    .talent-benefit { margin-left: 18rpx; color: #FFFFFF; font-size: 22rpx; line-height: 31rpx; font-weight: 600; }
    .amount-card, .method-card, .summary-card { margin-top: 26rpx; padding: 27rpx 30rpx; }
    .card-label { display: block; color: #5F6979; font-size: 26rpx; line-height: 37rpx; }
    .amount-input-row { display: flex; height: 88rpx; margin-top: 9rpx; flex-direction: row; align-items: center; }
    .currency { color: #A1A9B6; font-size: 36rpx; line-height: 50rpx; font-weight: 700; }
    .amount-input { height: 88rpx; margin-left: 10rpx; flex: 1; color: #111827; font-size: 50rpx; line-height: 70rpx; font-weight: 700; }
    .field-error { display: block; margin-top: 12rpx; color: #EE5A5A; font-size: 23rpx; line-height: 33rpx; }
    .benefit-tag-list { display: flex; margin-top: 16rpx; flex-direction: row; flex-wrap: wrap; align-items: center; }
    .benefit-tag { display: block; padding: 8rpx 18rpx; border-radius: 22rpx; font-size: 22rpx; line-height: 31rpx; font-weight: 600; }
    .benefit-rule-tag { color: #F26A31; background-color: #FFF1E8; }
    .benefit-card-tag { margin-left: 12rpx; color: #FFFFFF; background-color: #D7AD43; }
    .rule-list { margin-top: 14rpx; }
    .talent-name { color: #FFFFFF; font-size: 39rpx; line-height: 55rpx; font-weight: 700; }
    .talent-level { padding: 8rpx 17rpx; border-radius: 22rpx; color: #FFFFFF; font-size: 21rpx; line-height: 30rpx; font-weight: 600; background-color: rgba(151, 105, 21, .46); }
    .talent-mobile { display: block; margin-top: 13rpx; color: #FFFFFF; font-size: 28rpx; line-height: 40rpx; font-weight: 600; position: relative; }
    .talent-benefit-line { display: flex; margin-top: 15rpx; flex-direction: row; align-items: center; position: relative; }
    .talent-benefit-label { color: rgba(255,255,255,.78); font-size: 22rpx; line-height: 31rpx; }
    .talent-benefit { margin-left: 17rpx; color: #FFFFFF; font-size: 22rpx; line-height: 31rpx; font-weight: 600; }
    .amount-card { min-height: 176rpx; padding: 26rpx 30rpx 22rpx; margin-top: 22rpx; }
    .card-label { display: block; color: #8E98A7; font-size: 25rpx; line-height: 36rpx; }
    .amount-input-row { display: flex; height: 86rpx; margin-top: 10rpx; flex-direction: row; align-items: center; }
    .currency { color: #B4BDCB; font-size: 39rpx; line-height: 55rpx; font-weight: 600; }
    .amount-input { height: 86rpx; margin-left: 14rpx; flex: 1; color: #253146; font-size: 48rpx; line-height: 68rpx; font-weight: 700; }
    .field-error { display: block; margin-top: 7rpx; color: #EC6262; font-size: 22rpx; line-height: 31rpx; }
    .method-card, .summary-card { margin-top: 20rpx; padding: 24rpx 30rpx; }
    .method-card { min-height: 126rpx; }
    .benefit-tag-list { display: flex; min-height: 40rpx; margin-top: 15rpx; flex-direction: row; flex-wrap: wrap; align-items: center; }
    .benefit-tag { display: block; padding: 7rpx 16rpx; border-radius: 20rpx; font-size: 21rpx; line-height: 30rpx; font-weight: 600; }
    .benefit-rule-tag { color: #F17936; background-color: #FFF2E8; }
    .benefit-card-tag { margin-left: 12rpx; color: #FFFFFF; background-color: #CDA13A; }
    .rule-item { display: flex; min-height: 68rpx; padding: 13rpx 0; flex-direction: row; justify-content: space-between; align-items: center; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #EEF0F3; }
    .rule-item:last-child { border-bottom-width: 0; }
    .rule-name { display: block; color: #202B3D; font-size: 27rpx; line-height: 38rpx; font-weight: 600; }
    .rule-desc { display: block; margin-top: 3rpx; color: #8A94A3; font-size: 22rpx; line-height: 31rpx; }
    .rule-check { display: flex; width: 34rpx; height: 34rpx; justify-content: center; align-items: center; border-width: 2rpx; border-style: solid; border-color: #CBD2DA; border-radius: 17rpx; color: #FFFFFF; font-size: 22rpx; line-height: 30rpx; }
    .rule-selected .rule-check { border-color: #FF6B5E; background-color: #FF6B5E; }
    .summary-card { margin-bottom: 20rpx; }
    .summary-row { display: flex; min-height: 50rpx; flex-direction: row; justify-content: space-between; align-items: center; color: #657082; font-size: 25rpx; line-height: 35rpx; }
    .strong-row { color: #3D4858; font-weight: 600; }
    .pay-amount { color: #F45C60; font-weight: 700; }
    .content-bottom-space { height: 170rpx; }
    .bottom-bar { display: flex; height: 142rpx; padding: 20rpx 84rpx 30rpx; flex: none; background-color: #FFFFFF; box-sizing: border-box; }
    .submit-button { display: flex; height: 92rpx; flex: 1; justify-content: center; align-items: center; border-radius: 12rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-color: #FF6B5E; }
    .submit-disabled { background-color: #C8C8C8; }
    .summary-card { padding-top: 19rpx; padding-bottom: 18rpx; }
    .summary-row { display: flex; min-height: 47rpx; flex-direction: row; justify-content: space-between; align-items: center; color: #8C96A6; font-size: 25rpx; line-height: 35rpx; }
    .summary-row text:last-child { color: #273348; font-weight: 600; }
    .strong-row { color: #273348; font-weight: 700; }
    .strong-row text:last-child, .pay-amount { color: #FF6538 !important; font-weight: 700; }
    .content-bottom-space { height: 164rpx; }
    .bottom-bar { display: flex; height: 144rpx; padding: 19rpx 96rpx 31rpx; flex: none; background-color: #FFFFFF; box-sizing: border-box; }
    .submit-button { display: flex; height: 88rpx; flex: 1; justify-content: center; align-items: center; border-radius: 48rpx; color: #FFFFFF; font-size: 29rpx; line-height: 41rpx; font-weight: 700; background-image: linear-gradient(90deg, #FF7628 0%, #FF9631 100%); box-shadow: 0 9rpx 18rpx rgba(255, 112, 37, .21); }
    .submit-disabled { background-image: none; background-color: #C9C9C9; box-shadow: none; }
    .page-state { display: flex; flex: 1; flex-direction: column; justify-content: center; align-items: center; color: #778394; font-size: 27rpx; line-height: 38rpx; }
    .retry-text { margin-top: 24rpx; color: #FF6B5E; font-weight: 700; }
</style>
rencaika_merchant/pages/verification-success/verification-success.uvue
@@ -1,6 +1,6 @@
<template>
    <view class="page">
        <view class="navigation-bar"><view class="back-button" @tap="returnToScan"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view><text class="navigation-title">核销结果</text></view>
        <view class="navigation-bar"><view class="back-button" @tap="returnToScan"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view></view>
        <view class="result-content"><view class="success-shell"><image class="success-icon" src="/static/icons/lucide/badge-check-orange.svg" mode="aspectFit"></image></view><text class="result-title">核销成功</text><text class="result-message">订单号:{{orderNo}}</text><view class="return-button" @tap="viewOrder"><text>查看订单</text></view></view>
    </view>
</template>
@@ -22,13 +22,12 @@
    page { background-color: #F6F5F2; }
    .page { display: flex; width: 100%; height: 100vh; flex-direction: column; background-color: #F6F5F2; }
    .navigation-bar { display: flex; height: 176rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 26rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; }
    .navigation-title { color: #293344; font-size: 34rpx; line-height: 48rpx; font-weight: 700; }
    .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; left: 24rpx; bottom: 14rpx; }
    .back-icon { width: 38rpx; height: 38rpx; }
    .result-content { display: flex; flex: 1; padding: 176rpx 64rpx 0; flex-direction: column; align-items: center; box-sizing: border-box; }
    .success-shell { display: flex; width: 170rpx; height: 170rpx; justify-content: center; align-items: center; border-radius: 85rpx; background-color: #FFF0E7; box-shadow: 0 16rpx 34rpx rgba(255, 107, 44, 0.13); }
    .success-icon { width: 100rpx; height: 100rpx; }
    .result-title { margin-top: 42rpx; color: #273345; font-size: 38rpx; line-height: 54rpx; font-weight: 700; }
    .result-message { margin-top: 20rpx; color: #8A94A3; font-size: 25rpx; line-height: 36rpx; text-align: center; }
    .return-button { display: flex; width: 240rpx; height: 72rpx; margin-top: 34rpx; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFC9A7; border-radius: 38rpx; color: #FF5B3B; font-size: 26rpx; line-height: 37rpx; font-weight: 600; background-color: #FFF8EF; }
    .result-content { display: flex; flex: 1; padding: 222rpx 64rpx 0; flex-direction: column; align-items: center; box-sizing: border-box; }
    .success-shell { display: flex; width: 144rpx; height: 144rpx; justify-content: center; align-items: center; border-width: 12rpx; border-style: solid; border-color: #FFE2C4; border-radius: 78rpx; background-color: #FFECCE; box-sizing: border-box; }
    .success-icon { width: 86rpx; height: 86rpx; }
    .result-title { margin-top: 38rpx; color: #273345; font-size: 32rpx; line-height: 45rpx; font-weight: 700; }
    .result-message { margin-top: 12rpx; color: #9AA6B8; font-size: 22rpx; line-height: 31rpx; text-align: center; }
    .return-button { display: flex; width: 216rpx; height: 68rpx; margin-top: 28rpx; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFCCAA; border-radius: 36rpx; color: #FF6B3C; font-size: 25rpx; line-height: 35rpx; font-weight: 600; background-color: #FFF4E8; }
</style>
rencaika_merchant/project.config.json
@@ -1,5 +1,5 @@
{
  "appid": "wxfb87c6ef164c2922",
  "appid": "wx550e520ad19458ea",
  "projectname": "人才卡商户端",
  "compileType": "miniprogram",
  "miniprogramRoot": "",
talent-card-admin/src/views/MerchantCreateView.vue
@@ -89,6 +89,9 @@
const isSpouseEmploymentMerchant = computed(() => normalizedMerchantType.value === '配偶推荐就业')
const isApprovalMerchant = computed(() => normalizedMerchantType.value === '行政审批')
const isServiceMerchant = computed(() => isFoodMerchant.value || normalizedMerchantType.value === '景区服务')
const supportsStoreConsumption = computed(() => [
  '酒店服务', '景区服务', '餐饮服务', '口腔治疗保健', '免费体检',
].includes(normalizedMerchantType.value) || (isCarWashMerchant.value && form.miniProgramJumpType === 'normal'))
const merchantAppLoginOptionalTypes = [
  '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务',
]
@@ -518,6 +521,8 @@
function defaultMerchantRights(): MerchantRightConfig[] {
  const currentType = queryString('merchantType', '') || normalizedMerchantType.value
  const supportsDefaultStoreConsumption = ['餐饮服务', '景区服务', '口腔治疗保健', '免费体检'].includes(currentType)
    || (currentType === '自助洗车' && form.miniProgramJumpType === 'normal')
  const isHotel = currentType === '酒店服务'
  const isCarWash = currentType === '自助洗车'
  const isHealthCheck = currentType === '免费体检'
@@ -535,33 +540,33 @@
        enabled: true,
        tag: '',
        detail: '',
        storeConsume: false,
        storeConsume: true,
        discount: '',
        governmentSubsidy: false,
        governmentSubsidy: true,
        consumeDesc: '',
        consumeEnabled: false,
        consumeEnabled: true,
      },
      {
        level: '杰出人才卡',
        enabled: true,
        tag: '',
        detail: '',
        storeConsume: false,
        storeConsume: true,
        discount: '',
        governmentSubsidy: false,
        governmentSubsidy: true,
        consumeDesc: '',
        consumeEnabled: false,
        consumeEnabled: true,
      },
      {
        level: '菁英人才卡',
        enabled: true,
        tag: '',
        detail: '',
        storeConsume: false,
        storeConsume: true,
        discount: '',
        governmentSubsidy: false,
        governmentSubsidy: true,
        consumeDesc: '',
        consumeEnabled: false,
        consumeEnabled: true,
      },
    ]
  }
@@ -729,44 +734,44 @@
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
        },
        {
          level: '杰出人才卡',
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
        },
        {
          level: '菁英人才卡',
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
        },
        {
          level: '青年人才卡',
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
        },
      ]
    }
@@ -778,11 +783,11 @@
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
          parkingType: 'free',
          parkingScope: 'all',
        },
@@ -791,11 +796,11 @@
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
          parkingType: 'paid',
        },
        {
@@ -803,11 +808,11 @@
          enabled: true,
          tag: '',
          detail: '',
          storeConsume: true,
          storeConsume: false,
          discount: '',
          governmentSubsidy: true,
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: true,
          consumeEnabled: false,
          parkingType: 'paid',
        },
      ]
@@ -904,33 +909,33 @@
    enabled: true,
    tag: '',
    detail: '',
    storeConsume: true,
    storeConsume: supportsDefaultStoreConsumption,
    discount: '',
    governmentSubsidy: true,
    governmentSubsidy: supportsDefaultStoreConsumption,
    consumeDesc: '',
    consumeEnabled: true,
    consumeEnabled: supportsDefaultStoreConsumption,
  },
  {
    level: '杰出人才卡',
    enabled: true,
    tag: '',
    detail: '',
    storeConsume: true,
    storeConsume: supportsDefaultStoreConsumption,
    discount: '',
    governmentSubsidy: true,
    governmentSubsidy: supportsDefaultStoreConsumption,
    consumeDesc: '',
    consumeEnabled: true,
    consumeEnabled: supportsDefaultStoreConsumption,
  },
  {
    level: '菁英人才卡',
    enabled: true,
    tag: '',
    detail: '',
    storeConsume: true,
    storeConsume: supportsDefaultStoreConsumption,
    discount: '',
    governmentSubsidy: true,
    governmentSubsidy: supportsDefaultStoreConsumption,
    consumeDesc: '',
    consumeEnabled: true,
    consumeEnabled: supportsDefaultStoreConsumption,
  },
  {
    level: '普通游客',
@@ -1320,6 +1325,18 @@
function serializeMerchantRightsConfig() {
  return rightsConfigs.map((item, index) => {
    const payload = { ...item }
    if (!supportsStoreConsumption.value) {
      payload.storeConsume = false
      payload.discount = ''
      payload.governmentSubsidy = false
      payload.consumeDesc = ''
      payload.consumeEnabled = false
      payload.consumeType = ''
      payload.consumeDiscount = ''
      payload.consumeDiscountSubsidy = false
      payload.consumeCoupon = ''
      payload.consumeCouponSubsidy = false
    }
    if (isCarWashMerchant.value) {
      if (index === 0) {
        Object.assign(payload, carWashJumpMeta())
@@ -2360,7 +2377,7 @@
              </div>
          </div>
          <template v-if="item.consumeEnabled && !isApartmentMerchant && !isFinancialMerchant && !isMovieMerchant && !isSpouseEmploymentMerchant && (!isCarWashMerchant || form.miniProgramJumpType === 'normal') && (!isParkingMerchant || item.parkingType === 'paid')">
          <template v-if="supportsStoreConsumption && item.consumeEnabled">
            <template v-if="isHotelMerchant">
              <div class="merchant-consume-row hotel-voucher-row">
                <strong>免费券</strong>
@@ -2401,7 +2418,7 @@
              </div>
            </template>
            <template v-else>
              <div class="merchant-consume-row" v-if="false">
              <div class="merchant-consume-row">
                <strong>到店消费</strong>
                <el-switch v-model="item.storeConsume" />
                <span>折扣</span>
@@ -2413,8 +2430,8 @@
            </template>
            <div v-if="!isParkingMerchant" class="merchant-rights-detail-row consume-desc-row">
<!--              <label>到店消费使用描述</label>-->
<!--              <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" />-->
              <label>到店消费使用描述</label>
              <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" />
              <div class="merchant-rights-example">
                <span>示例:</span>
                <div class="merchant-rights-phone-preview">
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -38,7 +38,7 @@
        ensurePending(source);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("verificationId", verificationId);
        result.put("customerName", maskName(string(source.get("talentName"))));
        result.put("customerName", displayName(string(source.get("talentName")), number(source.get("gender"))));
        result.put("customerMobile", maskMobile(string(source.get("talentMobile"))));
        result.put("cardLevel", string(source.get("cardLevel")));
        result.put("benefitText", benefitText(principal.merchantId(), string(source.get("cardLevel"))));
@@ -107,7 +107,7 @@
        List<Map<String, Object>> sources = rows("""
                select a.id attempt_id, a.dynamic_code_id, a.status attempt_status, a.expires_at, a.manual_order_id,
                       d.talent_id, d.card_id, c.card_level,
                       t.name talent_name, t.mobile talent_mobile
                       t.name talent_name, t.mobile talent_mobile, t.gender
                from merchant_verification_attempt a
                join talent_dynamic_code d on d.id = a.dynamic_code_id
                join talent_card c on c.id = d.card_id and c.deleted = 0
@@ -326,13 +326,13 @@
                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("benefitText")))
                .map(rule -> string(rule.get("consumeDesc")))
                .filter(value -> !value.isBlank())
                .findFirst()
                .orElse("");
        if (configuredDescription.isBlank()) {
            configuredDescription = configuredRules(merchantId, cardLevel).stream()
                    .map(rule -> string(rule.get("consumeDesc")))
                    .map(rule -> string(rule.get("benefitText")))
                    .filter(value -> !value.isBlank())
                    .findFirst()
                    .orElse("");
@@ -395,6 +395,12 @@
        return name.length() == 1 ? name + "*" : name.substring(0, 1) + "*".repeat(name.length() - 1);
    }
    private String displayName(String name, Long gender) {
        if (name.isBlank()) return "持卡人";
        if (name.endsWith("先生") || name.endsWith("女士")) return name;
        return name + (gender != null && gender == 2 ? "女士" : "先生");
    }
    private String maskMobile(String mobile) {
        if (mobile.length() < 7) return "";
        return mobile.substring(0, 3) + "****" + mobile.substring(mobile.length() - 4);
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
@@ -45,7 +45,8 @@
        arguments.addAll(filters.arguments());
        String recordSql = """
                select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender, tc.card_level,
                select o.id, o.order_no, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
                       coalesce(nullif(tu.talent_level, ''), tc.card_level) as card_level,
                       b.benefit_name, o.product_name_snapshot, o.order_type, o.order_amount, o.subsidy_amount, o.pay_amount,
                       o.order_status, o.verify_status, o.created_at, verified.verify_time
                from benefit_order o
@@ -70,7 +71,8 @@
    public Map<String, Object> order(String orderNo, Long merchantId, Long storeId) {
        List<Map<String, Object>> rows = query("""
                select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
                       o.card_id, tc.card_no, tc.card_level, o.benefit_id, b.benefit_name,
                       o.card_id, tc.card_no, coalesce(nullif(tu.talent_level, ''), tc.card_level) as card_level,
                       o.benefit_id, b.benefit_name,
                       o.merchant_id, m.merchant_name, o.product_id, o.product_name_snapshot, p.rights_config,
                       o.order_type, o.order_amount, o.base_pay_amount, o.coupon_amount,
                       o.subsidy_amount, o.pay_amount, o.order_status, o.verify_status, o.created_at,
talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentService.java
@@ -15,6 +15,8 @@
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@@ -22,6 +24,7 @@
public class HuifuPaymentService {
    private static final String PROVIDER = "HUIFU";
    private static final Logger log = LoggerFactory.getLogger(HuifuPaymentService.class);
    private final JdbcTemplate jdbc;
    private final TransactionTemplate transactions;
@@ -215,6 +218,7 @@
        result.put("paymentNo", paymentNo);
        result.put("paymentStatus", "PAYMENT_PENDING");
        result.put("paymentParams", parsePayload(payload));
        log.info("Mini-program payment response: paymentNo={}, payload={}", paymentNo, logPayload(result));
        return result;
    }
@@ -306,6 +310,45 @@
        }
    }
    private String logPayload(Object value) {
        try {
            return JSON.toJSONString(maskPaymentPayload(value));
        } catch (Exception ignored) {
            return "{\"log_error\":\"Unable to serialize payment response\"}";
        }
    }
    private Object maskPaymentPayload(Object value) {
        if (value instanceof Map<?, ?> source) {
            Map<String, Object> result = new LinkedHashMap<>();
            for (Map.Entry<?, ?> entry : source.entrySet()) {
                String key = String.valueOf(entry.getKey());
                result.put(key, sensitiveLogKey(key) ? maskLogValue(String.valueOf(entry.getValue())) : maskPaymentPayload(entry.getValue()));
            }
            return result;
        }
        if (value instanceof List<?> source) return source.stream().map(this::maskPaymentPayload).toList();
        if (value instanceof String text && (text.startsWith("{") || text.startsWith("["))) {
            try {
                return maskPaymentPayload(JSON.parse(text));
            } catch (Exception ignored) {
                return text;
            }
        }
        return value;
    }
    private boolean sensitiveLogKey(String key) {
        String normalized = key == null ? "" : key.toLowerCase();
        return normalized.contains("sign") || normalized.contains("secret") || normalized.contains("openid") || normalized.contains("authorization");
    }
    private String maskLogValue(String value) {
        if (value == null || value.isBlank()) return "";
        if (value.length() <= 8) return "***";
        return value.substring(0, 4) + "***" + value.substring(value.length() - 4);
    }
    private LocalDateTime expiryOrDefault(LocalDateTime expiresAt) {
        return expiresAt == null ? LocalDateTime.now().plusMinutes(15) : expiresAt;
    }
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuMiniProgramPaymentGateway.java
@@ -4,6 +4,7 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson.JSON;
@@ -52,8 +53,21 @@
        extendInfo.put("delay_acct_flag", properties.isDelayAccount() ? "Y" : "N");
        extendInfo.put("time_expire", command.expiresAt().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
        request.setExtendInfo(extendInfo);
        log.info("Huifu mini-program payment request: {}", logJson(Map.of(
                "req_date", REQUEST_DATE.format(LocalDateTime.now()),
                "req_seq_id", command.paymentNo(),
                "huifu_id", properties.getMerchantId().trim(),
                "goods_desc", command.goodsDescription(),
                "trans_amt", BigDecimal.valueOf(command.amountInCent(), 2).toPlainString(),
                "trade_type", "T_MINIAPP",
                "system_id", properties.getSystemId().trim(),
                "product_id", properties.getProductId().trim(),
                "extend_info", extendInfo
        )));
        try {
            return BasePayClient.request(request, false);
            Map<String, Object> response = BasePayClient.request(request, false);
            log.info("Huifu mini-program payment response: {}", logJson(response));
            return response;
        } catch (Exception exception) {
            // 支付参数包含用户 OpenID 与签名材料,日志仅保留 SDK 错误类型及平台消息。
            log.warn("Huifu mini-program payment request failed: type={}, message={}",
@@ -121,6 +135,46 @@
        return message.length() > 500 ? message.substring(0, 500) : message;
    }
    private String logJson(Object value) {
        try {
            return JSON.toJSONString(sanitizeForLog(value));
        } catch (Exception ignored) {
            return "{\"log_error\":\"Unable to serialize payment payload\"}";
        }
    }
    private Object sanitizeForLog(Object value) {
        if (value instanceof Map<?, ?> source) {
            Map<String, Object> result = new LinkedHashMap<>();
            for (Map.Entry<?, ?> entry : source.entrySet()) {
                String key = String.valueOf(entry.getKey());
                result.put(key, isSensitiveKey(key) ? mask(String.valueOf(entry.getValue())) : sanitizeForLog(entry.getValue()));
            }
            return result;
        }
        if (value instanceof List<?> source) return source.stream().map(this::sanitizeForLog).toList();
        if (value instanceof String text && (text.startsWith("{") || text.startsWith("["))) {
            try {
                return sanitizeForLog(JSON.parse(text));
            } catch (Exception ignored) {
                return text;
            }
        }
        return value;
    }
    private boolean isSensitiveKey(String key) {
        String normalized = key == null ? "" : key.toLowerCase();
        return normalized.contains("private") || normalized.contains("secret") || normalized.contains("sign")
                || normalized.contains("openid") || normalized.contains("authorization") || normalized.contains("certificate");
    }
    private String mask(String value) {
        if (value == null || value.isBlank()) return "";
        if (value.length() <= 8) return "***";
        return value.substring(0, 4) + "***" + value.substring(value.length() - 4);
    }
    private boolean isOutcomeUnknown(Exception exception) {
        String message = safeMessage(exception.getMessage()).toLowerCase();
        return message.contains("no response") || message.contains("timeout") || message.contains("timed out");
talent-card-server/src/main/resources/db/migration/V109__sync_premium_topic_merchant_mappings.sql
New file
@@ -0,0 +1,29 @@
-- Keep existing premium-benefit merchants created through the admin merchant screen visible
-- on the talent mini-program. New and edited merchants are synchronized in AdminBusinessService.
insert into premium_topic_merchant(merchant_id, category_key, business_type, sort_no, deleted)
select m.id,
       case m.merchant_type
           when '免费体检' then 'medical'
           when '配偶推荐就业' then 'employment'
           when '金融服务' then 'finance'
           when '行政审批' then 'approval'
           when '学生托付' then 'children'
           when '子女托管' then 'children'
       end,
       case m.merchant_type
           when '免费体检' then 'MEDICAL'
           when '配偶推荐就业' then 'SPOUSE_EMPLOYMENT'
           when '金融服务' then 'FINANCE'
           when '行政审批' then 'ADMIN_APPROVAL'
           when '学生托付' then 'CHILD_ENTRUSTMENT'
           when '子女托管' then 'CHILD_ENTRUSTMENT'
       end,
       m.sort_no,
       0
from merchant m
where m.deleted = 0
  and m.merchant_type in ('免费体检', '配偶推荐就业', '金融服务', '行政审批', '学生托付', '子女托管')
on duplicate key update category_key = values(category_key),
                        business_type = values(business_type),
                        sort_no = values(sort_no),
                        deleted = 0;