| | |
| | | "quickapp": {}, |
| | | /* 小程序特有相关 */ |
| | | "mp-weixin": { |
| | | "appid": "wx6ae2cadfea28fce1", |
| | | "appid": "wxcfee477acb54d179", |
| | | "setting": { |
| | | "urlCheck": false |
| | | }, |
| | |
| | | { |
| | | "name": "人才卡商户端", |
| | | "appid": "wx6ae2cadfea28fce1", |
| | | "appid": "wx550e520ad19458ea", |
| | | "description": "", |
| | | "versionName": "1.0.0", |
| | | "versionCode": "100", |
| | |
| | | "quickapp": {}, |
| | | /* 小程序特有相关 */ |
| | | "mp-weixin": { |
| | | "appid": "wxe91f1af7638aa5dd", |
| | | "appid": "wx550e520ad19458ea", |
| | | "setting": { |
| | | "urlCheck": false |
| | | }, |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | <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> |
| | |
| | | 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> |
| | |
| | | { |
| | | "appid": "wxfb87c6ef164c2922", |
| | | "appid": "wx550e520ad19458ea", |
| | | "projectname": "人才卡商户端", |
| | | "compileType": "miniprogram", |
| | | "miniprogramRoot": "", |
| | |
| | | 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 = [ |
| | | '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '电信服务', '口腔治疗保健', '加油服务', |
| | | ] |
| | |
| | | |
| | | 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 === '免费体检' |
| | |
| | | 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, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | 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, |
| | | }, |
| | | ] |
| | | } |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | parkingType: 'free', |
| | | parkingScope: 'all', |
| | | }, |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | parkingType: 'paid', |
| | | }, |
| | | { |
| | |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: true, |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: true, |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: true, |
| | | consumeEnabled: false, |
| | | parkingType: 'paid', |
| | | }, |
| | | ] |
| | |
| | | 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: '普通游客', |
| | |
| | | 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()) |
| | |
| | | </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> |
| | |
| | | </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> |
| | |
| | | </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"> |
| | |
| | | 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")))); |
| | |
| | | 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 |
| | |
| | | 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(""); |
| | |
| | | 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); |
| | |
| | | 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 |
| | |
| | | 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, |
| | |
| | | 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; |
| | | |
| | |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | |
| | | 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={}", |
| | |
| | | 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"); |
| New file |
| | |
| | | -- 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; |