wenminjie
2026-08-06 02c7bf03fa071325c201d29353330152d35ac71a
小程序对接
35个文件已修改
477 ■■■■■ 已修改文件
rencaika/pages/car-wash-merchant/car-wash-merchant.uvue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/medical-booking/medical-booking.uvue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/merchant/merchant.uvue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/transport-benefits/transport-benefits.uvue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/services/portal.uts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminWorkbenchStatisticsController.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/BenefitServiceCouponAdminController.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/CarWashBenefitAdminController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/DependentCardApplyAdminController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/CarWashCredentialPortalController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/CarWashMerchantPortalController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/CarWashMerchantPortalService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/FamilyCardPortalService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/ProductPriceCalculator.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/config/BenefitServiceCouponExceptionHandler.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/config/DependentCardApplyAdminExceptionHandler.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportCarWashCredentialMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportCarWashServiceMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportParkingCardLevelRightMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportParkingPlateRegistrationMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CarWashBenefitRuleRequest.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CarWashCredentialAuditRequest.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CarWashCredentialRollbackRequest.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CarWashCredentialSubmitRequest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CouponClaimIssueRequest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CouponClaimStatusRequest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CouponManagementSaveRequest.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/request/CouponManagementStatusRequest.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/AdminWorkbenchStatisticsService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/BenefitQuotaService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/CarWashBenefitManagementService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/resources/application.yml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/car-wash-merchant/car-wash-merchant.uvue
@@ -1,37 +1,56 @@
<template>
    <view class="page">
        <view class="top-area">
            <view class="custom-nav">
                <view class="safe-space" :style="'height:' + navTop + 'px'"></view>
                <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
                    <view class="back-button" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
                    <text class="nav-title">{{merchantName}}</text>
                    <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
                </view>
        <view class="page-header">
            <view class="safe-space" :style="'height:' + navTop + 'px'"></view>
            <view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
                <view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
                <text class="nav-title">{{merchantName}}</text>
                <view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
            </view>
        </view>
        <view v-if="merchantLoading" class="detail-state loading-state"><view class="loading-spinner"></view><text>正在加载洗车服务</text></view>
        <view v-else-if="merchantError" class="detail-state error-state"><text>洗车服务加载失败</text><text class="retry-text" @tap="loadMerchantDetail">重新加载</text></view>
        <view v-else class="detail-content">
        <view v-if="merchantLoading" class="state-block"><view class="loading-spinner"></view><text>正在加载洗车服务</text></view>
        <view v-else-if="merchantError" class="state-block error-state"><text>洗车服务加载失败</text><text class="retry-text" @tap="loadMerchantDetail">重新加载</text></view>
        <view v-else class="content">
            <view class="service-card">
                <view class="service-title-row"><view class="service-icon"><image src="/static/merchant-assets/car-wash-orange.svg" mode="aspectFit"></image></view><text class="service-title">{{merchantName}}</text></view>
                <view class="service-title-row">
                    <view class="service-icon"><image src="/static/merchant-assets/car-wash-orange.svg" mode="aspectFit"></image></view>
                    <text class="service-title">{{merchantName}}</text>
                </view>
                <view class="talent-tag"><text>{{applicableCards}}</text></view>
                <text class="service-desc">{{servicePointText}}</text>
                <text class="service-tip">{{tipText}}</text>
                <view v-if="!available" class="unavailable-note"><text>{{unavailableReason}}</text></view>
            </view>
            <text class="service-tip">{{tipText}}</text>
            <view v-if="!available" class="unavailable-note"><text>{{unavailableReason}}</text></view>
        </view>
        <view class="action-bar">
            <view class="wash-button" :class="!available || merchantLoading || merchantError ? 'is-disabled' : ''" @tap="openExternalService"><image src="/static/merchant-assets/car-wash-white.svg" mode="aspectFit"></image><text>去洗车</text></view>
            <view class="wash-button" :class="!available || merchantLoading || merchantError ? 'is-disabled' : ''" @tap="openExternalService">
                <image src="/static/merchant-assets/car-wash-white.svg" mode="aspectFit"></image>
                <text v-if="externalLinkType == 'miniProgram' || externalLinkType == 'web'">去洗车</text>
                <text v-else>提交手机号</text>
            </view>
        </view>
        <view v-if="showPhoneForm" class="phone-mask" @tap="closePhoneForm">
            <view class="phone-dialog" @tap.stop>
                <view class="phone-dialog-close" @tap="closePhoneForm"><text>×</text></view>
                <text class="phone-title">填写手机号</text>
                <text class="phone-desc">提交后商家将根据手机号与您联系</text>
                <view class="phone-input-box"><input v-model="phone" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="phone-placeholder" /></view>
                <text v-if="phoneError != ''" class="phone-error">{{phoneError}}</text>
                <view class="phone-submit" @tap="submitPhone"><text>提交</text></view>
            </view>
        </view>
    </view>
</template>
<script setup lang="uts">
    import { isApiConfigured } from '../../config/api.uts'
    import { getCarWashMerchantDetail } from '../../services/portal.uts'
    import { getCarWashMerchantDetail, getTalentCardProfile, submitCarWashCredential } from '../../services/portal.uts'
    import { getSession } from '../../services/session.uts'
    import { safeBack } from '../../utils/navigation.uts'
    const navTop = ref(28)
@@ -49,6 +68,12 @@
    const externalMiniProgramAppId = ref('')
    const externalMiniProgramPath = ref('')
    const externalWebUrl = ref('')
    const talentId = ref('')
    const cardId = ref('')
    const showPhoneForm = ref(false)
    const phone = ref('')
    const phoneSubmitting = ref(false)
    const phoneError = ref('')
    const merchantLoading = ref(true)
    const merchantError = ref(false)
@@ -63,6 +88,9 @@
        // #endif
        if (options != null && options['merchantId'] != null) merchantId.value = options['merchantId'] as string
        if (options != null && options['name'] != null && merchantName.value == '自助洗车服务') merchantName.value = options['name'] as string
        const session = getSession()
        if (session != null && session.talentId != null) talentId.value = session.talentId as string
        if (talentId.value != '') getTalentCardProfile(talentId.value, (data: any) => { const card = data != null ? data.card : null; if (card != null && card.id != null) cardId.value = '' + card.id }, () => {})
        loadMerchantDetail()
    })
@@ -79,8 +107,8 @@
        getCarWashMerchantDetail(merchantId.value, (data: any) => {
            if (data == null) { merchantLoading.value = false; merchantError.value = true; return }
            merchantName.value = data.merchantName != null && data.merchantName != '' ? data.merchantName as string : merchantName.value
            applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '请咨询商户'
            servicePointText.value = data.servicePointText != null ? data.servicePointText as string : '请咨询商户'
            applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '请咨询商家'
            servicePointText.value = data.servicePointText != null ? data.servicePointText as string : '请咨询商家'
            tipText.value = data.tipText != null ? data.tipText as string : '请按现场指引操作'
            available.value = data.available === true
            unavailableReason.value = data.unavailableReason != null && data.unavailableReason != '' ? data.unavailableReason as string : '洗车服务当前不可用'
@@ -90,6 +118,32 @@
            externalWebUrl.value = data.externalWebUrl != null ? data.externalWebUrl as string : ''
            merchantLoading.value = false
        }, () => { merchantLoading.value = false; merchantError.value = true })
    }
    const closePhoneForm = () => { showPhoneForm.value = false; phoneError.value = '' }
    const submitPhone = () => {
        if (phoneSubmitting.value) return
        const value = phone.value.trim()
        if (!/^1[3-9][0-9]{9}$/.test(value)) {
            phoneError.value = '请输入正确的 11 位手机号'
            return
        }
        if (merchantId.value == '' || talentId.value == '' || cardId.value == '') {
            phoneError.value = '请登录后提交手机号'
            return
        }
        phoneSubmitting.value = true
        phoneError.value = ''
        const key = 'car-wash-credential-' + merchantId.value + '-' + talentId.value + '-' + Date.now()
        submitCarWashCredential(merchantId.value, talentId.value, cardId.value, value, key, (data: any) => {
            phoneSubmitting.value = false
            showPhoneForm.value = false
            phone.value = ''
            uni.showToast({ title: '手机号已提交', icon: 'success' })
            setTimeout(() => safeBack('/pages/transport-benefits/transport-benefits'), 800)
        }, (error) => {
            phoneSubmitting.value = false
            phoneError.value = error.message
        })
    }
    const openExternalService = () => {
        if (merchantLoading.value || merchantError.value) return
@@ -107,41 +161,52 @@
            uni.navigateTo({ url: '/pages/external-webview/external-webview?source=carWash&merchantId=' + encodeURIComponent(merchantId.value) })
            return
        }
        uni.showToast({ title: '洗车服务链接暂未配置', icon: 'none' })
        showPhoneForm.value = true
    }
</script>
<style>
    .page { width: 100%; height: 100vh; position: relative; overflow: hidden; background-color: #F4F7FB; }
    .top-area { height: 317rpx; background-image: linear-gradient(180deg, #FFE6BD 0%, #FFF0D9 100%); }
    .custom-nav { position: relative; z-index: 5; }
    .page { display: flex; width: 100%; height: 100vh; background-color: #F4F7FB; flex-direction: column; }
    .page-header { position: relative; z-index: 10; flex-shrink: 0; padding-bottom: 20rpx; background-image: linear-gradient(180deg, #FFE6BD 0%, #FFF0D9 100%); }
    .safe-space { height: 28px; }
    .nav-row { display: flex; box-sizing: border-box; padding-left: 20rpx; flex-direction: row; align-items: center; justify-content: space-between; }
    .nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
    .back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
    .back-button image { width: 36rpx; height: 36rpx; }
    .nav-title { position: absolute; left: 156rpx; right: 156rpx; color: #171B22; font-size: 31rpx; line-height: 48rpx; font-weight: 700; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .capsule-space { height: 100%; flex-shrink: 0; }
    .detail-content { display: block; }
    .service-card { position: absolute; z-index: 6; left: 40rpx; right: 40rpx; top: 242rpx; min-height: 318rpx; box-sizing: border-box; padding: 31rpx 34rpx 27rpx; border-radius: 28rpx; background-color: #FFFFFF; box-shadow: 0 13rpx 30rpx rgba(63,74,89,.07); }
    .back-button-active { opacity: .55; }
    .nav-title { position: absolute; left: 50%; top: 50%; color: #171B22; font-size: 37rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
    .capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
    .state-block { display: flex; min-height: 360rpx; color: #8E98A6; font-size: 26rpx; flex-direction: column; justify-content: center; align-items: center; }
    .error-state { flex-direction: column; }
    .retry-text { margin-top: 22rpx; color: #F26A38; }
    .loading-spinner { width: 28rpx; height: 28rpx; margin-right: 14rpx; box-sizing: border-box; border-width: 3rpx; border-style: solid; border-color: #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: wash-loading 720ms linear infinite; }
    @keyframes wash-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
    .content { min-height: 0; box-sizing: border-box; padding: 30rpx 40rpx 30rpx; flex: 1; overflow-y: auto; }
    .service-card { box-sizing: border-box; padding: 31rpx 34rpx 27rpx; border-radius: 28rpx; background-color: #FFFFFF; box-shadow: 0 13rpx 30rpx rgba(63,74,89,.07); }
    .service-title-row { display: flex; flex-direction: row; align-items: center; }
    .service-icon { display: flex; width: 80rpx; height: 80rpx; border-radius: 50%; background-color: #FFF3E6; justify-content: center; align-items: center; flex-shrink: 0; }
    .service-icon image { width: 48rpx; height: 48rpx; }
    .service-title { min-width: 0; flex: 1; margin-left: 21rpx; color: #202631; font-size: 34rpx; line-height: 47rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .talent-tag { display: flex; max-width: 100%; width: fit-content; min-height: 47rpx; margin-top: 25rpx; box-sizing: border-box; padding: 8rpx 17rpx; border-radius: 24rpx; background-image: linear-gradient(90deg, #FFF0E5 0%, #FFE8D7 100%); justify-content: center; align-items: center; }
    .talent-tag text { max-width: 540rpx; color: #D86B35; font-size: 23rpx; line-height: 31rpx; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .service-desc { display: block; max-height: 78rpx; margin-top: 24rpx; overflow: hidden; color: #6F7887; font-size: 25rpx; line-height: 39rpx; }
    .service-tip { position: absolute; left: 80rpx; right: 80rpx; top: 650rpx; max-height: 68rpx; overflow: hidden; color: #9CA6B5; font-size: 22rpx; line-height: 34rpx; text-align: center; }
    .unavailable-note { position: absolute; left: 80rpx; right: 80rpx; top: 748rpx; text-align: center; }
    .service-desc { display: block; margin-top: 24rpx; color: #6F7887; font-size: 25rpx; line-height: 39rpx; }
    .service-tip { display: block; margin-top: 16rpx; color: #9CA6B5; font-size: 22rpx; line-height: 34rpx; }
    .unavailable-note { margin-top: 16rpx; text-align: center; }
    .unavailable-note text { color: #D86B35; font-size: 23rpx; line-height: 34rpx; }
    .detail-state { display: flex; position: absolute; z-index: 6; left: 40rpx; right: 40rpx; top: 242rpx; height: 318rpx; border-radius: 28rpx; background-color: #FFFFFF; box-shadow: 0 13rpx 30rpx rgba(63,74,89,.07); flex-direction: column; justify-content: center; align-items: center; }
    .detail-state text { color: #8E98A6; font-size: 26rpx; }
    .error-state .retry-text { margin-top: 22rpx; color: #F26A38; }
    .loading-state { flex-direction: row; }
    .loading-spinner { width: 28rpx; height: 28rpx; margin-right: 14rpx; box-sizing: border-box; border-width: 3rpx; border-style: solid; border-color: #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: wash-loading 720ms linear infinite; }
    .action-bar { display: flex; position: absolute; left: 0; right: 0; bottom: 0; height: 151rpx; height: calc(151rpx + env(safe-area-inset-bottom)); box-sizing: border-box; padding: 17rpx 40rpx 13rpx; padding-bottom: calc(13rpx + env(safe-area-inset-bottom)); background-color: #FFFFFF; box-shadow: 0 -5rpx 16rpx rgba(0,0,0,.04); align-items: flex-start; }
    .phone-mask { display: flex; position: fixed; z-index: 50; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0,0,0,.45); justify-content: center; align-items: center; }
    .phone-dialog { display: flex; width: 590rpx; box-sizing: border-box; padding: 44rpx 40rpx 36rpx; border-radius: 26rpx; background-color: #FFFFFF; position: relative; flex-direction: column; align-items: center; }
    .phone-dialog-close { display: flex; width: 60rpx; height: 60rpx; position: absolute; top: 12rpx; right: 12rpx; justify-content: center; align-items: center; }
    .phone-dialog-close text { color: #9AA3AE; font-size: 36rpx; line-height: 60rpx; }
    .phone-title { color: #262D36; font-size: 31rpx; font-weight: 700; }
    .phone-desc { margin-top: 12rpx; color: #8A94A1; font-size: 24rpx; }
    .phone-input-box { display: flex; width: 100%; height: 88rpx; margin-top: 28rpx; box-sizing: border-box; padding: 0 24rpx; border-radius: 16rpx; background-color: #F5F7FA; justify-content: center; align-items: center; }
    .phone-input-box input { width: 100%; color: #2D333C; font-size: 28rpx; }
    .phone-placeholder { color: #BBC1CA; }
    .phone-error { margin-top: 12rpx; color: #F05C3A; font-size: 22rpx; }
    .phone-submit { display: flex; width: 100%; height: 84rpx; margin-top: 28rpx; border-radius: 42rpx; background-image: linear-gradient(90deg, #FF4B32 0%, #FF8A1E 100%); justify-content: center; align-items: center; }
    .phone-submit text { color: #FFFFFF; font-size: 28rpx; font-weight: 600; }
    .action-bar { display: flex; flex-shrink: 0; box-sizing: border-box; padding: 17rpx 40rpx calc(17rpx + env(safe-area-inset-bottom)); background-color: #FFFFFF; box-shadow: 0 -5rpx 16rpx rgba(0,0,0,.04); }
    .wash-button { display: flex; width: 100%; height: 88rpx; border-radius: 45rpx; background-image: linear-gradient(90deg, #FF4B32 0%, #FF8A1E 100%); flex-direction: row; justify-content: center; align-items: center; }
    .wash-button.is-disabled { background-image: none; background-color: #C7CDD5; }
    .wash-button image { width: 36rpx; height: 36rpx; margin-right: 18rpx; }
    .wash-button text { color: #FFFFFF; font-size: 30rpx; font-weight: 600; }
    @keyframes wash-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
</style>
rencaika/pages/medical-booking/medical-booking.uvue
@@ -86,9 +86,9 @@
            uni.showToast({ title: '请输入姓名', icon: 'none' })
            return
        }
        if (!/^1[3-9][0-9]{9}$/.test(userMobile.value.trim())) {
            uni.showToast({ title: '请输入正确的 11 位手机号', icon: 'none' })
            return
        if (!/^1[2-9][0-9]{9}$/.test(userMobile.value.trim())) {
            uni.showToast({ title: '请输入正确的 11 位手机号', icon: 'none' })
            return
        }
        if (!/^[0-9]{17}[0-9Xx]$/.test(userIdCard.value.trim())) {
            uni.showToast({ title: '请输入正确的 18 位身份证号', icon: 'none' })
rencaika/pages/merchant/merchant.uvue
@@ -122,7 +122,7 @@
<script setup lang="uts">
    import { apiEnvironment, isApiConfigured } from '../../config/api.uts'
    import { getBenefitQuota, getFuelMerchantDetail, getMedicalPackages, getMerchantDetail, getParkingMerchantDetail, getProducts, submitFuelMobileRegistration } from '../../services/portal.uts'
    import { getFuelMerchantDetail, getMedicalPackages, getMerchantDetail, getParkingMerchantDetail, getProducts, submitFuelMobileRegistration } from '../../services/portal.uts'
    import { resolveApiUrl } from '../../services/http.uts'
    import { getSession } from '../../services/session.uts'
    import { safeBack } from '../../utils/navigation.uts'
@@ -289,7 +289,7 @@
                    merchantServiceLabel.value = '加油服务'
                    merchantRightsText.value = '持卓越人才卡到站加油并出示人才码,可享指定油品 9.4 折优惠,具体适用油品、优惠上限、可用时段和活动库存以加油站现场说明为准。'
                }
            } else if (routedMerchantType == 'hotel' || routedMerchantType == '酒店服务' || name.indexOf('酒店') >= 0) {
            } else if (routedMerchantType == 'hotel' || name.indexOf('酒店') >= 0) {
                isHotelMerchant.value = true
                merchantType.value = 'hotel'
                merchantName.value = name
@@ -477,13 +477,8 @@
            merchantRightsText.value = data != null && data.rightsText != null && data.rightsText != '' ? data.rightsText as string : merchantRightsText.value
            usageInstructions.value = data != null && data.usageInstructions != null && data.usageInstructions != '' ? data.usageInstructions as string : usageInstructions.value
            groupBuyingEnabled.value = data == null || data.groupBuyingEnabled == null ? groupBuyingEnabled.value : data.groupBuyingEnabled === true || Number(data.groupBuyingEnabled) == 1
            isHotelMerchant.value = merchantType.value == 'hotel' || merchantType.value == '酒店服务'
            isHotelMerchant.value = merchantType.value == 'hotel'
            hasFreeVoucher.value = merchantDiscount.value.indexOf('免费') >= 0
            if (isHotelMerchant.value && /^\d+$/.test(merchantId.value) && talentId.value != '') {
                getBenefitQuota('HOTEL', talentId.value, '', merchantId.value, (quota: any) => {
                    hasFreeVoucher.value = quota != null && Number(quota.quotaTotal) > 0
                }, () => {})
            }
            merchantLocation.value = {
                latitude: data != null && data.latitude != null ? Number(data.latitude) : 0,
                longitude: data != null && data.longitude != null ? Number(data.longitude) : 0,
rencaika/pages/transport-benefits/transport-benefits.uvue
@@ -109,7 +109,7 @@
    const loadMoreServices = () => { if (initialLoading.value || loadingMore.value || !hasMore.value) return; loadMoreError.value = false; loadingMore.value = true; loadServices(true) }
    const replaceHeroImage = () => { currentScene.value.image = fallbackImage }; const replaceSceneImage = (index: number) => { if (index >= 0 && index < scenes.value.length) scenes.value[index].image = fallbackImage }; const replaceServiceImage = (item: any) => { item.image = fallbackImage }
    const goBack = () => safeBack('/pages/benefits/benefits')
    const openMerchant = (item: any) => { if (!item.available) { uni.showToast({ title: '该服务已下架', icon: 'none' }); return }; if (item.merchantType == 'carWash') { uni.navigateTo({ url: '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(item.id) + '&name=' + encodeURIComponent(item.name) }); return }; const action = item.merchantType == 'parking' && item.discount.indexOf('人才码') >= 0 ? 'talentCode' : ''; uni.navigateTo({ url: '/pages/merchant/merchant?type=transport&merchantType=' + encodeURIComponent(item.merchantType) + '&merchantId=' + encodeURIComponent(item.id) + '&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) + '&action=' + encodeURIComponent(action) }) }
    const openMerchant = (item: any) => { if (!item.available) { uni.showToast({ title: '该服务已下架', icon: 'none' }); return }; if (item.merchantType == 'carWash' || item.merchantType == '自助洗车') { uni.navigateTo({ url: '/pages/car-wash-merchant/car-wash-merchant?merchantId=' + encodeURIComponent(item.id) + '&name=' + encodeURIComponent(item.name) }); return }; const action = item.merchantType == 'parking' && item.discount.indexOf('人才码') >= 0 ? 'talentCode' : ''; uni.navigateTo({ url: '/pages/merchant/merchant?type=transport&merchantType=' + encodeURIComponent(item.merchantType) + '&merchantId=' + encodeURIComponent(item.id) + '&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) + '&action=' + encodeURIComponent(action) }) }
</script>
<style>
rencaika/services/portal.uts
@@ -64,6 +64,10 @@
export const submitFuelMobileRegistration = (merchantId: string, talentId: string, benefitId: string, mobile: string, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFuelMerchantDetail') + encodeURIComponent(merchantId) + '/registrations', method: 'POST', skipAuth: true, data: { talentId: Number(talentId), benefitId: Number(benefitId), mobile: mobile }, idempotencyKey: idempotencyKey }, onSuccess, onError)
export const getParkingMerchantDetail = (merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appParkingMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true }, onSuccess, onError)
export const getCarWashMerchantDetail = (merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appCarWashMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true }, onSuccess, onError)
export const submitCarWashCredential = (merchantId: string, talentId: string, cardId: string, phone: string, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => {
    const data: any = { talentId: Number(talentId), cardId: Number(cardId), proofFiles: [], description: '手机号:' + phone }
    return request<any>({ path: getApiEndpoint('appCarWashMerchantDetail') + encodeURIComponent(merchantId) + '/credentials', method: 'POST', skipAuth: true, data: data, idempotencyKey: idempotencyKey }, onSuccess, onError)
}
export const getShoppingMerchantDetail = (merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appShoppingMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true }, onSuccess, onError)
export const searchParkingLots = (merchantId: string, keyword: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appParkingMerchantDetail') + encodeURIComponent(merchantId) + '/parking-lots', method: 'GET', skipAuth: true, data: { keyword: keyword, page: page, size: size } }, onSuccess, onError)
export const submitParkingPlateRegistration = (merchantId: string, talentId: string, benefitId: string, parkingLotId: string, plateNumber: string, idempotencyKey: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => {
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminWorkbenchStatisticsController.java
@@ -12,7 +12,7 @@
@RestController
@RequestMapping("/api/admin/workbench")
@Tag(name = "\u540e\u53f0\u5de5\u4f5c\u53f0", description = "\u540e\u53f0\u9996\u9875\u6838\u5fc3\u6307\u6807\u548c\u5f02\u5e38\u544a\u8b66")
@Tag(name = "后台工作台", description = "后台首页核心指标和异常告警")
public class AdminWorkbenchStatisticsController {
    private final AdminWorkbenchStatisticsService service;
@@ -21,7 +21,7 @@
    }
    @GetMapping({"/statistics", "/dashboard"})
    @Operation(summary = "\u67e5\u8be2\u5de5\u4f5c\u53f0\u7edf\u8ba1", description = "\u8fd4\u56de\u7d2f\u8ba1\u8865\u8d34\u603b\u989d\u3001\u672c\u6708\u6838\u9500\u5355\u6570\u3001\u6301\u5361\u4eba\u603b\u6570\u3001\u9884\u7b97\u4f7f\u7528\u7387\u548c\u5f02\u5e38\u544a\u8b66")
    @Operation(summary = "查询工作台统计", description = "返回累计补贴总额、本月核销单数、持卡人总数、预算使用率和异常告警")
    public ApiResponse<Map<String, Object>> statistics() {
        return ApiResponse.success(service.summary());
    }
talent-card-server/src/main/java/com/talentcard/server/business/admin/BenefitServiceCouponAdminController.java
@@ -26,7 +26,7 @@
@RestController
@RequestMapping("/api/admin/benefit-service-config/coupons")
@Tag(name = "\u540e\u53f0-\u6743\u76ca\u670d\u52a1\u914d\u7f6e-\u4f18\u60e0\u5238\u7ba1\u7406", description = "\u7ba1\u7406\u4f18\u60e0\u5238\u7c7b\u578b\u3001\u9762\u989d\u3001\u9002\u7528\u5546\u6237\u3001\u6709\u6548\u671f\u3001\u53d1\u653e\u6279\u6b21\u53ca\u9886\u53d6/\u4f7f\u7528\u72b6\u6001")
@Tag(name = "后台-权益服务配置-优惠券管理", description = "管理优惠券类型、面额、适用商户、有效期、发放批次及领取/使用状态")
public class BenefitServiceCouponAdminController {
    private final MerchantCouponManagementService service;
@@ -35,61 +35,61 @@
    }
    @GetMapping
    @Operation(summary = "\u5206\u9875\u67e5\u8be2\u4f18\u60e0\u5238\u914d\u7f6e")
    @Operation(summary = "分页查询优惠券配置")
    public ApiResponse<PageResult<Map<String, Object>>> page(
            @Parameter(description = "\u9002\u7528\u5546\u6237ID") @RequestParam(required = false) Long merchantId,
            @Parameter(description = "\u4f18\u60e0\u5238\u7c7b\u578b") @RequestParam(required = false) String couponType,
            @Parameter(description = "\u53d1\u653e\u6279\u6b21\u53f7") @RequestParam(required = false) String batchNo,
            @Parameter(description = "\u72b6\u6001\uff1a0\u4e0b\u67b6\u30011\u4e0a\u67b6") @RequestParam(required = false) Integer status,
            @Parameter(description = "\u4f18\u60e0\u5238\u6216\u5546\u6237\u540d\u79f0\u5173\u952e\u8bcd") @RequestParam(required = false) String keyword,
            @Parameter(description = "\u67e5\u8be2\u6307\u5b9a\u65e5\u671f\u6709\u6548\u7684\u4f18\u60e0\u5238") @RequestParam(required = false) LocalDate validOn,
            @Parameter(description = "适用商户ID") @RequestParam(required = false) Long merchantId,
            @Parameter(description = "优惠券类型") @RequestParam(required = false) String couponType,
            @Parameter(description = "发放批次号") @RequestParam(required = false) String batchNo,
            @Parameter(description = "状态:0下架、1上架") @RequestParam(required = false) Integer status,
            @Parameter(description = "优惠券或商户名称关键词") @RequestParam(required = false) String keyword,
            @Parameter(description = "查询指定日期有效的优惠券") @RequestParam(required = false) LocalDate validOn,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return ApiResponse.success(service.coupons(merchantId, couponType, batchNo, status, keyword, validOn, page, size));
    }
    @GetMapping("/{id}")
    @Operation(summary = "\u67e5\u8be2\u4f18\u60e0\u5238\u914d\u7f6e\u8be6\u60c5")
    @Operation(summary = "查询优惠券配置详情")
    public ApiResponse<Map<String, Object>> detail(@PathVariable Long id) {
        return ApiResponse.success(service.coupon(id));
    }
    @PostMapping
    @Operation(summary = "\u65b0\u589e\u4f18\u60e0\u5238\u914d\u7f6e")
    @Operation(summary = "新增优惠券配置")
    public ApiResponse<Map<String, Object>> create(@Valid @RequestBody CouponManagementSaveRequest request) {
        return ApiResponse.success(service.createCoupon(request));
    }
    @PutMapping("/{id}")
    @Operation(summary = "\u4fee\u6539\u4f18\u60e0\u5238\u914d\u7f6e")
    @Operation(summary = "修改优惠券配置")
    public ApiResponse<Map<String, Object>> update(@PathVariable Long id,
                                                   @Valid @RequestBody CouponManagementSaveRequest request) {
        return ApiResponse.success(service.updateCoupon(id, request));
    }
    @PatchMapping("/{id}/status")
    @Operation(summary = "\u4f18\u60e0\u5238\u4e0a\u67b6\u6216\u4e0b\u67b6")
    @Operation(summary = "优惠券上架或下架")
    public ApiResponse<Map<String, Object>> status(@PathVariable Long id,
                                                   @Valid @RequestBody CouponManagementStatusRequest request) {
        return ApiResponse.success(service.updateCouponStatus(id, request.status()));
    }
    @PostMapping("/{id}/claims")
    @Operation(summary = "\u540e\u53f0\u53d1\u653e\u4f18\u60e0\u5238", description = "\u6309\u53d1\u653e\u6279\u6b21\u548c\u53d1\u653e\u603b\u91cf\u4e3a\u6307\u5b9a\u6301\u5361\u4eba\u751f\u6210\u9886\u53d6\u8bb0\u5f55")
    @Operation(summary = "后台发放优惠券", description = "按发放批次和发放总量为指定持卡人生成领取记录")
    public ApiResponse<Map<String, Object>> issue(@PathVariable Long id,
                                                  @Valid @RequestBody CouponClaimIssueRequest request) {
        return ApiResponse.success(service.issue(id, request));
    }
    @GetMapping("/claims")
    @Operation(summary = "\u5206\u9875\u67e5\u8be2\u4f18\u60e0\u5238\u9886\u53d6\u548c\u4f7f\u7528\u8bb0\u5f55")
    @Operation(summary = "分页查询优惠券领取和使用记录")
    public ApiResponse<PageResult<Map<String, Object>>> claims(
            @RequestParam(required = false) Long couponId,
            @RequestParam(required = false) Long merchantId,
            @RequestParam(required = false) String batchNo,
            @Parameter(description = "\u9886\u53d6\u72b6\u6001\uff1a0\u5f85\u9886\u53d6\u30011\u5df2\u9886\u53d6\u30012\u5df2\u53d6\u6d88") @RequestParam(required = false) Integer claimStatus,
            @Parameter(description = "\u4f7f\u7528\u72b6\u6001\uff1a0\u672a\u4f7f\u7528\u30011\u5df2\u4f7f\u7528\u30012\u5df2\u8fc7\u671f\u30013\u5df2\u4f5c\u5e9f") @RequestParam(required = false) Integer useStatus,
            @Parameter(description = "\u9886\u53d6\u7f16\u53f7/\u6301\u5361\u4eba/\u624b\u673a\u53f7/\u5361\u53f7\u5173\u952e\u8bcd") @RequestParam(required = false) String keyword,
            @Parameter(description = "领取状态:0待领取、1已领取、2已取消") @RequestParam(required = false) Integer claimStatus,
            @Parameter(description = "使用状态:0未使用、1已使用、2已过期、3已作废") @RequestParam(required = false) Integer useStatus,
            @Parameter(description = "领取编号/持卡人/手机号/卡号关键词") @RequestParam(required = false) String keyword,
            @RequestParam(required = false) LocalDate startDate,
            @RequestParam(required = false) LocalDate endDate,
            @RequestParam(defaultValue = "1") int page,
@@ -99,13 +99,13 @@
    }
    @GetMapping("/claims/{claimId}")
    @Operation(summary = "\u67e5\u8be2\u4f18\u60e0\u5238\u9886\u53d6\u8bb0\u5f55\u8be6\u60c5")
    @Operation(summary = "查询优惠券领取记录详情")
    public ApiResponse<Map<String, Object>> claim(@PathVariable Long claimId) {
        return ApiResponse.success(service.claim(claimId));
    }
    @PatchMapping("/claims/{claimId}/status")
    @Operation(summary = "\u7ef4\u62a4\u4f18\u60e0\u5238\u9886\u53d6\u548c\u4f7f\u7528\u72b6\u6001")
    @Operation(summary = "维护优惠券领取和使用状态")
    public ApiResponse<Map<String, Object>> claimStatus(@PathVariable Long claimId,
                                                        @Valid @RequestBody CouponClaimStatusRequest request) {
        return ApiResponse.success(service.updateClaimStatus(claimId, request));
talent-card-server/src/main/java/com/talentcard/server/business/admin/CarWashBenefitAdminController.java
@@ -18,25 +18,25 @@
@RestController
@RequestMapping("/api/admin/car-wash-benefits")
@Tag(name = "\u540e\u53f0-\u6d17\u8f66\u6743\u76ca\u7ba1\u7406", description = "\u6d17\u8f66\u670d\u52a1\u914d\u7f6e\u3001\u51ed\u8bc1\u4eba\u5de5\u5ba1\u6838\u3001\u6743\u76ca\u6b21\u6570\u6263\u51cf\u4e0e\u56de\u6eda")
@Tag(name = "后台-洗车权益管理", description = "洗车服务配置、凭证人工审核、权益次数扣减与回滚")
public class CarWashBenefitAdminController {
    private final CarWashBenefitManagementService service;
    public CarWashBenefitAdminController(CarWashBenefitManagementService service) { this.service = service; }
    @GetMapping("/merchants/{merchantId}/rules")
    @Operation(summary = "\u67e5\u8be2\u6d17\u8f66\u6743\u76ca\u6b21\u6570\u914d\u7f6e")
    @Operation(summary = "查询洗车权益次数配置")
    public ApiResponse<Map<String, Object>> rules(@PathVariable Long merchantId) { return ApiResponse.success(service.rules(merchantId)); }
    @PutMapping("/merchants/{merchantId}/rules")
    @Operation(summary = "\u4fdd\u5b58\u6d17\u8f66\u6743\u76ca\u6b21\u6570\u914d\u7f6e", description = "\u6309\u4eba\u624d\u5361\u7b49\u7ea7\u914d\u7f6e\u603b\u6b21\u6570\u3001\u91cd\u7f6e\u5468\u671f\u548c\u4eb2\u5c5e\u9644\u5c5e\u5361\u5171\u4eab")
    @Operation(summary = "保存洗车权益次数配置", description = "按人才卡等级配置总次数、重置周期和亲属附属卡共享")
    public ApiResponse<Map<String, Object>> saveRules(@PathVariable Long merchantId, @Valid @RequestBody CarWashBenefitRuleRequest request) { return ApiResponse.success(service.saveRules(merchantId, request)); }
    @GetMapping("/credentials")
    @Operation(summary = "\u67e5\u8be2\u6d17\u8f66\u51ed\u8bc1\u5217\u8868")
    @Operation(summary = "查询洗车凭证列表")
    public ApiResponse<PageResult<Map<String, Object>>> credentials(
            @RequestParam(required = false) Long merchantId,
            @Parameter(description = "\u72b6\u6001\uff1a0\u5f85\u5ba1\u6838\u30011\u5df2\u901a\u8fc7\u30012\u5df2\u9a73\u56de\u30013\u5df2\u56de\u6eda") @RequestParam(required = false) Integer status,
            @Parameter(description = "\u51ed\u8bc1\u7f16\u53f7/\u59d3\u540d/\u624b\u673a\u53f7/\u5361\u53f7") @RequestParam(required = false) String keyword,
            @Parameter(description = "状态:0待审核、1已通过、2已驳回、3已回滚") @RequestParam(required = false) Integer status,
            @Parameter(description = "凭证编号/姓名/手机号/卡号") @RequestParam(required = false) String keyword,
            @RequestParam(required = false) LocalDate startDate,
            @RequestParam(required = false) LocalDate endDate,
            @RequestParam(defaultValue = "1") int page,
@@ -45,11 +45,11 @@
    }
    @GetMapping("/credentials/{id}")
    @Operation(summary = "\u67e5\u8be2\u6d17\u8f66\u51ed\u8bc1\u8be6\u60c5")
    @Operation(summary = "查询洗车凭证详情")
    public ApiResponse<Map<String, Object>> detail(@PathVariable Long id) { return ApiResponse.success(service.detail(id)); }
    @PatchMapping("/credentials/{id}/audit")
    @Operation(summary = "\u4eba\u5de5\u5ba1\u6838\u6d17\u8f66\u51ed\u8bc1", description = "\u901a\u8fc7\u65f6\u6821\u9a8c\u6743\u76ca\u5269\u4f59\u6b21\u6570\u5e76\u751f\u6210\u6838\u9500\u6263\u51cf\u6d41\u6c34")
    @Operation(summary = "人工审核洗车凭证", description = "通过时校验权益剩余次数并生成核销扣减流水")
    public ApiResponse<Map<String, Object>> audit(@PathVariable Long id,
            @Valid @RequestBody CarWashCredentialAuditRequest request,
            @RequestAttribute(AdminAuthInterceptor.OPERATOR_ID_ATTRIBUTE) Long operatorId) {
@@ -57,7 +57,7 @@
    }
    @PostMapping("/credentials/{id}/rollback")
    @Operation(summary = "\u56de\u6eda\u6d17\u8f66\u6743\u76ca\u6263\u51cf", description = "\u64a4\u9500\u5bf9\u5e94\u6838\u9500\u6d41\u6c34\uff0c\u6743\u76ca\u5269\u4f59\u6b21\u6570\u81ea\u52a8\u6062\u590d")
    @Operation(summary = "回滚洗车权益扣减", description = "撤销对应核销流水,权益剩余次数自动恢复")
    public ApiResponse<Map<String, Object>> rollback(@PathVariable Long id,
            @Valid @RequestBody CarWashCredentialRollbackRequest request,
            @RequestAttribute(AdminAuthInterceptor.OPERATOR_ID_ATTRIBUTE) Long operatorId) {
talent-card-server/src/main/java/com/talentcard/server/business/admin/DependentCardApplyAdminController.java
@@ -23,7 +23,7 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "\u7ba1\u7406\u540e\u53f0-\u5bb6\u5c5e\u9644\u5c5e\u5361\u5ba1\u6838", description = "\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7\u67e5\u8be2\u3001\u5ba1\u6838\u548c\u72b6\u6001\u8bb0\u5f55")
@Tag(name = "管理后台-家属附属卡审核", description = "家属附属卡申请查询、审核和状态记录")
@RestController
@RequestMapping("/api/admin/dependent-card-applies")
public class DependentCardApplyAdminController {
@@ -33,27 +33,27 @@
        this.service = service;
    }
    @Operation(summary = "\u5206\u9875\u67e5\u8be2\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7")
    @Operation(summary = "分页查询家属附属卡申请")
    @GetMapping
    public ApiResponse<PageResult<DependentCardApplyAdminResponse>> page(
            @Parameter(description = "\u4e3b\u5361ID") @RequestParam(required = false) Long talentCardId,
            @Parameter(description = "\u72b6\u6001\uff1a0\u5f85\u5ba1\u6838\u30011\u5df2\u901a\u8fc7\u30012\u5df2\u9a73\u56de\u30013\u5df2\u89e3\u7ed1") @RequestParam(required = false) Integer status,
            @Parameter(description = "\u9875\u7801") @RequestParam(defaultValue = "1") int page,
            @Parameter(description = "\u6bcf\u9875\u6570\u91cf\uff0c\u6700\u5927100") @RequestParam(defaultValue = "10") int size
            @Parameter(description = "主卡ID") @RequestParam(required = false) Long talentCardId,
            @Parameter(description = "状态:0待审核、1已通过、2已驳回、3已解绑") @RequestParam(required = false) Integer status,
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
            @Parameter(description = "每页数量,最大100") @RequestParam(defaultValue = "10") int size
    ) {
        return ApiResponse.success(service.page(talentCardId, status, page, size));
    }
    @Operation(summary = "\u67e5\u8be2\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7\u8be6\u60c5")
    @Operation(summary = "查询家属附属卡申请详情")
    @GetMapping("/{id}")
    public ApiResponse<DependentCardApplyAdminResponse> detail(@PathVariable Long id) {
        return ApiResponse.success(service.detail(id));
    }
    @Operation(summary = "\u5ba1\u6838\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7", description = "\u4ec5\u5f85\u5ba1\u6838\u7533\u8bf7\u53ef\u4ee5\u53d8\u66f4\u4e3a\u901a\u8fc7\u6216\u9a73\u56de\uff1b\u901a\u8fc7\u65f6\u6821\u9a8c\u4e3b\u5361\u6743\u9650\u548c\u5bb6\u5c5e\u5361\u540d\u989d")
    @Operation(summary = "审核家属附属卡申请", description = "仅待审核申请可以变更为通过或驳回;通过时校验主卡权限和家属卡名额")
    @ApiResponses({
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "\u5ba1\u6838\u6210\u529f"),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "\u5ba1\u6838\u5931\u8d25\u3001\u7533\u8bf7\u4e0d\u5b58\u5728\u3001\u72b6\u6001\u51b2\u7a81\u6216\u7ed1\u5b9a\u540d\u989d\u5df2\u6ee1")
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "审核成功"),
            @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "审核失败、申请不存在、状态冲突或绑定名额已满")
    })
    @PatchMapping("/{id}/audit")
    public ApiResponse<DependentCardApplyAdminResponse> audit(
@@ -64,7 +64,7 @@
        return ApiResponse.success(service.audit(id, request, operatorId));
    }
    @Operation(summary = "\u67e5\u8be2\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7\u72b6\u6001\u8bb0\u5f55")
    @Operation(summary = "查询家属附属卡申请状态记录")
    @GetMapping("/{id}/status-logs")
    public ApiResponse<List<DependentCardApplyStatusLogResponse>> statusLogs(@PathVariable Long id) {
        return ApiResponse.success(service.statusLogs(id));
talent-card-server/src/main/java/com/talentcard/server/business/portal/CarWashCredentialPortalController.java
@@ -13,20 +13,20 @@
@RestController
@RequestMapping("/api/app/car-wash-merchants")
@Tag(name = "\u5c0f\u7a0b\u5e8f-\u6d17\u8f66\u51ed\u8bc1", description = "\u7528\u6237\u63d0\u4ea4\u6d17\u8f66\u51ed\u8bc1\u6216\u6d88\u8d39\u622a\u56fe")
@Tag(name = "小程序-洗车凭证", description = "用户提交洗车凭证或消费截图")
public class CarWashCredentialPortalController {
    private final CarWashBenefitManagementService service;
    public CarWashCredentialPortalController(CarWashBenefitManagementService service) { this.service = service; }
    @PostMapping("/{merchantId}/credentials")
    @Operation(summary = "\u63d0\u4ea4\u6d17\u8f66\u51ed\u8bc1", description = "\u5148\u8c03\u7528\u5c0f\u7a0b\u5e8f\u56fe\u7247\u4e0a\u4f20\u63a5\u53e3\uff0c\u518d\u5c06\u8fd4\u56de\u7684URL\u5217\u8868\u4f5c\u4e3a\u51ed\u8bc1\u63d0\u4ea4")
    @Operation(summary = "提交洗车凭证", description = "先调用小程序图片上传接口,再将返回的URL列表作为凭证提交")
    public ApiResponse<Map<String, Object>> submit(@PathVariable Long merchantId,
            @Valid @RequestBody CarWashCredentialSubmitRequest request,
            @Parameter(description = "\u5e42\u7b49\u63d0\u4ea4\u51ed\u8bc1") @RequestHeader(value = "X-Idempotency-Key", required = false) String idempotencyKey) {
            @Parameter(description = "幂等提交凭证") @RequestHeader(value = "X-Idempotency-Key", required = false) String idempotencyKey) {
        return ApiResponse.success(service.submit(merchantId, request, idempotencyKey));
    }
    @GetMapping("/credentials/{id}")
    @Operation(summary = "\u67e5\u8be2\u6d17\u8f66\u51ed\u8bc1\u5ba1\u6838\u7ed3\u679c")
    @Operation(summary = "查询洗车凭证审核结果")
    public ApiResponse<Map<String, Object>> detail(@PathVariable Long id) { return ApiResponse.success(service.detail(id)); }
}
talent-card-server/src/main/java/com/talentcard/server/business/portal/CarWashMerchantPortalController.java
@@ -21,4 +21,4 @@
    public ApiResponse<Map<String, Object>> detail(@PathVariable Long merchantId) {
        return ApiResponse.success(service.detail(merchantId));
    }
}
}
talent-card-server/src/main/java/com/talentcard/server/business/portal/CarWashMerchantPortalService.java
@@ -39,7 +39,7 @@
        Map<String, Object> merchant = one("""
                select id, merchant_name, contact_mobile, address, cover_url, service_label, status
                from merchant
                where id = ? and merchant_type = 'carWash' and deleted = 0
                where id = ? and merchant_type in ('carWash', '自助洗车') and deleted = 0
                """, merchantId);
        Map<String, Object> service = one("""
                select id, talent_levels, service_point_text, tip_text, external_link_type,
talent-card-server/src/main/java/com/talentcard/server/business/portal/FamilyCardPortalService.java
@@ -40,7 +40,7 @@
        return jdbc.queryForList("""
            select f.id, f.family_name, f.family_mobile, f.relationship, f.status
            from family_card_application f
            where f.deleted = 0 and f.talent_id = ? and f.status <> '\u5df2\u89e3\u7ed1'
            where f.deleted = 0 and f.talent_id = ? and f.status <> '已解绑'
            order by f.apply_time desc, f.id desc
            """, talentId).stream().map(DatabaseRows::normalize).map(this::dependentCardItem).toList();
    }
@@ -64,7 +64,7 @@
            where f.id = ? and f.talent_id = ? and f.deleted = 0
            limit 1
            """, id, talentId).stream().map(DatabaseRows::normalize).toList();
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "\u5bb6\u5c5e\u5361\u7533\u8bf7\u4e0d\u5b58\u5728");
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "家属卡申请不存在");
        return dependentCardItem(rows.get(0));
    }
    @Transactional
@@ -73,10 +73,10 @@
        if (!"卓越人才卡".equals(string(owner.get("cardLevel")))) throw new BusinessException(ErrorCode.CONFLICT, "家庭附属卡仅向卓越人才卡用户开放");
        if (!List.of("配偶", "父母", "子女", "其他").contains(request.relationship().trim())) throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的家属关系");
        if (!validIdCard(request.idCard().trim())) throw new BusinessException(ErrorCode.BAD_REQUEST, "身份证号格式不正确");
        Integer submitted = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('\u5f85\u5ba1\u6838', '\u5df2\u901a\u8fc7', '\u5df2\u9a73\u56de')", Integer.class, request.talentId());
        if (submitted != null && submitted >= 3) throw new BusinessException(ErrorCode.CONFLICT, "\u540c\u4e00\u5f20\u4e3b\u5361\u6700\u591a\u63d0\u4ea43\u4e2a\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7");
        Integer occupied = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('\u5f85\u5ba1\u6838', '\u5df2\u901a\u8fc7')", Integer.class, request.talentId());
        if (occupied != null && occupied >= 2) throw new BusinessException(ErrorCode.CONFLICT, "\u8be5\u4e3b\u5361\u5bb6\u5c5e\u9644\u5c5e\u5361\u540d\u989d\u5df2\u6ee1\uff0c\u6700\u591a\u7ed1\u5b9a2\u4e2a");
        Integer submitted = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('待审核', '已通过', '已驳回')", Integer.class, request.talentId());
        if (submitted != null && submitted >= 3) throw new BusinessException(ErrorCode.CONFLICT, "同一张主卡最多提交3个家属附属卡申请");
        Integer occupied = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('待审核', '已通过')", Integer.class, request.talentId());
        if (occupied != null && occupied >= 2) throw new BusinessException(ErrorCode.CONFLICT, "该主卡家属附属卡名额已满,最多绑定2个");
        Integer duplicate = jdbc.queryForObject("""
            select count(*) from family_card_application
            where deleted = 0 and talent_id = ? and id_card_no = ? and status in ('待审核', '已通过')
@@ -127,30 +127,30 @@
    }
    private Long talentIdByCardId(Long talentCardId) {
        if (talentCardId == null || talentCardId <= 0) throw new BusinessException(ErrorCode.NOT_FOUND, "\u4e3b\u5361\u4e0d\u5b58\u5728\u6216\u5df2\u5931\u6548");
        if (talentCardId == null || talentCardId <= 0) throw new BusinessException(ErrorCode.NOT_FOUND, "主卡不存在或已失效");
        List<Map<String, Object>> rows = jdbc.queryForList("""
            select talent_id from talent_card
            where id = ? and deleted = 0 and card_type = 'PERSONAL' and status = 1
            limit 1
            """, talentCardId).stream().map(DatabaseRows::normalize).toList();
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "\u4e3b\u5361\u4e0d\u5b58\u5728\u6216\u5df2\u5931\u6548");
        if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "主卡不存在或已失效");
        return number(rows.get(0).get("talentId"));
    }
    private DependentCardApplyItemResponse dependentCardItem(Map<String, Object> row) {
        Integer status = switch (string(row.get("status"))) {
            case "\u5f85\u5ba1\u6838" -> 0;
            case "\u5df2\u901a\u8fc7" -> 1;
            case "\u5df2\u9a73\u56de" -> 2;
            case "\u5df2\u89e3\u7ed1" -> 3;
            case "待审核" -> 0;
            case "已通过" -> 1;
            case "已驳回" -> 2;
            case "已解绑" -> 3;
            default -> -1;
        };
        String statusText = switch (status) {
            case 0 -> "\u5f85\u5ba1\u6838";
            case 1 -> "\u5df2\u901a\u8fc7";
            case 2 -> "\u5df2\u9a73\u56de";
            case 3 -> "\u5df2\u89e3\u7ed1";
            default -> "\u672a\u77e5";
            case 0 -> "待审核";
            case 1 -> "已通过";
            case 2 -> "已驳回";
            case 3 -> "已解绑";
            default -> "未知";
        };
        return new DependentCardApplyItemResponse(
                number(row.get("id")), string(row.get("familyName")),
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java
@@ -134,7 +134,7 @@
        return one("""
                select id, merchant_name, contact_mobile, address, cover_url, service_label, status, latitude, longitude
                from merchant
                where id = ? and merchant_type = 'parking' and deleted = 0
                where id = ? and merchant_type in ('parking', '停车服务') and deleted = 0
                """, merchantId);
    }
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -386,6 +386,8 @@
        String merchantWhere = " where deleted = 0 and merchant_type = ?";
        if (type.equals("parking")) {
            type = "停车服务";
        }if (type.equals("carWash")) {
            type = "自助洗车";
        }
        PageResult<Map<String, Object>> result = page("""
                select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address,
talent-card-server/src/main/java/com/talentcard/server/business/portal/ProductPriceCalculator.java
@@ -84,9 +84,9 @@
        if (value == null) return "";
        String raw = String.valueOf(value).trim();
        String upper = raw.toUpperCase(Locale.ROOT);
        if (upper.contains("EXCELLENT") || raw.contains("\u5353\u8d8a")) return "EXCELLENT";
        if (upper.contains("OUTSTANDING") || raw.contains("\u6770\u51fa")) return "OUTSTANDING";
        if (upper.contains("ELITE") || raw.contains("\u83c1\u82f1") || raw.contains("\u7cbe\u82f1")) return "ELITE";
        if (upper.contains("EXCELLENT") || raw.contains("卓越")) return "EXCELLENT";
        if (upper.contains("OUTSTANDING") || raw.contains("杰出")) return "OUTSTANDING";
        if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) return "ELITE";
        return upper;
    }
talent-card-server/src/main/java/com/talentcard/server/config/BenefitServiceCouponExceptionHandler.java
@@ -20,11 +20,11 @@
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> exception(Exception exception) {
        return error("\u4f18\u60e0\u5238\u914d\u7f6e\u5904\u7406\u5931\u8d25");
        return error("优惠券配置处理失败");
    }
    private ResponseEntity<ApiResponse<Void>> error(String message) {
        String fallback = "\u4f18\u60e0\u5238\u914d\u7f6e\u5904\u7406\u5931\u8d25";
        String fallback = "优惠券配置处理失败";
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.failure(500, message == null || message.isBlank() ? fallback : message));
    }
talent-card-server/src/main/java/com/talentcard/server/config/DependentCardApplyAdminExceptionHandler.java
@@ -25,16 +25,16 @@
    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class,
            ConstraintViolationException.class, HttpMessageNotReadableException.class})
    public ResponseEntity<ApiResponse<Void>> handleRequestException(Exception exception) {
        return error("\u5ba1\u6838\u53c2\u6570\u4e0d\u6b63\u786e");
        return error("审核参数不正确");
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception exception) {
        return error("\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7\u5904\u7406\u5931\u8d25");
        return error("家属附属卡申请处理失败");
    }
    private ResponseEntity<ApiResponse<Void>> error(String message) {
        String fallback = "\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7\u5904\u7406\u5931\u8d25";
        String fallback = "家属附属卡申请处理失败";
        String safeMessage = message == null || message.isBlank() ? fallback : message;
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ApiResponse.failure(500, safeMessage));
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportCarWashCredentialMapper.java
@@ -81,9 +81,9 @@
                   u.name as talentName, u.mobile as talentMobile
            from talent_card c join talent_user u on u.id = c.talent_id and u.deleted = 0
            where c.id = #{cardId} and c.talent_id = #{talentId} and c.deleted = 0 and c.status = 1
              and (c.valid_start is null or c.valid_start &lt;= current_date)
              and (c.valid_end is null or c.valid_end &gt;= current_date)
              and (c.expire_time is null or c.expire_time &gt; now())
              and (c.valid_start is null or c.valid_start <= current_date)
              and (c.valid_end is null or c.valid_end >= current_date)
              and (c.expire_time is null or c.expire_time > now())
            limit 1
            """)
    Map<String, Object> selectActiveCard(@Param("talentId") Long talentId, @Param("cardId") Long cardId);
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportCarWashServiceMapper.java
@@ -29,7 +29,7 @@
            from merchant m
            left join transport_car_wash_service s on s.merchant_id = m.id and s.deleted = 0
            where m.deleted = 0
              and m.merchant_type = 'carWash'
              and m.merchant_type = '自助洗车'
              and (#{keyword} is null or #{keyword} = '' or m.merchant_name like concat('%', #{keyword}, '%')
                   or m.address like concat('%', #{keyword}, '%') or m.contact_mobile like concat('%', #{keyword}, '%'))
              and (#{merchantId} is null or m.id = #{merchantId})
@@ -45,7 +45,7 @@
            select count(*)
            from merchant m
            where m.deleted = 0
              and m.merchant_type = 'carWash'
              and m.merchant_type = '自助洗车'
              and (#{keyword} is null or #{keyword} = '' or m.merchant_name like concat('%', #{keyword}, '%')
                   or m.address like concat('%', #{keyword}, '%') or m.contact_mobile like concat('%', #{keyword}, '%'))
              and (#{merchantId} is null or m.id = #{merchantId})
@@ -69,7 +69,7 @@
            from merchant m
            left join transport_car_wash_service s on s.merchant_id = m.id and s.deleted = 0
            where m.deleted = 0
              and m.merchant_type = 'carWash'
              and m.merchant_type = '自助洗车'
              and m.id = #{merchantId}
            limit 1
            """)
@@ -90,7 +90,7 @@
            from merchant m
            left join transport_car_wash_service s on s.merchant_id = m.id and s.deleted = 0
            where m.deleted = 0
              and m.merchant_type = 'carWash'
              and m.merchant_type = '自助洗车'
              and m.id = #{merchantId}
            limit 1
            """)
@@ -99,7 +99,7 @@
    @Select("""
            select count(*)
            from merchant
            where deleted = 0 and merchant_type = 'carWash' and id = #{merchantId}
            where deleted = 0 and merchant_type = '自助洗车' and id = #{merchantId}
            """)
    Long countCarWashMerchant(@Param("merchantId") Long merchantId);
}
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportParkingCardLevelRightMapper.java
@@ -36,7 +36,7 @@
                   b.action_mode as actionMode,
                   b.status as benefitStatus
            from transport_parking_card_level_right r
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type = 'parking'
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type in ('parking', '停车服务')
            left join transport_parking_benefit b on b.merchant_id = m.id and b.deleted = 0
            where r.deleted = 0
              and (#{merchantId} is null or r.merchant_id = #{merchantId})
@@ -57,7 +57,7 @@
    @Select("""
            select count(*)
            from transport_parking_card_level_right r
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type = 'parking'
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type in ('parking', '停车服务')
            where r.deleted = 0
              and (#{merchantId} is null or r.merchant_id = #{merchantId})
              and (#{cardLevel} is null or #{cardLevel} = '' or r.card_level = #{cardLevel})
@@ -93,7 +93,7 @@
                   b.action_mode as actionMode,
                   b.status as benefitStatus
            from transport_parking_card_level_right r
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type = 'parking'
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type in ('parking', '停车服务')
            left join transport_parking_benefit b on b.merchant_id = m.id and b.deleted = 0
            where r.deleted = 0
              and r.merchant_id = #{merchantId}
@@ -125,7 +125,7 @@
                   b.action_mode as actionMode,
                   b.status as benefitStatus
            from transport_parking_card_level_right r
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type = 'parking' and m.status = 1
            join merchant m on m.id = r.merchant_id and m.deleted = 0 and m.merchant_type in ('parking', '停车服务') and m.status = 1
            join transport_parking_benefit b on b.merchant_id = m.id and b.deleted = 0 and b.status = 1
            where r.deleted = 0
              and r.enabled = 1
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportParkingPlateRegistrationMapper.java
@@ -50,7 +50,7 @@
            left join transport_parking_benefit b on b.merchant_id = m.id and b.deleted = 0
            where m.id = #{merchantId}
              and m.deleted = 0
              and m.merchant_type = 'parking'
              and m.merchant_type in ('parking', '停车服务')
            limit 1
            """)
    Map<String, Object> selectParkingMerchant(@Param("merchantId") Long merchantId);
talent-card-server/src/main/java/com/talentcard/server/request/CarWashBenefitRuleRequest.java
@@ -9,14 +9,14 @@
import java.util.List;
@Schema(name = "CarWashBenefitRuleRequest", description = "\u6d17\u8f66\u6743\u76ca\u6b21\u6570\u914d\u7f6e")
@Schema(name = "CarWashBenefitRuleRequest", description = "洗车权益次数配置")
public record CarWashBenefitRuleRequest(
        @NotEmpty List<@Valid Rule> rules
) {
    public record Rule(
            @NotBlank String cardLevel,
            @NotNull @Min(1) Integer totalCount,
            @NotBlank @Schema(description = "\u91cd\u7f6e\u5468\u671f\uff1aDAY\u3001WEEK\u3001MONTH\u3001YEAR\u3001NONE") String resetCycle,
            @NotBlank @Schema(description = "重置周期:DAY、WEEK、MONTH、YEAR、NONE") String resetCycle,
            Boolean familyCardShared
    ) {}
}
talent-card-server/src/main/java/com/talentcard/server/request/CarWashCredentialAuditRequest.java
@@ -4,9 +4,9 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Schema(name = "CarWashCredentialAuditRequest", description = "\u6d17\u8f66\u51ed\u8bc1\u4eba\u5de5\u5ba1\u6838\u53c2\u6570")
@Schema(name = "CarWashCredentialAuditRequest", description = "洗车凭证人工审核参数")
public record CarWashCredentialAuditRequest(
        @NotNull @Schema(description = "\u5ba1\u6838\u72b6\u6001\uff1a1\u901a\u8fc7\u30012\u9a73\u56de") Integer status,
        @Schema(description = "\u6263\u51cf\u6b21\u6570\uff0c\u901a\u8fc7\u65f6\u9ed8\u8ba41") Integer quantity,
        @NotNull @Schema(description = "审核状态:1通过、2驳回") Integer status,
        @Schema(description = "扣减次数,通过时默认1") Integer quantity,
        @Size(max = 500) String auditRemark
) {}
talent-card-server/src/main/java/com/talentcard/server/request/CarWashCredentialRollbackRequest.java
@@ -4,7 +4,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Schema(name = "CarWashCredentialRollbackRequest", description = "\u6d17\u8f66\u6743\u76ca\u6263\u51cf\u56de\u6eda\u53c2\u6570")
@Schema(name = "CarWashCredentialRollbackRequest", description = "洗车权益扣减回滚参数")
public record CarWashCredentialRollbackRequest(
        @NotBlank @Size(max = 500) @Schema(description = "\u56de\u6eda\u539f\u56e0") String reason
        @NotBlank @Size(max = 500) @Schema(description = "回滚原因") String reason
) {}
talent-card-server/src/main/java/com/talentcard/server/request/CarWashCredentialSubmitRequest.java
@@ -1,19 +1,18 @@
package com.talentcard.server.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
@Schema(name = "CarWashCredentialSubmitRequest", description = "\u6d17\u8f66\u51ed\u8bc1\u63d0\u4ea4\u53c2\u6570")
@Schema(name = "CarWashCredentialSubmitRequest", description = "洗车凭证提交参数")
public record CarWashCredentialSubmitRequest(
        @NotNull Long talentId,
        @NotNull Long cardId,
        @NotEmpty @Size(max = 9) @Schema(description = "\u6d17\u8f66\u51ed\u8bc1\u6216\u6d88\u8d39\u622a\u56feURL\u5217\u8868") List<@Size(max = 500) String> proofFiles,
        @Size(max = 9) @Schema(description = "洗车凭证或消费截图URL列表,普通商家可不传,手机号写入备注") List<@Size(max = 500) String> proofFiles,
        @Size(max = 500) String description,
        @Schema(description = "\u6d17\u8f66\u6d88\u8d39\u65f6\u95f4") LocalDateTime consumeTime,
        @Schema(description = "洗车消费时间") LocalDateTime consumeTime,
        @Size(max = 128) String idempotencyKey
) {}
talent-card-server/src/main/java/com/talentcard/server/request/CouponClaimIssueRequest.java
@@ -6,10 +6,10 @@
import java.time.LocalDateTime;
@Schema(name = "CouponClaimIssueRequest", description = "\u540e\u53f0\u53d1\u653e\u4f18\u60e0\u5238\u53c2\u6570")
@Schema(name = "CouponClaimIssueRequest", description = "后台发放优惠券参数")
public record CouponClaimIssueRequest(
        @NotNull @Schema(description = "\u9886\u53d6\u4eba\u624dID") Long talentId,
        @Schema(description = "\u4eba\u624d\u5361ID") Long cardId,
        @Schema(description = "\u9886\u53d6\u5238\u8fc7\u671f\u65f6\u95f4\uff0c\u4e0d\u4f20\u4f7f\u7528\u4f18\u60e0\u5238\u6709\u6548\u671f") LocalDateTime expireTime,
        @Size(max = 500) @Schema(description = "\u53d1\u653e\u5907\u6ce8") String remark
        @NotNull @Schema(description = "领取人才ID") Long talentId,
        @Schema(description = "人才卡ID") Long cardId,
        @Schema(description = "领取券过期时间,不传使用优惠券有效期") LocalDateTime expireTime,
        @Size(max = 500) @Schema(description = "发放备注") String remark
) {}
talent-card-server/src/main/java/com/talentcard/server/request/CouponClaimStatusRequest.java
@@ -3,10 +3,10 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
@Schema(name = "CouponClaimStatusRequest", description = "\u4f18\u60e0\u5238\u9886\u53d6\u548c\u4f7f\u7528\u72b6\u6001\u53c2\u6570")
@Schema(name = "CouponClaimStatusRequest", description = "优惠券领取和使用状态参数")
public record CouponClaimStatusRequest(
        @Schema(description = "\u9886\u53d6\u72b6\u6001\uff1a0\u5f85\u9886\u53d6\u30011\u5df2\u9886\u53d6\u30012\u5df2\u53d6\u6d88") Integer claimStatus,
        @Schema(description = "\u4f7f\u7528\u72b6\u6001\uff1a0\u672a\u4f7f\u7528\u30011\u5df2\u4f7f\u7528\u30012\u5df2\u8fc7\u671f\u30013\u5df2\u4f5c\u5e9f") Integer useStatus,
        @Schema(description = "\u4f7f\u7528\u8ba2\u5355ID") Long orderId,
        @Size(max = 500) @Schema(description = "\u72b6\u6001\u5907\u6ce8") String remark
        @Schema(description = "领取状态:0待领取、1已领取、2已取消") Integer claimStatus,
        @Schema(description = "使用状态:0未使用、1已使用、2已过期、3已作废") Integer useStatus,
        @Schema(description = "使用订单ID") Long orderId,
        @Size(max = 500) @Schema(description = "状态备注") String remark
) {}
talent-card-server/src/main/java/com/talentcard/server/request/CouponManagementSaveRequest.java
@@ -11,19 +11,19 @@
import java.time.LocalDate;
import java.util.List;
@Schema(name = "CouponManagementSaveRequest", description = "\u4f18\u60e0\u5238\u914d\u7f6e\u4fdd\u5b58\u53c2\u6570")
@Schema(name = "CouponManagementSaveRequest", description = "优惠券配置保存参数")
public record CouponManagementSaveRequest(
        @NotNull @Schema(description = "\u9002\u7528\u5546\u6237ID", example = "1") Long merchantId,
        @NotBlank @Size(max = 128) @Schema(description = "\u4f18\u60e0\u5238\u540d\u79f0") String couponName,
        @NotBlank @Schema(description = "\u7c7b\u578b\uff1aCASH\u4ee3\u91d1\u5238\u3001FULL_REDUCTION\u6ee1\u51cf\u5238\u3001DISCOUNT\u6298\u6263\u5238\u3001EXCHANGE\u5151\u6362\u5238") String couponType,
        @NotNull @DecimalMin("0.00") @Schema(description = "\u9762\u989d\uff0c\u5355\u4f4d\u5143", example = "50.00") BigDecimal couponAmount,
        @Schema(description = "\u4f7f\u7528\u6761\u4ef6\uff1aNONE\u65e0\u95e8\u69db\u3001FULL\u6ee1\u51cf") String conditionType,
        @DecimalMin("0.00") @Schema(description = "\u6ee1\u51cf\u95e8\u69db\u91d1\u989d") BigDecimal conditionAmount,
        @Schema(description = "\u9002\u7528\u4eba\u624d\u5361\u7b49\u7ea7") List<String> talentLevels,
        @Schema(description = "\u6709\u6548\u5f00\u59cb\u65e5\u671f") LocalDate validStart,
        @Schema(description = "\u6709\u6548\u7ed3\u675f\u65e5\u671f") LocalDate validEnd,
        @Size(max = 64) @Schema(description = "\u53d1\u653e\u6279\u6b21\u53f7\uff0c\u4e0d\u4f20\u81ea\u52a8\u751f\u6210") String batchNo,
        @NotNull @Min(-1) @Schema(description = "\u53d1\u653e\u603b\u91cf\uff0c-1\u8868\u793a\u4e0d\u9650\u91cf", example = "1000") Integer totalQuantity,
        @Schema(description = "\u662f\u5426\u653f\u5e9c\u8865\u8d34") Boolean governmentSubsidy,
        @Schema(description = "\u72b6\u6001\uff1a0\u4e0b\u67b6\u30011\u4e0a\u67b6", example = "1") Integer status
        @NotNull @Schema(description = "适用商户ID", example = "1") Long merchantId,
        @NotBlank @Size(max = 128) @Schema(description = "优惠券名称") String couponName,
        @NotBlank @Schema(description = "类型:CASH代金券、FULL_REDUCTION满减券、DISCOUNT折扣券、EXCHANGE兑换券") String couponType,
        @NotNull @DecimalMin("0.00") @Schema(description = "面额,单位元", example = "50.00") BigDecimal couponAmount,
        @Schema(description = "使用条件:NONE无门槛、FULL满减") String conditionType,
        @DecimalMin("0.00") @Schema(description = "满减门槛金额") BigDecimal conditionAmount,
        @Schema(description = "适用人才卡等级") List<String> talentLevels,
        @Schema(description = "有效开始日期") LocalDate validStart,
        @Schema(description = "有效结束日期") LocalDate validEnd,
        @Size(max = 64) @Schema(description = "发放批次号,不传自动生成") String batchNo,
        @NotNull @Min(-1) @Schema(description = "发放总量,-1表示不限量", example = "1000") Integer totalQuantity,
        @Schema(description = "是否政府补贴") Boolean governmentSubsidy,
        @Schema(description = "状态:0下架、1上架", example = "1") Integer status
) {}
talent-card-server/src/main/java/com/talentcard/server/request/CouponManagementStatusRequest.java
@@ -3,7 +3,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
@Schema(name = "CouponManagementStatusRequest", description = "\u4f18\u60e0\u5238\u4e0a\u4e0b\u67b6\u53c2\u6570")
@Schema(name = "CouponManagementStatusRequest", description = "优惠券上下架参数")
public record CouponManagementStatusRequest(
        @NotNull @Schema(description = "\u72b6\u6001\uff1a0\u4e0b\u67b6\u30011\u4e0a\u67b6") Integer status
        @NotNull @Schema(description = "状态:0下架、1上架") Integer status
) {}
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java
@@ -5,7 +5,7 @@
import com.talentcard.server.entity.DependentCardApply;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "DependentCardApplyAdminResponse", description = "\u7ba1\u7406\u540e\u53f0\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7\u4fe1\u606f")
@Schema(name = "DependentCardApplyAdminResponse", description = "管理后台家属附属卡申请信息")
public record DependentCardApplyAdminResponse(
        Long id,
        String applyNo,
@@ -38,13 +38,13 @@
    }
    private static String statusText(Integer status) {
        if (status == null) return "\u672a\u77e5";
        if (status == null) return "未知";
        return switch (status) {
            case DependentCardApply.STATUS_PENDING_REVIEW -> "\u5f85\u5ba1\u6838";
            case DependentCardApply.STATUS_APPROVED -> "\u5df2\u901a\u8fc7";
            case DependentCardApply.STATUS_REJECTED -> "\u5df2\u9a73\u56de";
            case DependentCardApply.STATUS_CANCELLED -> "\u5df2\u89e3\u7ed1";
            default -> "\u672a\u77e5";
            case DependentCardApply.STATUS_PENDING_REVIEW -> "待审核";
            case DependentCardApply.STATUS_APPROVED -> "已通过";
            case DependentCardApply.STATUS_REJECTED -> "已驳回";
            case DependentCardApply.STATUS_CANCELLED -> "已解绑";
            default -> "未知";
        };
    }
}
talent-card-server/src/main/java/com/talentcard/server/service/AdminWorkbenchStatisticsService.java
@@ -25,7 +25,7 @@
    public Map<String, Object> summary() {
        Map<String, Object> row = mapper.selectSummary();
        if (row == null) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "\u5de5\u4f5c\u53f0\u7edf\u8ba1\u67e5\u8be2\u5931\u8d25");
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "工作台统计查询失败");
        }
        long subsidyCents = number(row.get("subsidyAmountCents"));
        long monthlyVerification = number(row.get("monthlyVerificationCount"));
@@ -43,12 +43,12 @@
        result.put("budgetUsageRateText", "0.00%");
        List<Map<String, Object>> alerts = new ArrayList<>();
        addAlert(alerts, "\u5f85\u5904\u7406\u8ba2\u5355", number(row.get("pendingOrderCount")));
        addAlert(alerts, "\u5f85\u5ba1\u6838\u5546\u54c1", number(row.get("pendingProductCount")));
        addAlert(alerts, "\u5f85\u5904\u7406\u9884\u7ea6", number(row.get("pendingApplicationCount")));
        addAlert(alerts, "待处理订单", number(row.get("pendingOrderCount")));
        addAlert(alerts, "待审核商品", number(row.get("pendingProductCount")));
        addAlert(alerts, "待处理预约", number(row.get("pendingApplicationCount")));
        result.put("alertCount", alerts.stream().mapToLong(a -> number(a.get("count"))).sum());
        result.put("alerts", alerts);
        result.put("statisticsBasis", "\u8865\u8d34\u4ec5\u7edf\u8ba1\u653f\u5e9c\u627f\u62c5\u7684\u5546\u54c1\u8865\u8d34\u548c\u653f\u5e9c\u8865\u8d34\u5238\u91d1\u989d\uff0c\u5546\u5bb6\u81ea\u884c\u8ba9\u5229\u4e0d\u8ba1\u5165\u653f\u5e9c\u8865\u8d34");
        result.put("statisticsBasis", "补贴仅统计政府承担的商品补贴和政府补贴券金额,商家自行让利不计入政府补贴");
        return result;
    }
talent-card-server/src/main/java/com/talentcard/server/service/BenefitQuotaService.java
@@ -140,20 +140,20 @@
            aliases.add(raw);
        }
        String upper = raw.toUpperCase(Locale.ROOT);
        if (upper.contains("EXCELLENT") || raw.contains("\u5353\u8d8a")) {
        if (upper.contains("EXCELLENT") || raw.contains("卓越")) {
            aliases.add("EXCELLENT");
            aliases.add("\u5353\u8d8a\u4eba\u624d\u5361");
            aliases.add("\u5353\u8d8a");
            aliases.add("卓越人才卡");
            aliases.add("卓越");
        }
        if (upper.contains("OUTSTANDING") || raw.contains("\u6770\u51fa")) {
        if (upper.contains("OUTSTANDING") || raw.contains("杰出")) {
            aliases.add("OUTSTANDING");
            aliases.add("\u6770\u51fa\u4eba\u624d\u5361");
            aliases.add("\u6770\u51fa");
            aliases.add("杰出人才卡");
            aliases.add("杰出");
        }
        if (upper.contains("ELITE") || raw.contains("\u83c1\u82f1") || raw.contains("\u7cbe\u82f1")) {
        if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) {
            aliases.add("ELITE");
            aliases.add("\u83c1\u82f1\u4eba\u624d\u5361");
            aliases.add("\u83c1\u82f1");
            aliases.add("菁英人才卡");
            aliases.add("菁英");
        }
        return aliases.isEmpty() ? List.of(raw) : List.copyOf(aliases);
    }
@@ -420,13 +420,13 @@
    private String normalizeCardLevel(Object value) {
        String raw = text(value);
        String upper = raw.toUpperCase(Locale.ROOT);
        if (upper.contains("EXCELLENT") || raw.contains("\u5353\u8d8a")) {
        if (upper.contains("EXCELLENT") || raw.contains("卓越")) {
            return "EXCELLENT";
        }
        if (upper.contains("OUTSTANDING") || raw.contains("\u6770\u51fa")) {
        if (upper.contains("OUTSTANDING") || raw.contains("杰出")) {
            return "OUTSTANDING";
        }
        if (upper.contains("ELITE") || raw.contains("\u83c1\u82f1") || raw.contains("\u7cbe\u82f1")) {
        if (upper.contains("ELITE") || raw.contains("菁英") || raw.contains("精英")) {
            return "ELITE";
        }
        return upper;
@@ -502,16 +502,16 @@
        if (cycle.isBlank() || "NONE".equals(cycle)) {
            return LocalDate.of(1970, 1, 1).atStartOfDay();
        }
        if (cycle.contains("DAY") || cycle.contains("DAILY") || cycle.contains("\u5929") || cycle.contains("\u65e5")) {
        if (cycle.contains("DAY") || cycle.contains("DAILY") || cycle.contains("天") || cycle.contains("日")) {
            return today.atStartOfDay();
        }
        if (cycle.contains("WEEK") || cycle.contains("\u5468") || cycle.contains("\u661f\u671f")) {
        if (cycle.contains("WEEK") || cycle.contains("周") || cycle.contains("星期")) {
            return today.minusDays(today.getDayOfWeek().getValue() - 1L).atStartOfDay();
        }
        if (cycle.contains("YEAR") || cycle.contains("ANNUAL") || cycle.contains("\u5e74")) {
        if (cycle.contains("YEAR") || cycle.contains("ANNUAL") || cycle.contains("年")) {
            return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay();
        }
        if (cycle.contains("MONTH") || cycle.contains("\u6708") || monthlyDefault) {
        if (cycle.contains("MONTH") || cycle.contains("月") || monthlyDefault) {
            return today.withDayOfMonth(1).atStartOfDay();
        }
        return today.with(TemporalAdjusters.firstDayOfYear()).atStartOfDay();
talent-card-server/src/main/java/com/talentcard/server/service/CarWashBenefitManagementService.java
@@ -120,8 +120,7 @@
            primaryCardId = longValue(primary, "primaryCardId");
            cardLevel = text(primary, "cardLevel");
        }
        List<String> files = request.proofFiles().stream().map(this::clean).filter(v -> !v.isBlank()).distinct().toList();
        if (files.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传洗车凭证或消费截图");
        List<String> files = request.proofFiles() == null ? List.of() : request.proofFiles().stream().map(this::clean).filter(v -> !v.isBlank()).distinct().toList();
        for (String file : files) {
            if (!(file.startsWith("/uploads/") || file.startsWith("https://"))) {
                throw new BusinessException(ErrorCode.BAD_REQUEST, "凭证文件地址不正确");
@@ -306,7 +305,8 @@
    }
    private void requireMerchant(Long merchantId) {
        if (merchantId == null || merchantId <= 0 || number(configMapper.countCarWashMerchant(merchantId)).longValue() == 0) throw new BusinessException(ErrorCode.NOT_FOUND, "洗车商家不存在");
        if (merchantId == null || merchantId <= 0 || number(configMapper.countCarWashMerchant(merchantId)).longValue() == 0)
            throw new BusinessException(ErrorCode.NOT_FOUND, "洗车商家不存在");
    }
    private void validateStatus(Integer status) { if (status != null && (status < 0 || status > 3)) throw new BusinessException(ErrorCode.BAD_REQUEST, "洗车凭证状态不正确"); }
    private Number number(Object value) { return value instanceof Number number ? number : 0L; }
talent-card-server/src/main/resources/application.yml
@@ -9,7 +9,7 @@
      max-file-size: 10MB
      max-request-size: 10MB
  datasource:
    url: ${MYSQL_URL:jdbc:mysql://localhost:3306/talent_card?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false}
    url: ${MYSQL_URL:jdbc:mysql://192.168.110.13:3306/talent_card?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false}
    username: ${MYSQL_USERNAME:root}
    password: ${MYSQL_PASSWORD:123456}
    driver-class-name: com.mysql.cj.jdbc.Driver
@@ -23,7 +23,7 @@
      repositories:
        enabled: false
  flyway:
    enabled: true
    enabled: false
    locations: classpath:db/migration
    baseline-on-migrate: true
  jpa: