shinnosuke
2026-09-21 7844f5af457ff3b7d3f8eeac8799e8d281e635a1
更新....
2个文件已添加
25个文件已修改
431 ■■■■■ 已修改文件
rencaika/pages/medical-package/medical-package.uvue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/pending-payment/pending-payment.uvue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/premium-benefits/premium-benefits.uvue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/merchant/merchant.uvue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/order-detail/order-detail.uvue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/order-submit-success/order-submit-success.uvue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/orders/orders.uvue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika_merchant/pages/product-detail/product-detail.uvue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/api/admin.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MovieTicketView.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentNewsEditView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentNewsPublishView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/TalentPolicyPublishView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/cp-verify.txt 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/auth/TalentAuthService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/MedicalPackagePortalService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/common/GlobalExceptionHandler.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/medical-package/medical-package.uvue
@@ -9,6 +9,7 @@
                <view class="tabs"><view class="tab-item active"><text>详情</text><view class="tab-line"></view></view></view>
                <view v-if="itemsLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载检查项目</text></view><view v-else-if="itemsError" class="list-state error-state"><text>检查项目加载失败</text><text class="retry-text" @tap="loadItems">重新加载</text></view><view v-else-if="itemsLoaded && packageItems.length == 0" class="list-state"><text>暂无体检检查项目</text></view>
                <view v-if="itemsLoaded && packageItems.length > 0" class="check-table"><view class="check-head"><view class="check-cell cell-item"><text>检查项目</text></view><view class="check-cell cell-meaning"><text>项目意义</text></view><view class="check-cell cell-gender"><text>男</text></view><view class="check-cell cell-gender cell-gender-end"><text>女</text></view></view><view class="check-row" v-for="item in packageItems" :key="item.id"><view class="check-cell cell-item"><text>{{item.name}}</text></view><view class="check-cell cell-meaning"><text>{{item.meaning}}</text></view><view class="check-cell cell-gender"><text>{{item.male ? '√' : ''}}</text></view><view class="check-cell cell-gender cell-gender-end"><text>{{item.female ? '√' : ''}}</text></view></view></view>
                <view v-if="detailImages.length > 0" class="detail-images"><image v-for="(imageUrl, index) in detailImages" :key="imageUrl + index" class="detail-image" :src="imageUrl" mode="widthFix" @error="dropDetailImage(index)"></image></view>
            </view><view class="content-bottom"></view>
        </view></scroll-view>
        <view class="action-bar"><view :class="canBook ? 'booking-button' : 'booking-button booking-disabled'" hover-class="booking-button-active" @tap="goBooking"><text>{{packageAvailable ? '预约' : '暂不可预约'}}</text></view></view>
@@ -23,6 +24,8 @@
    const packageId = ref(''); const hospitalId = ref(''); const routeApplicableCards = ref(''); const detailLoading = ref(true); const detailError = ref(false); const itemsLoading = ref(false); const itemsLoaded = ref(false); const itemsError = ref(false)
    const packageName = ref(''); const heroImage = ref(''); const freeLabel = ref('免费'); const applicableCards = ref(''); const packageAvailable = ref(false); const unavailableReason = ref('该套餐暂不可预约')
    const hospitalName = ref(''); const hospitalAddress = ref(''); const hospitalPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const packageItems = ref<any[]>([])
    // 商户在「图文详情」上传的图片(可能多张),按顺序铺在检查项目表格下面。
    const detailImages = ref<string[]>([])
    const canNavigate = computed(() => latitude.value > 0 && longitude.value > 0)
    const canBook = computed(() => !detailLoading.value && !detailError.value && packageAvailable.value)
    const decodeRouteText = (value: any): string => {
@@ -38,12 +41,13 @@
    const loadDetail = () => {
        if (packageId.value == '') { detailLoading.value = false; detailError.value = true; return }
        detailLoading.value = true; detailError.value = false
        getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''; freeLabel.value = data.freeLabel || '免费'; const apiApplicableCards = data.applicableCards != null ? String(data.applicableCards).trim() : ''; applicableCards.value = apiApplicableCards != '' ? apiApplicableCards : routeApplicableCards.value; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true })
        getMedicalPackage(packageId.value, (data: any) => { const hospital: any = data.hospital || {}; packageName.value = data.name || ''; heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : ''; freeLabel.value = data.freeLabel || '免费'; const apiApplicableCards = data.applicableCards != null ? String(data.applicableCards).trim() : ''; applicableCards.value = apiApplicableCards != '' ? apiApplicableCards : routeApplicableCards.value; packageAvailable.value = data.available === true; unavailableReason.value = data.unavailableReason || unavailableReason.value; hospitalId.value = String(hospital.hospitalId || hospitalId.value); hospitalName.value = hospital.name || ''; hospitalAddress.value = hospital.address || '医院暂未配置地址'; hospitalPhone.value = hospital.mobile || ''; latitude.value = Number(hospital.latitude); longitude.value = Number(hospital.longitude); const images: string[] = []; const sourceImages: any[] = data.detailImages instanceof Array ? data.detailImages as any[] : []; for (let index = 0; index < sourceImages.length; index++) { const value = sourceImages[index] == null ? '' : ('' + sourceImages[index]).trim(); if (value.length > 0) images.push(resolveApiUrl(value)) }; detailImages.value = images; detailLoading.value = false; loadItems() }, () => { detailLoading.value = false; detailError.value = true })
    }
    const loadItems = () => { if (packageId.value == '') return; itemsLoading.value = true; itemsLoaded.value = false; itemsError.value = false; getMedicalPackageItems(packageId.value, (data: any) => { packageItems.value = data instanceof Array ? data as any[] : []; itemsLoading.value = false; itemsLoaded.value = true }, () => { itemsLoading.value = false; itemsError.value = true }) }
    const goBack = () => safeBack('/pages/premium-benefits/premium-benefits')
    const handleScroll = (event: any) => { const show = event.detail.scrollTop > 64; if (isSolidNav.value != show) isSolidNav.value = show }
    const replaceHero = () => { heroImage.value = '' }
    const dropDetailImage = (index: number) => { const next = detailImages.value.slice(); next.splice(index, 1); detailImages.value = next }
    const openMap = () => { if (!canNavigate.value) { uni.showToast({ title: '医院暂未配置位置', icon: 'none' }); return }; uni.openLocation({ latitude: latitude.value, longitude: longitude.value, name: hospitalName.value, address: hospitalAddress.value, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) }
    const callHospital = () => { if (hospitalPhone.value == '') { uni.showToast({ title: '医院暂未配置联系电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: hospitalPhone.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
    const goBooking = () => { if (!canBook.value) { if (!detailLoading.value && !detailError.value) uni.showToast({ title: unavailableReason.value, icon: 'none' }); return }; uni.navigateTo({ url: '/pages/medical-booking/medical-booking?packageId=' + encodeURIComponent(packageId.value) + '&hospitalId=' + encodeURIComponent(hospitalId.value) + '&package=' + encodeURIComponent(packageName.value) + '&hospital=' + encodeURIComponent(hospitalName.value) }) }
@@ -83,4 +87,15 @@
        display: flex;
        flex-direction: row;
    }
    /* 商户在「图文详情」上传的多张图片:整组铺在检查项目表格下面,按原图比例完整显示。 */
    .detail-images {
        margin: 16rpx 25rpx 0;
    }
    .detail-image {
        display: block;
        width: 100%;
        margin-bottom: 16rpx;
        border-radius: 8rpx;
        background-color: #F2F4F7;
    }
</style>
rencaika/pages/pending-payment/pending-payment.uvue
@@ -477,9 +477,11 @@
    .merchant-main { min-width: 0; flex: 1; }
    .merchant-name { display: block; color: #252B34; font-size: 32rpx; line-height: 43rpx; font-weight: 700; }
    .merchant-address { display: block; margin-top: 6rpx; color: #A0A8B3; font-size: 21rpx; line-height: 31rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .merchant-tag { display: flex; width: 234rpx; height: 52rpx; margin-top: 27rpx; border-radius: 27rpx; background-image: linear-gradient(90deg, #FFF0E5 0%, rgba(255,240,229,.45) 100%); flex-direction: row; justify-content: center; align-items: center; }
    .merchant-tag image { width: 29rpx; height: 29rpx; margin-right: 12rpx; }
    .merchant-tag text { color: #FF812E; font-size: 21rpx; }
    /* 宽度不能写死:标签文案是后台配的(如「到店消费 - 卓越人才卡」),固定 234rpx 会把两端文字裁掉。
       改成按内容自适应,最高占满整行,放不下才省略。 */
    .merchant-tag { display: flex; width: fit-content; max-width: 100%; min-height: 52rpx; padding: 0 22rpx; margin-top: 27rpx; box-sizing: border-box; border-radius: 27rpx; background-image: linear-gradient(90deg, #FFF0E5 0%, rgba(255,240,229,.45) 100%); flex-direction: row; justify-content: center; align-items: center; }
    .merchant-tag image { width: 29rpx; height: 29rpx; margin-right: 12rpx; flex-shrink: 0; }
    .merchant-tag text { color: #FF812E; font-size: 21rpx; line-height: 30rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .merchant-actions { display: flex; width: 138rpx; margin-left: 15rpx; flex-shrink: 0; flex-direction: row; justify-content: space-between; align-items: center; }
    .merchant-action { display: flex; width: 60rpx; flex-direction: column; align-items: center; }
    .merchant-action image { width: 38rpx; height: 38rpx; }
@@ -503,12 +505,14 @@
    .actual-label { color: #9BA3AF; font-size: 21rpx; line-height: 30rpx; }
    .actual-amount { margin-top: 6rpx; color: #FF4D35; font-size: 47rpx; line-height: 58rpx; font-weight: 700; }
    .payment-checking { margin-top: 8rpx; color: #FF812E; font-size: 22rpx; line-height: 30rpx; }
    .pay-button { display: flex; min-width: 160rpx; height: 58rpx; margin: 18rpx auto 0; padding: 0 24rpx; box-sizing: border-box; border-radius: 30rpx; background-image: linear-gradient(90deg, #FF4E35 0%, #FF8318 100%); justify-content: center; align-items: center; }
    .pay-button text { color: #FFFFFF; font-size: 25rpx; font-weight: 600; }
    /* 主按钮按原型:与「确认支付」页同一套尺寸(514×86rpx、胶囊圆角、橙色渐变、30rpx 白字)。 */
    .pay-button { display: flex; width: 514rpx; height: 86rpx; margin: 26rpx auto 0; box-sizing: border-box; border-radius: 45rpx; background-image: linear-gradient(90deg, #FF4C32 0%, #FF841C 100%); justify-content: center; align-items: center; }
    .pay-button text { color: #FFFFFF; font-size: 30rpx; font-weight: 600; }
    .pay-button-active { opacity: .78; }
    .pay-button.disabled { background-image: none; background-color: #C7CBD1; }
    .cancel-button { display: flex; min-width: 160rpx; height: 58rpx; margin-top: 18rpx; border-width: 1rpx; border-style: solid; border-color: #FF6D32; border-radius: 30rpx; justify-content: center; align-items: center; }
    .cancel-button text { color: #FF6730; font-size: 25rpx; font-weight: 600; } .cancel-button-active { opacity: .55; }
    .refresh-button { display: flex; min-width: 160rpx; height: 58rpx; margin-top: 18rpx; border-width: 1rpx; border-style: solid; border-color: #C7CBD1; border-radius: 30rpx; justify-content: center; align-items: center; }
    .refresh-button text { color: #6B7280; font-size: 25rpx; font-weight: 600; }
    /* 次要操作按原型做成纯文字(不再画描边胶囊)。 */
    .cancel-button { display: flex; width: 514rpx; height: 76rpx; margin-top: 10rpx; justify-content: center; align-items: center; }
    .cancel-button text { color: #4A5261; font-size: 28rpx; font-weight: 500; } .cancel-button-active { opacity: .55; }
    .refresh-button { display: flex; width: 514rpx; height: 76rpx; margin-top: 10rpx; justify-content: center; align-items: center; }
    .refresh-button text { color: #6B7280; font-size: 28rpx; font-weight: 500; }
</style>
rencaika/pages/premium-benefits/premium-benefits.uvue
@@ -6,7 +6,7 @@
            <view v-if="topicError" class="topic-error"><text>专题配置加载失败</text><text @tap="loadPremiumTopic">重新加载</text></view>
            <view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view>
            <view class="service-section"><scroll-view class="tab-scroll" scroll-x :show-scrollbar="false"><view class="service-tabs" :style="'width:' + Math.max(750, serviceTabs.length * 234) + 'rpx'"><view v-for="tab in serviceTabs" :key="tab.id" class="service-tab" :class="tab.id == activeTab ? 'active' : ''" @tap="selectTab(tab.id)"><text>{{tab.name}}</text><view class="tab-line"></view></view></view></scroll-view>
                <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载臻享服务</text></view><view v-else-if="initialError" class="service-empty error-state"><text>臻享服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关臻享服务</text></view><view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text v-if="!item.available || item.benefitTagVisible" class="service-benefit">{{item.available ? item.benefit : '已停用'}}</text><view class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></view>
                <view v-if="initialLoading" class="service-empty"><view class="loading-spinner"></view><text>正在加载臻享服务</text></view><view v-else-if="initialError" class="service-empty error-state"><text>臻享服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="service-empty"><text>暂无相关臻享服务</text></view><view v-else-if="hasLoaded" class="service-list"><view v-for="item in visibleServices" :key="item.id" class="service-item" :class="item.available ? '' : 'is-unavailable'" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><view v-if="!item.available || item.benefitTagVisible" class="service-benefit-row"><text class="service-benefit">{{item.available ? item.benefit : '已停用'}}</text><text v-if="item.available && item.discount != ''" class="service-benefit service-benefit-extra">{{item.discount}}</text></view><view class="address-row"><image src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text>{{item.address}}</text></view></view></view></view>
                <view v-if="loadingMore" class="loading-row"><view class="loading-spinner"></view><text>正在加载</text></view><view v-else-if="loadMoreError" class="loading-row load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view><view v-else-if="hasLoaded && !hasMore && visibleServices.length > 0" class="no-more"><view></view><text>已经到底了</text><view></view></view>
            </view><view class="bottom-space"></view>
        </scroll-view>
@@ -50,7 +50,7 @@
    const loadServices = (append: boolean) => {
        if (!isApiConfigured() || activeTab.value == '') { initialLoading.value = false; loadingMore.value = false; initialError.value = true; return }; const requestVersion = listRequestVersion.value
        if (!append && failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return }; if (append && failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return }
        getPremiumTopicServices(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; const tagVisible = item.benefitTagVisible !== false; next.push({ id: '' + item.id, businessType: item.businessType != null ? item.businessType as string : 'MERCHANT', merchantType: item.merchantType != null ? item.merchantType as string : 'service', name: item.merchantName != null ? item.merchantName as string : '臻享服务', benefit: tagVisible ? (item.benefitText != null ? item.benefitText as string : '') : '', benefitTagVisible: tagVisible, address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true })
        getPremiumTopicServices(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; const tagVisible = item.benefitTagVisible !== false; next.push({ id: '' + item.id, businessType: item.businessType != null ? item.businessType as string : 'MERCHANT', merchantType: item.merchantType != null ? item.merchantType as string : 'service', name: item.merchantName != null ? item.merchantName as string : '臻享服务', benefit: tagVisible ? (item.benefitText != null ? item.benefitText as string : '') : '', benefitTagVisible: tagVisible, discount: item.discountText != null && item.discountText != item.benefitText ? item.discountText as string : '', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true })
    }
    const loadMore = () => { if (initialLoading.value || loadingMore.value || !hasMore.value) return; loadMoreError.value = false; loadingMore.value = true; loadServices(true) }
    const goBack = () => safeBack('/pages/benefits/benefits')
@@ -70,4 +70,7 @@
    .hero { height:730rpx; position:relative; overflow:visible; background-color:#7296B5; }.hero-image { position:absolute; left:0; top:0; width:100%; height:730rpx; }.hero-top-shade { position:absolute; left:0; right:0; top:0; height:245rpx; background-image:linear-gradient(180deg,rgba(0,0,0,.48),rgba(0,0,0,0)); }.hero-bottom-shade { position:absolute; left:0; right:0; bottom:0; height:230rpx; background-image:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,0,0,.40)); }.scene-name { position:absolute; z-index:3; left:38rpx; top:318rpx; color:#FFFFFF; font-family:'FoodCalligraphy'; font-size:48rpx; line-height:66rpx; text-shadow:0 3rpx 10rpx rgba(0,0,0,.55); }.scene-scroll { position:absolute; z-index:4; left:0; right:0; top:469rpx; width:100%; height:150rpx; white-space:nowrap; }.scene-row { display:flex; height:150rpx; flex-direction:row; align-items:center; }.scene-pad { width:322rpx; height:1rpx; flex-shrink:0; }.scene-item { display:flex; width:106rpx; height:106rpx; margin-right:31rpx; box-sizing:border-box; overflow:hidden; position:relative; z-index:1; border:3rpx solid rgba(255,255,255,.9); border-radius:8rpx; flex-shrink:0; transition:transform 180ms ease-out; }.scene-item image { width:100%; height:100%; }.scene-item.active { z-index:3; border-width:4rpx; border-color:#FFD329; transform:scale(1.28); box-shadow:0 8rpx 18rpx rgba(0,0,0,.28); }
    .topic-error { display:flex; height:76rpx; padding:0 31rpx; background-color:#FFF8F4; flex-direction:row; justify-content:center; align-items:center; }.topic-error text { color:#A48A7A; font-size:22rpx; }.topic-error text + text { margin-left:22rpx; color:#F26A38; }.topic-summary { position:relative; z-index:4; margin-top:-66rpx; }.intro-card { margin:0 31rpx; box-sizing:border-box; padding:25rpx 31rpx; border-radius:26rpx; background-image:linear-gradient(90deg,#FF5320,#FF8A22); box-shadow:0 10rpx 22rpx rgba(255,104,28,.24); }.intro-card text { color:#FFFFFF; font-size:23rpx; line-height:34rpx; }.service-section { padding:48rpx 31rpx 0; }.tab-scroll { width:100%; height:78rpx; white-space:nowrap; }.service-tabs { display:flex; height:72rpx; flex-direction:row; align-items:flex-start; }.service-tab { display:flex; width:210rpx; margin-right:24rpx; flex-direction:column; align-items:center; flex-shrink:0; }.service-tab text { color:#8A8D92; font-size:28rpx; font-weight:600; white-space:nowrap; }.service-tab.active text { color:#363636; font-weight:700; }.tab-line { width:0; height:7rpx; margin-top:12rpx; border-radius:4rpx; background-image:linear-gradient(90deg,#FF4D36,#FFBF3A); transition:width 180ms ease-out; }.service-tab.active .tab-line { width:61rpx; }
    .service-list { padding-top:10rpx; }.service-item { display:flex; min-height:215rpx; margin-bottom:22rpx; flex-direction:row; align-items:flex-start; }.service-item.is-unavailable { opacity:.55; }.service-image { width:184rpx; height:184rpx; border-radius:17rpx; flex-shrink:0; background-color:#EEEEEE; }.service-info { display:flex; min-width:0; min-height:184rpx; flex:1; box-sizing:border-box; padding:4rpx 0 4rpx 24rpx; flex-direction:column; justify-content:space-between; }.service-name { color:#292929; font-size:29rpx; line-height:40rpx; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.service-benefit { align-self:flex-start; max-width:400rpx; padding:6rpx 9rpx; border-radius:6rpx; background-color:#FFF5E7; color:#FF7C2B; font-size:21rpx; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }.address-row { display:flex; min-width:0; box-sizing:border-box; padding:7rpx 10rpx; border-radius:7rpx; background-image:linear-gradient(90deg,rgba(239,240,242,.98),rgba(242,243,245,.76) 58%,rgba(255,255,255,0)); flex-direction:row; align-items:flex-start; }.address-row image { width:27rpx; height:27rpx; margin-right:8rpx; flex-shrink:0; }.address-row text { min-width:0; flex:1; max-height:62rpx; overflow:hidden; color:#7E8490; font-size:21rpx; line-height:31rpx; }.service-empty { display:flex; height:310rpx; justify-content:center; align-items:center; }.service-empty text { color:#A5A8AD; font-size:25rpx; }.error-state { flex-direction:column; }.error-state .retry-text { margin-top:22rpx; color:#F26A38; }.loading-row,.no-more { display:flex; height:90rpx; flex-direction:row; justify-content:center; align-items:center; }.loading-spinner { width:28rpx; height:28rpx; margin-right:13rpx; box-sizing:border-box; border:3rpx solid #FFD6C4; border-top-color:#FF6C2B; border-radius:50%; animation:premium-loading 720ms linear infinite; }.loading-row text { color:#989DA5; font-size:22rpx; }.load-error text { color:#F26A38; }.no-more view { width:70rpx; height:1rpx; background-color:#E7E7E7; }.no-more text { margin:0 18rpx; color:#B1B3B7; font-size:21rpx; }.bottom-space { height:calc(30rpx + env(safe-area-inset-bottom)); }@keyframes premium-loading { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
    /* 标签与折扣并排显示:标签是后台配的文字(如「卓越人才卡」),折扣是另一个字段(如「8折」)。 */
    .service-benefit-row { display:flex; flex-direction:row; align-items:center; }
    .service-benefit-row .service-benefit + .service-benefit { margin-left:10rpx; }
</style>
rencaika_merchant/pages/merchant/merchant.uvue
@@ -1,5 +1,7 @@
<template>
    <view class="page">
        <!-- 内容改放 scroll-view:商户中心加了「收款能力」「商户进件」几行后,固定高度会把最后一行「设置」挤出屏幕(页面本身 overflow: hidden,滚不到)。 -->
        <scroll-view class="page-scroll" scroll-y="true" :show-scrollbar="false">
        <view class="hero">
            <image class="hero-image" :src="merchant.coverUrl.length > 0 ? merchant.coverUrl : '/static/merchant-header-hampton.jpg'" mode="aspectFill"></image>
            <view class="hero-mask"></view>
@@ -33,6 +35,7 @@
            </view>
        </view>
        </scroll-view>
        <view class="merchant-tabbar-host"><merchant-tabbar active="merchant"></merchant-tabbar></view>
    </view>
</template>
@@ -138,6 +141,7 @@
<style>
    .page { display: flex; flex: 1; flex-direction: column; width: 100%; height: 100vh; position: relative; overflow: hidden; background-color: #F8F8F8; }
    .page-scroll { flex: 1; height: 0; }
    .hero { height: 624rpx; position: relative; overflow: hidden; flex: none; }
    .hero-image { position: absolute; top: 0; left: 0; width: 100%; height: 624rpx; }
    .hero-mask { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(180deg, rgba(18, 23, 31, 0.18) 0%, rgba(18, 23, 31, 0.56) 100%); }
rencaika_merchant/pages/order-detail/order-detail.uvue
@@ -92,7 +92,8 @@
            statusIcon.value = '/static/icons/lucide/x.svg'
        } else if (pendingPayment) {
            statusTitle.value = detail.statusText.length > 0 ? detail.statusText : '待支付'
            statusDesc.value = '订单已发送给人才,等待其完成支付。'
            // 文案按原型:待支付订单还没核销,别说成「已完成支付,请确认后核销」。
            statusDesc.value = '订单已生成,等待用户完成支付后即可继续核销。'
            statusIcon.value = '/static/icons/lucide/clock-3.svg'
        } else {
            statusTitle.value = isCompleted.value ? '已完成' : '待核销'
rencaika_merchant/pages/order-submit-success/order-submit-success.uvue
New file
@@ -0,0 +1,37 @@
<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></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>
<script setup lang="uts">
    // 到店消费:商家提交订单后的结果页。与核销成功页同一套视觉,只是文案换成「提交成功,待用户支付」。
    const orderNo = ref('-')
    onLoad((options: any) => {
        if (options != null && options.orderNo != null && ('' + options.orderNo).length > 0) orderNo.value = decodeURIComponent('' + options.orderNo)
    })
    const viewOrder = () => {
        if (orderNo.value == '-' || orderNo.value.length == 0) {
            uni.showToast({ title: '订单号缺失', icon: 'none' })
            return
        }
        uni.navigateTo({ url: '/pages/order-detail/order-detail?orderNo=' + encodeURIComponent(orderNo.value) })
    }
    // 返回时回到扫码页接着扫下一位顾客(提交页是 redirectTo 进来的,栈里只剩扫码页)。
    const returnToScan = () => uni.navigateBack({ delta: 1, fail: () => uni.reLaunch({ url: '/pages/scan/scan' }) })
</script>
<style>
    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; }
    .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: 222rpx 64rpx 0; flex-direction: column; align-items: center; box-sizing: border-box; }
    .success-shell { display: flex; width: 144rpx; height: 144rpx; justify-content: center; align-items: center; border-width: 12rpx; border-style: solid; border-color: #FFE2C4; border-radius: 78rpx; background-color: #FFECCE; box-sizing: border-box; }
    .success-icon { width: 86rpx; height: 86rpx; }
    .result-title { margin-top: 38rpx; color: #273345; font-size: 32rpx; line-height: 45rpx; font-weight: 700; }
    .result-message { margin-top: 12rpx; color: #9AA6B8; font-size: 22rpx; line-height: 31rpx; text-align: center; }
    .return-button { display: flex; width: 216rpx; height: 68rpx; margin-top: 28rpx; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFCCAA; border-radius: 36rpx; color: #FF6B3C; font-size: 25rpx; line-height: 35rpx; font-weight: 600; background-color: #FFF4E8; }
</style>
rencaika_merchant/pages/orders/orders.uvue
@@ -392,7 +392,11 @@
            setPickerDate(startDateValue.value > 0 ? startDateValue.value : defaultDateValue.value)
            return
        }
        // 不再把起始日期抄进截止日期:现在支持开区间,抄一次会把"起始日期之后"悄悄变成"只有这一天"。
        // 闭区间:点截止日期时若还没选过,先用起始日期的值垫上(查同一天就不用滚两遍)。
        if (endDateValue.value == 0 && startDateValue.value > 0) {
            endDateValue.value = startDateValue.value
            endDate.value = startDate.value
        }
        setPickerDate(endDateValue.value > 0 ? endDateValue.value : (startDateValue.value > 0 ? startDateValue.value : defaultDateValue.value))
    }
@@ -467,15 +471,21 @@
    }
    const confirmDateFilter = () => {
        // 开区间:只选一头也算有效筛选(后端 startDate / endDate 各自可空)。
        // 以前两头都必须选,且不管缺哪一头都提示"请选择截止日期":只选了截止日的人会以为
        // 系统没认下他选的那天,反复点确认也过不去。
        if (startDateValue.value == 0 && endDateValue.value == 0) {
            selectDateField('start')
            uni.showToast({ title: '请选择起始日期或截止日期', icon: 'none' })
        // 闭区间:起始日期和截止日期都要选。缺哪一头就提示哪一头,并把焦点挪到那一头
        // (以前不管缺哪一头都提示"请选择截止日期":只选了截止日的人会以为系统没认下他选的那天)。
        if (startDateValue.value == 0) {
            activeDateField.value = 'start'
            setPickerDate(defaultDateValue.value)
            uni.showToast({ title: '请选择起始日期', icon: 'none' })
            return
        }
        if (startDateValue.value > 0 && endDateValue.value > 0 && startDateValue.value > endDateValue.value) {
        if (endDateValue.value == 0) {
            activeDateField.value = 'end'
            setPickerDate(startDateValue.value)
            uni.showToast({ title: '请选择截止日期', icon: 'none' })
            return
        }
        if (startDateValue.value > endDateValue.value) {
            uni.showToast({ title: '起始日不能晚于截止日', icon: 'none' })
            return
        }
@@ -504,6 +514,11 @@
    onLoad(() => {
        if (hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {})
    })
    // 列表用 onShow 拉:核销成功后从结果页/详情页返回时,状态才是最新的。
    // 之前只在 onLoad 拉一次,返回列表还停在「待核销」,要切一次分类标签才刷新。
    onShow(() => {
        reloadOrders()
    })
rencaika_merchant/pages/product-detail/product-detail.uvue
@@ -51,14 +51,15 @@
                <view class="change-cover" @tap="addCheckItem"><text class="change-plus">+</text><text>新增检查项目</text></view>
            </view>
            <view v-if="!isHealthCheckMerchant" class="form-card detail-card">
            <!-- 图文详情:所有商家都能传(体检医院的商品详情是下面的检查项目表,这里只放图片,不放富文本)。 -->
            <view class="form-card detail-card">
                <text class="section-title">图文详情</text>
                <view class="gallery-row">
                    <image v-for="(imagePath, index) in detailImages" :key="index" class="detail-thumb" :src="imagePath" mode="aspectFill"></image>
                    <view v-if="detailImages.length < productDetailImageMaxCount" class="add-image" @tap="addDetailImage"><text>+</text></view>
                </view>
                <text class="image-rule">已选择 {{detailImages.length}}/{{productDetailImageMaxCount}} 张</text>
                <textarea class="detail-textarea" maxlength="1000" v-model="detailText" placeholder="请输入" placeholder-class="input-placeholder" :auto-height="false"></textarea>
                <textarea v-if="!isHealthCheckMerchant" class="detail-textarea" maxlength="1000" v-model="detailText" placeholder="请输入" placeholder-class="input-placeholder" :auto-height="false"></textarea>
            </view>
            <view v-if="!isHealthCheckMerchant" class="form-card notice-card">
@@ -155,6 +156,20 @@
        itemError.value = ''
    }
    /** 判断 detailText 是不是体检检查项目([{name,meaning,gender}] 的 JSON 数组)。 */
    const looksLikeHealthCheckItems = (text: string): boolean => {
        const source = text == null ? '' : ('' + text).trim()
        if (source.length == 0 || source.indexOf('[') != 0) return false
        try {
            const parsed = JSON.parse(source)
            if (!(parsed instanceof Array) || parsed.length == 0) return false
            const first = parsed[0] as any
            return first != null && first.name != null && first.meaning != null && first.gender != null
        } catch (error) {
            return false
        }
    }
    /** 检查项目在后台团单里存成 detailText(JSON 数组);坏数据或空值按一行空项目处理。 */
    const parseCheckItems = (text: string): any[] => {
        const items: any[] = []
@@ -215,7 +230,9 @@
        otherRules.value = detail.purchaseNotice != null && detail.purchaseNotice.otherRules != null ? '' + detail.purchaseNotice.otherRules : ''
        coverImage.value = detail.coverUrl.length > 0 ? detail.coverUrl : '/static/product-sauerkraut-fish.jpg'
        detailImages.value = detail.productImages.length > 0 ? detail.productImages.slice() : []
        isHealthCheckMerchant.value = detail.merchantTypeCode == 'medical'
        // 体检医院优先看商家类型;后端还没带上 merchantTypeCode 时,用「商品详情就是检查项目 JSON」兜底识别,
        // 否则页面会退回通用表单,把 detail_text 的原始 JSON 直接摊在文本框里给商户看。
        isHealthCheckMerchant.value = detail.merchantTypeCode == 'medical' || looksLikeHealthCheckItems(detail.detailText)
        noticeValidPeriod.value = detail.purchaseNotice != null && detail.purchaseNotice.validPeriod != null ? '' + detail.purchaseNotice.validPeriod : ''
        checkItems.value = parseCheckItems(detail.detailText)
        const terms = parseReserveTerms(noticeValidPeriod.value)
talent-card-admin/src/api/admin.ts
@@ -629,6 +629,11 @@
  return http.get<AnyRecord, AnyRecord>(`/api/admin/ticket-packages/${packageId}`)
}
// 票包表单的「适用商家」下拉:走票包自己的只读接口,避免要求「商家查询」权限。
export function getTicketPackageMerchantOptions(params: AnyRecord = {}) {
  return http.get<AnyRecord[], AnyRecord[]>('/api/admin/ticket-packages/merchant-options', { params })
}
export function createTicketPackage(payload: AnyRecord) {
  return http.post<AnyRecord, AnyRecord>('/api/admin/ticket-packages', payload)
}
talent-card-admin/src/style.css
@@ -3694,6 +3694,9 @@
  color: #8a94a6;
  font-size: 12px;
  line-height: 18px;
  /* 图片只有一张时没有"移除"按钮,提示会落到 72px 的按钮列里被挤成"一字一行",
     所以这里固定让它跨满整行、单独一行显示。 */
  grid-column: 1 / -1;
}
/* 上传图片处的推荐尺寸提示:各处复用同一套样式,避免每个页面各写一份。 */
@@ -3701,6 +3704,26 @@
  color: #8a94a6;
  font-size: 12px;
  line-height: 18px;
  /* 独占一行,别被同一行的 flex/grid 挤成"一字一行":
     grid 里跨满所有列,flex 里占满整行(配合下面的 flex-wrap)。 */
  grid-column: 1 / -1;
  flex: 0 0 100%;
  width: 100%;
  max-width: 100%;
}
/* 提示所在的弹性容器要允许换行,否则 100% 宽度的提示会把整行撑出容器。 */
.admin-image-size-form-item .el-form-item__content {
  flex-wrap: wrap;
}
/* 不用逐页加类:凡是把提示直接放在 el-form-item 里的地方都自动允许换行。 */
.el-form-item__content:has(> .admin-image-size-tip) {
  flex-wrap: wrap;
}
.merchant-parking-address-actions {
  flex-wrap: wrap;
}
.equity-upload-row .el-button {
talent-card-admin/src/views/MerchantCreateView.vue
@@ -1117,8 +1117,9 @@
const fuelStores = ref<FuelStore[]>([])
// 加油服务详情页轮播图(后台维护,保存时整表替换)。
const fuelHeroImages = ref<Array<{ id?: number | string; imageUrl: string; enabled: boolean }>>([])
// 可配置详情页轮播图的商家类型:加油服务、子女托管、行政审批。
const supportsHeroImages = computed(() => isFuelMerchant.value || isChildcareMerchant.value || isApprovalMerchant.value)
// 可配置详情页轮播图的商家类型:加油服务、子女托管。
// 行政审批不再单独配「详情页图片」——人才端详情页顶部直接用商家封面图。
const supportsHeroImages = computed(() => isFuelMerchant.value || isChildcareMerchant.value)
const merchantHeroImageTip = computed(() => isFuelMerchant.value ? '加油服务' : (isChildcareMerchant.value ? '子女托管' : '行政审批'))
const activeFuelStore = ref<FuelStore>()
const schoolAddresses = ref<SchoolAddress[]>([])
talent-card-admin/src/views/MovieTicketView.vue
@@ -6,9 +6,9 @@
import {
  createTicketPackage,
  generateTicketPackageCodes,
  getAdminMerchants,
  getTicketPackageClaims,
  getTicketPackageCodes,
  getTicketPackageMerchantOptions,
  getTicketPackages,
  importTicketPackageCodes,
  updateTicketPackage,
@@ -180,12 +180,8 @@
async function loadMerchants() {
  try {
    const result = await getAdminMerchants({ page: 1, size: 200, type: '观影服务', status: 1 } as any)
    merchants.value = result.records || []
    if (!merchants.value.length) {
      const fallback = await getAdminMerchants({ page: 1, size: 200, keyword: '电影', status: 1 } as any)
      merchants.value = fallback.records || []
    }
    // 走票包自己的只读接口:票包运营账号不一定有「商家查询」权限,用原来的商家列表接口会整页报错。
    merchants.value = (await getTicketPackageMerchantOptions()) || []
  } catch {
    merchants.value = []
  }
@@ -585,8 +581,9 @@
                <el-image v-if="form.coverUrl" class="movie-ticket-cover-preview" :src="form.coverUrl" fit="cover" />
                <div v-else class="movie-ticket-cover-placeholder"><span>+</span><small>上传图片</small></div>
              </el-upload>
              <div class="admin-image-size-tip">推荐尺寸 150 × 150 px(小程序电影券列表缩略图,1:1 方图)</div>
            </div>
            <!-- 提示放在行外:封面图那一行只有 58+160px,放里面会被挤成"一字一行"。 -->
            <div class="admin-image-size-tip">推荐尺寸 150 × 150 px(小程序电影券列表缩略图,1:1 方图)</div>
          </el-form>
          <h3>人才权益</h3>
talent-card-admin/src/views/TalentNewsEditView.vue
@@ -145,7 +145,7 @@
          <el-switch v-model="form.pinned" />
        </el-form-item>
        <el-form-item label="封面图" prop="coverUrl" required>
        <el-form-item class="admin-image-size-form-item" label="封面图" prop="coverUrl" required>
          <el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
            <el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" />
            <div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div>
talent-card-admin/src/views/TalentNewsPublishView.vue
@@ -98,7 +98,7 @@
          <el-switch v-model="form.pinned" />
        </el-form-item>
        <el-form-item label="封面图" prop="coverUrl" required>
        <el-form-item class="admin-image-size-form-item" label="封面图" prop="coverUrl" required>
          <el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover">
            <el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" />
            <div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div>
talent-card-admin/src/views/TalentPolicyPublishView.vue
@@ -54,7 +54,7 @@
    <el-form-item label="政策标题" prop="title" required><el-input v-model="form.title" placeholder="请输入政策标题" /></el-form-item>
    <el-form-item label="政策标签" prop="subtitle" required><el-input v-model="form.subtitle" placeholder="请输入政策标签" /></el-form-item>
    <el-form-item class="news-editor-form-item" label="政策内容" prop="content" required><AdminRichTextEditor v-model="form.content" @update:model-value="validateContent" /></el-form-item>
    <el-form-item label="封面图" prop="coverUrl" required><el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" /><div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div></el-upload><div class="admin-image-size-tip">推荐尺寸 750 × 320 px(小程序政策详情页顶部大图)</div></el-form-item>
    <el-form-item class="admin-image-size-form-item" label="封面图" prop="coverUrl" required><el-upload class="article-cover-upload" accept="image/*" :auto-upload="false" :show-file-list="false" :on-change="uploadCover"><el-image v-if="form.coverUrl" class="article-cover-preview" :src="form.coverUrl" fit="cover" /><div v-else class="article-cover-upload-card"><span>+</span><small>上传图片</small></div></el-upload><div class="admin-image-size-tip">推荐尺寸 750 × 320 px(小程序政策详情页顶部大图)</div></el-form-item>
    <el-form-item label="是否置顶"><el-switch v-model="form.isTop" /></el-form-item>
    <el-form-item label="排序" prop="sortNo" required><el-input v-model.number="form.sortNo" type="number" placeholder="请输入非负整数" /></el-form-item>
    <el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
talent-card-server/cp-verify.txt
New file
@@ -0,0 +1 @@
C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-actuator\4.1.0\spring-boot-starter-actuator-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter\4.1.0\spring-boot-starter-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-logging\4.1.0\spring-boot-starter-logging-4.1.0.jar;C:\Users\Sasuke\.m2\repository\ch\qos\logback\logback-classic\1.5.34\logback-classic-1.5.34.jar;C:\Users\Sasuke\.m2\repository\ch\qos\logback\logback-core\1.5.34\logback-core-1.5.34.jar;C:\Users\Sasuke\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.25.4\log4j-to-slf4j-2.25.4.jar;C:\Users\Sasuke\.m2\repository\org\slf4j\jul-to-slf4j\2.0.18\jul-to-slf4j-2.0.18.jar;C:\Users\Sasuke\.m2\repository\jakarta\annotation\jakarta.annotation-api\3.0.0\jakarta.annotation-api-3.0.0.jar;C:\Users\Sasuke\.m2\repository\org\yaml\snakeyaml\2.6\snakeyaml-2.6.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-micrometer-metrics\4.1.0\spring-boot-starter-micrometer-metrics-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-micrometer-metrics\4.1.0\spring-boot-micrometer-metrics-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-micrometer-observation\4.1.0\spring-boot-micrometer-observation-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-actuator-autoconfigure\4.1.0\spring-boot-actuator-autoconfigure-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-actuator\4.1.0\spring-boot-actuator-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-health\4.1.0\spring-boot-health-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot\4.1.0\spring-boot-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-context\7.0.8\spring-context-7.0.8.jar;C:\Users\Sasuke\.m2\repository\io\micrometer\micrometer-observation\1.17.0\micrometer-observation-1.17.0.jar;C:\Users\Sasuke\.m2\repository\org\jspecify\jspecify\1.0.0\jspecify-1.0.0.jar;C:\Users\Sasuke\.m2\repository\io\micrometer\micrometer-commons\1.17.0\micrometer-commons-1.17.0.jar;C:\Users\Sasuke\.m2\repository\io\micrometer\micrometer-jakarta9\1.17.0\micrometer-jakarta9-1.17.0.jar;C:\Users\Sasuke\.m2\repository\io\micrometer\micrometer-core\1.17.0\micrometer-core-1.17.0.jar;C:\Users\Sasuke\.m2\repository\org\hdrhistogram\HdrHistogram\2.2.2\HdrHistogram-2.2.2.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-data-jpa\4.1.0\spring-boot-starter-data-jpa-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-jdbc\4.1.0\spring-boot-starter-jdbc-4.1.0.jar;C:\Users\Sasuke\.m2\repository\com\zaxxer\HikariCP\7.0.2\HikariCP-7.0.2.jar;C:\Users\Sasuke\.m2\repository\org\slf4j\slf4j-api\2.0.18\slf4j-api-2.0.18.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-data-jpa\4.1.0\spring-boot-data-jpa-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-data-commons\4.1.0\spring-boot-data-commons-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-persistence\4.1.0\spring-boot-persistence-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\data\spring-data-commons\4.1.0\spring-data-commons-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-hibernate\4.1.0\spring-boot-hibernate-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-jpa\4.1.0\spring-boot-jpa-4.1.0.jar;C:\Users\Sasuke\.m2\repository\jakarta\persistence\jakarta.persistence-api\3.2.0\jakarta.persistence-api-3.2.0.jar;C:\Users\Sasuke\.m2\repository\org\hibernate\orm\hibernate-core\7.4.1.Final\hibernate-core-7.4.1.Final.jar;C:\Users\Sasuke\.m2\repository\jakarta\transaction\jakarta.transaction-api\2.0.1\jakarta.transaction-api-2.0.1.jar;C:\Users\Sasuke\.m2\repository\org\hibernate\models\hibernate-models\1.1.1\hibernate-models-1.1.1.jar;C:\Users\Sasuke\.m2\repository\org\glassfish\jaxb\jaxb-runtime\4.0.9\jaxb-runtime-4.0.9.jar;C:\Users\Sasuke\.m2\repository\org\glassfish\jaxb\jaxb-core\4.0.9\jaxb-core-4.0.9.jar;C:\Users\Sasuke\.m2\repository\org\eclipse\angus\angus-activation\2.0.3\angus-activation-2.0.3.jar;C:\Users\Sasuke\.m2\repository\org\glassfish\jaxb\txw2\4.0.9\txw2-4.0.9.jar;C:\Users\Sasuke\.m2\repository\com\sun\istack\istack-commons-runtime\4.1.2\istack-commons-runtime-4.1.2.jar;C:\Users\Sasuke\.m2\repository\jakarta\inject\jakarta.inject-api\2.0.1\jakarta.inject-api-2.0.1.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-orm\7.0.8\spring-orm-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\data\spring-data-jpa\4.1.0\spring-data-jpa-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-aop\7.0.8\spring-aop-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-tx\7.0.8\spring-tx-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\antlr\antlr4-runtime\4.13.2\antlr4-runtime-4.13.2.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-aspects\7.0.8\spring-aspects-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\aspectj\aspectjweaver\1.9.25.1\aspectjweaver-1.9.25.1.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-jdbc\4.1.0\spring-boot-jdbc-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-sql\4.1.0\spring-boot-sql-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-transaction\4.1.0\spring-boot-transaction-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-jdbc\7.0.8\spring-jdbc-7.0.8.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-spring-boot4-starter\3.5.17\mybatis-plus-spring-boot4-starter-3.5.17.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus\3.5.17\mybatis-plus-3.5.17.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-core\3.5.17\mybatis-plus-core-3.5.17.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-annotation\3.5.17\mybatis-plus-annotation-3.5.17.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-spring\3.5.17\mybatis-plus-spring-3.5.17.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-extension\3.5.17\mybatis-plus-extension-3.5.17.jar;C:\Users\Sasuke\.m2\repository\org\mybatis\mybatis\3.5.19\mybatis-3.5.19.jar;C:\Users\Sasuke\.m2\repository\org\mybatis\mybatis-spring\4.0.0\mybatis-spring-4.0.0.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-spring-boot-autoconfigure\3.5.17\mybatis-plus-spring-boot-autoconfigure-3.5.17.jar;C:\Users\Sasuke\.m2\repository\com\baomidou\mybatis-plus-spring-boot-native-image\3.5.17\mybatis-plus-spring-boot-native-image-3.5.17.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\4.1.0\spring-boot-autoconfigure-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springdoc\springdoc-openapi-starter-webmvc-ui\3.1.0\springdoc-openapi-starter-webmvc-ui-3.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springdoc\springdoc-openapi-starter-webmvc-api\3.1.0\springdoc-openapi-starter-webmvc-api-3.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springdoc\springdoc-openapi-starter-common\3.1.0\springdoc-openapi-starter-common-3.1.0.jar;C:\Users\Sasuke\.m2\repository\io\swagger\core\v3\swagger-core-jakarta\2.2.52\swagger-core-jakarta-2.2.52.jar;C:\Users\Sasuke\.m2\repository\io\swagger\core\v3\swagger-annotations-jakarta\2.2.52\swagger-annotations-jakarta-2.2.52.jar;C:\Users\Sasuke\.m2\repository\io\swagger\core\v3\swagger-models-jakarta\2.2.52\swagger-models-jakarta-2.2.52.jar;C:\Users\Sasuke\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.21.4\jackson-databind-2.21.4.jar;C:\Users\Sasuke\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.21.4\jackson-core-2.21.4.jar;C:\Users\Sasuke\.m2\repository\com\fasterxml\jackson\dataformat\jackson-dataformat-yaml\2.21.4\jackson-dataformat-yaml-2.21.4.jar;C:\Users\Sasuke\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.21.4\jackson-datatype-jsr310-2.21.4.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-web-server\4.1.0\spring-boot-web-server-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\webjars\swagger-ui\5.32.11\swagger-ui-5.32.11.jar;C:\Users\Sasuke\.m2\repository\org\webjars\webjars-locator-lite\1.1.3\webjars-locator-lite-1.1.3.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-data-redis\4.1.0\spring-boot-starter-data-redis-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-data-redis\4.1.0\spring-boot-data-redis-4.1.0.jar;C:\Users\Sasuke\.m2\repository\io\lettuce\lettuce-core\7.5.2.RELEASE\lettuce-core-7.5.2.RELEASE.jar;C:\Users\Sasuke\.m2\repository\redis\clients\authentication\redis-authx-core\0.1.1-beta2\redis-authx-core-0.1.1-beta2.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-common\4.2.15.Final\netty-common-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-handler\4.2.15.Final\netty-handler-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-resolver\4.2.15.Final\netty-resolver-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-buffer\4.2.15.Final\netty-buffer-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-transport-native-unix-common\4.2.15.Final\netty-transport-native-unix-common-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-codec-base\4.2.15.Final\netty-codec-base-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-transport\4.2.15.Final\netty-transport-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\projectreactor\reactor-core\3.8.6\reactor-core-3.8.6.jar;C:\Users\Sasuke\.m2\repository\org\reactivestreams\reactive-streams\1.0.4\reactive-streams-1.0.4.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-resolver-dns\4.2.15.Final\netty-resolver-dns-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\io\netty\netty-codec-dns\4.2.15.Final\netty-codec-dns-4.2.15.Final.jar;C:\Users\Sasuke\.m2\repository\org\springframework\data\spring-data-redis\4.1.0\spring-data-redis-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\data\spring-data-keyvalue\4.1.0\spring-data-keyvalue-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-oxm\7.0.8\spring-oxm-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-context-support\7.0.8\spring-context-support-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-netty\4.1.0\spring-boot-netty-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-messaging\7.0.8\spring-messaging-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-beans\7.0.8\spring-beans-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-core\7.0.8\spring-core-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-flyway\4.1.0\spring-boot-starter-flyway-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-flyway\4.1.0\spring-boot-flyway-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-validation\4.1.0\spring-boot-starter-validation-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-validation\4.1.0\spring-boot-validation-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\11.0.22\tomcat-embed-el-11.0.22.jar;C:\Users\Sasuke\.m2\repository\org\hibernate\validator\hibernate-validator\9.1.0.Final\hibernate-validator-9.1.0.Final.jar;C:\Users\Sasuke\.m2\repository\jakarta\validation\jakarta.validation-api\3.1.1\jakarta.validation-api-3.1.1.jar;C:\Users\Sasuke\.m2\repository\org\jboss\logging\jboss-logging\3.6.3.Final\jboss-logging-3.6.3.Final.jar;C:\Users\Sasuke\.m2\repository\com\fasterxml\classmate\1.7.3\classmate-1.7.3.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-webmvc\4.1.0\spring-boot-starter-webmvc-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-jackson\4.1.0\spring-boot-starter-jackson-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-jackson\4.1.0\spring-boot-jackson-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\4.1.0\spring-boot-starter-tomcat-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat-runtime\4.1.0\spring-boot-starter-tomcat-runtime-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\11.0.22\tomcat-embed-core-11.0.22.jar;C:\Users\Sasuke\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\11.0.22\tomcat-embed-websocket-11.0.22.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-tomcat\4.1.0\spring-boot-tomcat-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-http-converter\4.1.0\spring-boot-http-converter-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-web\7.0.8\spring-web-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-webmvc\4.1.0\spring-boot-webmvc-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-servlet\4.1.0\spring-boot-servlet-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-webmvc\7.0.8\spring-webmvc-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-expression\7.0.8\spring-expression-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\flywaydb\flyway-mysql\12.4.0\flyway-mysql-12.4.0.jar;C:\Users\Sasuke\.m2\repository\org\flywaydb\flyway-core\12.4.0\flyway-core-12.4.0.jar;C:\Users\Sasuke\.m2\repository\tools\jackson\core\jackson-databind\3.1.4\jackson-databind-3.1.4.jar;C:\Users\Sasuke\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.21\jackson-annotations-2.21.jar;C:\Users\Sasuke\.m2\repository\tools\jackson\core\jackson-core\3.1.4\jackson-core-3.1.4.jar;C:\Users\Sasuke\.m2\repository\com\mysql\mysql-connector-j\9.7.0\mysql-connector-j-9.7.0.jar;C:\Users\Sasuke\.m2\repository\org\jsoup\jsoup\1.18.3\jsoup-1.18.3.jar;C:\Users\Sasuke\.m2\repository\com\google\zxing\core\3.5.3\core-3.5.3.jar;C:\Users\Sasuke\.m2\repository\com\google\zxing\javase\3.5.3\javase-3.5.3.jar;C:\Users\Sasuke\.m2\repository\com\beust\jcommander\1.82\jcommander-1.82.jar;C:\Users\Sasuke\.m2\repository\com\github\jai-imageio\jai-imageio-core\1.4.0\jai-imageio-core-1.4.0.jar;C:\Users\Sasuke\.m2\repository\org\apache\poi\poi-ooxml\5.4.1\poi-ooxml-5.4.1.jar;C:\Users\Sasuke\.m2\repository\org\apache\poi\poi\5.4.1\poi-5.4.1.jar;C:\Users\Sasuke\.m2\repository\commons-codec\commons-codec\1.21.0\commons-codec-1.21.0.jar;C:\Users\Sasuke\.m2\repository\org\apache\commons\commons-math3\3.6.1\commons-math3-3.6.1.jar;C:\Users\Sasuke\.m2\repository\com\zaxxer\SparseBitSet\1.3\SparseBitSet-1.3.jar;C:\Users\Sasuke\.m2\repository\org\apache\poi\poi-ooxml-lite\5.4.1\poi-ooxml-lite-5.4.1.jar;C:\Users\Sasuke\.m2\repository\org\apache\xmlbeans\xmlbeans\5.3.0\xmlbeans-5.3.0.jar;C:\Users\Sasuke\.m2\repository\org\apache\commons\commons-compress\1.27.1\commons-compress-1.27.1.jar;C:\Users\Sasuke\.m2\repository\commons-io\commons-io\2.18.0\commons-io-2.18.0.jar;C:\Users\Sasuke\.m2\repository\com\github\virtuald\curvesapi\1.08\curvesapi-1.08.jar;C:\Users\Sasuke\.m2\repository\org\apache\logging\log4j\log4j-api\2.25.4\log4j-api-2.25.4.jar;C:\Users\Sasuke\.m2\repository\org\apache\commons\commons-collections4\4.4\commons-collections4-4.4.jar;C:\Users\Sasuke\.m2\repository\com\huifu\bspay\sdk\dg-java-sdk\3.0.40\dg-java-sdk-3.0.40.jar;C:\Users\Sasuke\.m2\repository\org\apache\httpcomponents\httpclient\4.5.2\httpclient-4.5.2.jar;C:\Users\Sasuke\.m2\repository\org\apache\httpcomponents\httpcore\4.4.16\httpcore-4.4.16.jar;C:\Users\Sasuke\.m2\repository\commons-logging\commons-logging\1.3.6\commons-logging-1.3.6.jar;C:\Users\Sasuke\.m2\repository\org\apache\httpcomponents\httpmime\4.5.2\httpmime-4.5.2.jar;C:\Users\Sasuke\.m2\repository\org\apache\commons\commons-lang3\3.20.0\commons-lang3-3.20.0.jar;C:\Users\Sasuke\.m2\repository\com\squareup\okhttp3\okhttp\4.9.1\okhttp-4.9.1.jar;C:\Users\Sasuke\.m2\repository\com\squareup\okio\okio\2.8.0\okio-2.8.0.jar;C:\Users\Sasuke\.m2\repository\org\jetbrains\kotlin\kotlin-stdlib-common\1.4.0\kotlin-stdlib-common-1.4.0.jar;C:\Users\Sasuke\.m2\repository\org\jetbrains\kotlin\kotlin-stdlib\2.3.21\kotlin-stdlib-2.3.21.jar;C:\Users\Sasuke\.m2\repository\org\jetbrains\annotations\13.0\annotations-13.0.jar;C:\Users\Sasuke\.m2\repository\com\alibaba\fastjson\1.2.83\fastjson-1.2.83.jar;C:\Users\Sasuke\.m2\repository\org\projectlombok\lombok\1.18.46\lombok-1.18.46.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-actuator-test\4.1.0\spring-boot-starter-actuator-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-test\4.1.0\spring-boot-starter-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-test\4.1.0\spring-boot-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-test-autoconfigure\4.1.0\spring-boot-test-autoconfigure-4.1.0.jar;C:\Users\Sasuke\.m2\repository\com\jayway\jsonpath\json-path\2.10.0\json-path-2.10.0.jar;C:\Users\Sasuke\.m2\repository\jakarta\xml\bind\jakarta.xml.bind-api\4.0.5\jakarta.xml.bind-api-4.0.5.jar;C:\Users\Sasuke\.m2\repository\jakarta\activation\jakarta.activation-api\2.1.4\jakarta.activation-api-2.1.4.jar;C:\Users\Sasuke\.m2\repository\net\minidev\json-smart\2.6.0\json-smart-2.6.0.jar;C:\Users\Sasuke\.m2\repository\net\minidev\accessors-smart\2.6.0\accessors-smart-2.6.0.jar;C:\Users\Sasuke\.m2\repository\org\ow2\asm\asm\9.7.1\asm-9.7.1.jar;C:\Users\Sasuke\.m2\repository\org\assertj\assertj-core\3.27.7\assertj-core-3.27.7.jar;C:\Users\Sasuke\.m2\repository\net\bytebuddy\byte-buddy\1.18.10\byte-buddy-1.18.10.jar;C:\Users\Sasuke\.m2\repository\org\awaitility\awaitility\4.3.0\awaitility-4.3.0.jar;C:\Users\Sasuke\.m2\repository\org\hamcrest\hamcrest\3.0\hamcrest-3.0.jar;C:\Users\Sasuke\.m2\repository\org\junit\jupiter\junit-jupiter\6.0.3\junit-jupiter-6.0.3.jar;C:\Users\Sasuke\.m2\repository\org\junit\jupiter\junit-jupiter-api\6.0.3\junit-jupiter-api-6.0.3.jar;C:\Users\Sasuke\.m2\repository\org\opentest4j\opentest4j\1.3.0\opentest4j-1.3.0.jar;C:\Users\Sasuke\.m2\repository\org\junit\platform\junit-platform-commons\6.0.3\junit-platform-commons-6.0.3.jar;C:\Users\Sasuke\.m2\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;C:\Users\Sasuke\.m2\repository\org\junit\jupiter\junit-jupiter-params\6.0.3\junit-jupiter-params-6.0.3.jar;C:\Users\Sasuke\.m2\repository\org\junit\jupiter\junit-jupiter-engine\6.0.3\junit-jupiter-engine-6.0.3.jar;C:\Users\Sasuke\.m2\repository\org\junit\platform\junit-platform-engine\6.0.3\junit-platform-engine-6.0.3.jar;C:\Users\Sasuke\.m2\repository\org\mockito\mockito-core\5.23.0\mockito-core-5.23.0.jar;C:\Users\Sasuke\.m2\repository\net\bytebuddy\byte-buddy-agent\1.18.10\byte-buddy-agent-1.18.10.jar;C:\Users\Sasuke\.m2\repository\org\objenesis\objenesis\3.3\objenesis-3.3.jar;C:\Users\Sasuke\.m2\repository\org\mockito\mockito-junit-jupiter\5.23.0\mockito-junit-jupiter-5.23.0.jar;C:\Users\Sasuke\.m2\repository\org\skyscreamer\jsonassert\1.5.3\jsonassert-1.5.3.jar;C:\Users\Sasuke\.m2\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;C:\Users\Sasuke\.m2\repository\org\springframework\spring-test\7.0.8\spring-test-7.0.8.jar;C:\Users\Sasuke\.m2\repository\org\xmlunit\xmlunit-core\2.11.0\xmlunit-core-2.11.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-micrometer-metrics-test\4.1.0\spring-boot-starter-micrometer-metrics-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-micrometer-metrics-test\4.1.0\spring-boot-micrometer-metrics-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\io\micrometer\micrometer-observation-test\1.17.0\micrometer-observation-test-1.17.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-data-jpa-test\4.1.0\spring-boot-starter-data-jpa-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-jdbc-test\4.1.0\spring-boot-starter-jdbc-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-jdbc-test\4.1.0\spring-boot-jdbc-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-data-jpa-test\4.1.0\spring-boot-data-jpa-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-jpa-test\4.1.0\spring-boot-jpa-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-data-redis-test\4.1.0\spring-boot-starter-data-redis-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-data-redis-test\4.1.0\spring-boot-data-redis-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-flyway-test\4.1.0\spring-boot-starter-flyway-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-validation-test\4.1.0\spring-boot-starter-validation-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-webmvc-test\4.1.0\spring-boot-starter-webmvc-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-starter-jackson-test\4.1.0\spring-boot-starter-jackson-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-webmvc-test\4.1.0\spring-boot-webmvc-test-4.1.0.jar;C:\Users\Sasuke\.m2\repository\org\springframework\boot\spring-boot-resttestclient\4.1.0\spring-boot-resttestclient-4.1.0.jar;C:\Users\Sasuke\.m2\repository\com\h2database\h2\2.4.240\h2-2.4.240.jar
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminBusinessService.java
@@ -747,7 +747,9 @@
    }
    /** 详情页轮播图按商家配置的服务类型:其余类型顶部图仍用封面图或团单封面。 */
    private static final List<String> HERO_IMAGE_MERCHANT_TYPES = List.of("children", "approval");
    // 只有子女托管走通用的 merchant_hero_image;加油服务用 transport_fuel_hero_image。
    // 行政审批不再单独配详情页图片(人才端直接用商家封面图),所以不在这里。
    private static final List<String> HERO_IMAGE_MERCHANT_TYPES = List.of("children");
    /**
     * 同步商家详情页轮播图。
talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java
@@ -1,6 +1,7 @@
package com.talentcard.server.business.admin;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import com.talentcard.server.business.PageResult;
@@ -58,6 +59,14 @@
        return ApiResponse.success(service.packageDetail(packageId));
    }
    @Operation(summary = "票包适用商家选项", description = "票包表单的「适用商家」下拉,只返回启用的观影服务商家")
    @GetMapping("/merchant-options")
    public ApiResponse<List<Map<String, Object>>> merchantOptions(
            @Parameter(description = "商家名称关键词") @RequestParam(required = false) String keyword
    ) {
        return ApiResponse.success(service.merchantOptions(keyword));
    }
    @Operation(summary = "查询票包库存", description = "查看指定票包兑换码总量、可发放、锁定、已发放、已使用和作废数量")
    @GetMapping("/{packageId}/stock")
    public ApiResponse<Map<String, Object>> stock(@Parameter(description = "票包ID") @PathVariable Long packageId) {
talent-card-server/src/main/java/com/talentcard/server/business/auth/TalentAuthService.java
@@ -125,11 +125,17 @@
                """, openId);
        if (!binding.isEmpty() && number(binding.get(0).get("id")) != talentId) {
            long previousTalentId = number(binding.get(0).get("id"));
            jdbc.update("update talent_user set openid = null where id = ?", previousTalentId);
            jdbc.update("update talent_session set revoked_at = coalesce(revoked_at, now()) where talent_id = ? and revoked_at is null", previousTalentId);
        }
        // uk_openid 是唯一索引:已软删/停用的历史账号照样占着这个 openid,不先摘掉就会撞唯一键(接口报「服务器内部错误」)。
        releaseOpenId(openId, talentId);
        jdbc.update("update talent_user set openid = ? where id = ?", openId, talentId);
        return issueSession(talentId);
    }
    /** 把 openid 从除 keepTalentId 之外的所有行上摘掉(含软删、停用的历史账号)。 */
    private void releaseOpenId(String openId, long keepTalentId) {
        jdbc.update("update talent_user set openid = null where openid = ? and id <> ?", openId, keepTalentId);
    }
    private long registerVisitor(String openId, String mobile) {
@@ -144,6 +150,8 @@
            jdbc.update("update talent_user set mobile = ? where id = ?", mobile, talentId);
            return talentId;
        }
        // 没有可用的绑定时要把历史占用清掉,否则下面这条 insert 会撞 uk_openid。
        jdbc.update("update talent_user set openid = null where openid = ?", openId);
        jdbc.update("""
                insert into talent_user(openid, name, mobile, talent_level, source_area_type, status)
                values (?, '普通游客', ?, '普通游客', 'LOCAL', 1)
talent-card-server/src/main/java/com/talentcard/server/business/merchant/ManualConsumptionService.java
@@ -45,7 +45,8 @@
        result.put("cardLevel", string(source.get("cardLevel")));
        List<Map<String, Object>> matchedRules = rules(principal.merchantId(), string(source.get("cardLevel")), string(source.get("sourceAreaType")));
        if (matchedRules.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "当前商户未配置适用于该人才卡的到店消费权益");
        result.put("benefitText", benefitText(principal.merchantId(), string(source.get("cardLevel")), string(source.get("sourceAreaType"))));
        result.put("benefitText", benefitTextForLevel(principal.merchantId(), string(source.get("cardLevel")),
                string(source.get("sourceAreaType")), matchedRules));
        result.put("rules", matchedRules);
        result.put("coupons", coupons(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel"))));
        result.put("expiresAt", source.get("expiresAt"));
@@ -248,7 +249,6 @@
                        && configuredStoreConsumptionEnabled(right)
                        && matchesConfiguredLevel(string(right.get("level")), cardLevel, sourceAreaType))
                .map(this::configuredRule)
                .filter(rule -> decimal(rule.get("discountRate")).signum() > 0 || decimal(rule.get("fixedReduction")).signum() > 0)
                .toList();
    }
@@ -271,6 +271,9 @@
        result.put("merchantDetail", right.get("detail"));
        result.put("consumeDesc", right.get("consumeDesc"));
        result.put("benefitText", configuredBenefitText(right));
        // 到店消费开着但没配折扣(子女托管、文化服务常见)时也要能发单:按原价收,标签上写明。
        // 以前这类规则被过滤掉,页面上价格全是 0、发送订单永远点不动。
        if (string(result.get("benefitText")).isBlank()) result.put("benefitText", "按原价支付");
        return result;
    }
@@ -450,8 +453,11 @@
            List<Map<String, Object>> configs = rightsConfigs(merchantId).stream()
                    .filter(right -> booleanValue(right.get("enabled")))
                    .toList();
            return firstRightsText(configs, cardLevel, sourceAreaType,
            String configuredText = firstRightsText(configs, cardLevel, sourceAreaType,
                    "culture".equalsIgnoreCase(merchantType) ? "consumeDesc" : "detail");
            // 没填权益描述时也要返回兜底文案:调用方会据此换成规则自己的权益(如「按原价支付」),
            // 返回空串会让卡片上的「当前权益」整行空着。
            return configuredText.isBlank() ? DEFAULT_BENEFIT_TEXT : configuredText;
        }
        String configuredDescription = consumeDescFromRights(merchantId, cardLevel, sourceAreaType);
        if (!configuredDescription.isBlank()) return configuredDescription;
@@ -460,7 +466,38 @@
                where merchant_id = ? and status = 1 order by sort_no desc, id desc
                """, merchantId);
        return benefits.stream().filter(benefit -> matchesCardLevel(string(benefit.get("cardLevel")), cardLevel))
                .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准");
                .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse(DEFAULT_BENEFIT_TEXT);
    }
    /** 后台没给该人才等级填权益描述时的兜底:页面上至少要显示这条规则自己的权益文案。 */
    private static final String DEFAULT_BENEFIT_TEXT = "以当前商户到店消费规则为准";
    /**
     * 「当前权益」文案:优先用后台按人才等级配置的权益描述;没配置时用匹配到的规则文案
     * (如「到店消费8折」)顶上,别给商户看「以当前商户到店消费规则为准」这种没有信息量的兜底话术。
     */
    private String benefitTextForLevel(Long merchantId, String cardLevel, String sourceAreaType,
                                       List<Map<String, Object>> matchedRules) {
        String configured = benefitText(merchantId, cardLevel, sourceAreaType);
        if (!DEFAULT_BENEFIT_TEXT.equals(configured)) return configured;
        for (Map<String, Object> rule : matchedRules) {
            String text = ruleBenefitText(rule);
            if (!text.isBlank()) return text;
        }
        return configured;
    }
    /** 规则自身的权益文案:结构化配置里已经算好了;旧的门店规则表只有名字和折扣,按折扣拼一句。 */
    private String ruleBenefitText(Map<String, Object> rule) {
        String configured = string(rule.get("benefitText"));
        if (!configured.isBlank()) return configured;
        String name = string(rule.get("name"));
        if (!name.isBlank() && (name.contains("折") || name.contains("减"))) return name;
        BigDecimal rate = decimal(rule.get("discountRate"));
        if (rate.signum() > 0) return "到店消费" + rate.movePointRight(1).stripTrailingZeros().toPlainString() + "折";
        BigDecimal reduction = decimal(rule.get("fixedReduction"));
        if (reduction.signum() > 0) return "到店消费立减" + reduction.stripTrailingZeros().toPlainString() + "元";
        return name;
    }
    private String consumeDescFromRights(Long merchantId, String cardLevel, String sourceAreaType) {
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
@@ -336,9 +336,12 @@
    }
    private Map<String, Object> findFreeStayCode(String token, MerchantPrincipal principal) {
        // source_area_type 必须一起选:酒店按「本地/外地」分等级配置时,确认页要显示的
        // 「超过天数按门市价X折」就是靠它去匹配对应等级的折扣,漏了会拿不到折扣文案。
        List<Map<String, Object>> codes = rows("""
                select c.id free_stay_verification_code_id, c.expires_at, v.total_count, v.used_count, v.valid_end,
                       v.status voucher_status, v.merchant_id, t.name talent_name, t.mobile talent_mobile, tc.card_level
                       v.status voucher_status, v.merchant_id, t.name talent_name, t.mobile talent_mobile, tc.card_level,
                       t.source_area_type
                from free_stay_verification_code c
                join talent_free_stay_voucher v on v.id = c.voucher_id
                join talent_user t on t.id = v.talent_id and t.deleted = 0
talent-card-server/src/main/java/com/talentcard/server/business/portal/AdminApprovalPortalService.java
@@ -34,9 +34,10 @@
        result.put("serviceId", serviceId);
        result.put("title", benefit.get("title"));
        result.put("freeLabel", benefit.get("freeLabel"));
        // 详情页图优先用该商家自己配置的轮播图,没配时先回落商家封面图,再回落服务权益里的全局图。
        result.put("heroImage", merchantHeroImage(serviceId, text(merchant.get("coverUrl"), text(benefit.get("heroImage")))));
        result.put("heroImages", merchantHeroImages(serviceId));
        // 行政审批详情页顶部直接用商家封面图(后台不再单独配置「详情页图片」);
        // 商家没配封面时再回落服务权益里的全局图。
        result.put("heroImage", text(merchant.get("coverUrl"), text(benefit.get("heroImage"))));
        result.put("heroImages", List.of());
        result.put("available", number(benefit.get("enabled")) == 1 && number(merchant.get("status")) == 1);
        result.put("unavailableReason", text(benefit.get("unavailableReason"), "该权益暂未开放"));
        List<Map<String, Object>> configuredOrganizations = configuredOrganizations(merchant);
@@ -44,23 +45,6 @@
                ? organizations(((Number) benefit.get("id")).longValue())
                : configuredOrganizations);
        return result;
    }
    private List<String> merchantHeroImages(Long merchantId) {
        return query("""
                select image_url
                from merchant_hero_image
                where merchant_id = ? and deleted = 0 and enabled = 1
                order by sort_no asc, id asc
                """, merchantId).stream()
                .map(row -> text(row.get("imageUrl")))
                .filter(url -> !url.isBlank())
                .toList();
    }
    private String merchantHeroImage(Long merchantId, String fallback) {
        List<String> images = merchantHeroImages(merchantId);
        return images.isEmpty() ? fallback : images.get(0);
    }
    private List<Map<String, Object>> configuredOrganizations(Map<String, Object> merchant) {
talent-card-server/src/main/java/com/talentcard/server/business/portal/MedicalPackagePortalService.java
@@ -47,6 +47,11 @@
        result.put("packageId", row.get("packageId"));
        result.put("name", row.get("packageName"));
        result.put("heroImage", detailHeroImage(row));
        // 商户在「图文详情」里可以传多张:顶部大图取第一张,剩下的(含第一张)在这里整组返回,
        // 详情页按顺序铺出来,否则商户传了几张也只能看到一张。
        result.put("detailImages", imageUrls(row.get("productImages")).stream()
                .filter(url -> !url.isBlank())
                .toList());
        result.put("free", number(row.get("freeStatus")) == 1);
        result.put("freeLabel", number(row.get("freeStatus")) == 1 ? "免费" : "优惠");
        result.put("applicableCards", row.get("applicableCards"));
talent-card-server/src/main/java/com/talentcard/server/business/portal/PortalService.java
@@ -926,11 +926,16 @@
            // 标签只用后台配的:先取商家权益里第一个启用等级的标签,再退回商家服务标签;
            // 都没配就留空(后台标签是必填项,不再兜底"人才专享")。
            String benefitText = tagVisible ? configuredMerchantTag(row.get("merchantRightsConfig"), row.get("serviceLabel")) : "";
            // 折扣是单独配置的字段(子女托管、文化服务这类商家的「标签」是文字),列表里两个都要摆出来。
            String discountText = tagVisible ? configuredMerchantDiscount(row.get("merchantRightsConfig")) : "";
            List<String> tags = new ArrayList<>();
            if (!benefitText.isBlank()) tags.add(benefitText);
            if (!discountText.isBlank()) tags.add(discountText);
            // 没有标签文本时把 visible 置 false,小程序就不会渲染空标签块。
            boolean tagPresent = tagVisible && !benefitText.isBlank();
            boolean tagPresent = tagVisible && !tags.isEmpty();
            item.put("benefitText", benefitText);
            item.put("discountText", benefitText);
            item.put("benefitTags", tagPresent ? List.of(benefitText) : List.of());
            item.put("discountText", discountText);
            item.put("benefitTags", tagPresent ? tags : List.of());
            item.put("benefitTagVisible", tagPresent);
            item.remove("merchantRightsConfig");
            return item;
@@ -948,6 +953,30 @@
        return textOrDefault(serviceLabel, "");
    }
    /**
     * 列表上的折扣标签:取商家权益里**启用等级中最低的到店消费折扣**(如「8折」)。
     *
     * <p>子女托管、文化服务这类商家把「标签」配成文字(如「卓越人才卡」),折扣在另一个字段里;
     * 列表只看标签的话,后台明明配了折扣也看不到。
     */
    String configuredMerchantDiscount(Object rightsConfig) {
        BigDecimal lowest = null;
        for (Map<String, Object> right : merchantRightsConfigs(rightsConfig)) {
            if (!booleanValue(right.get("enabled"))) continue;
            String raw = textOrDefault(right.get("discount"), "").trim();
            if (raw.isEmpty()) continue;
            BigDecimal discount;
            try {
                discount = new BigDecimal(raw);
            } catch (NumberFormatException ignored) {
                continue;
            }
            if (discount.signum() <= 0 || discount.compareTo(BigDecimal.TEN) > 0) continue;
            if (lowest == null || discount.compareTo(lowest) < 0) lowest = discount;
        }
        return lowest == null ? "" : lowest.stripTrailingZeros().toPlainString() + "折";
    }
    private Map<String, Object> enrichMerchantSearchRow(Map<String, Object> row) {
        Map<String, Object> result = new LinkedHashMap<>(row);
        String merchantType = String.valueOf(result.getOrDefault("merchantType", ""));
talent-card-server/src/main/java/com/talentcard/server/common/GlobalExceptionHandler.java
@@ -3,9 +3,11 @@
import java.util.stream.Collectors;
import jakarta.validation.ConstraintViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -54,6 +56,41 @@
        return badRequest("请求内容格式不正确");
    }
    /**
     * 权限拦截器用 {@link ResponseStatusException} 表达 403;不显式处理会被下面的兜底分支包成 500,
     * 前端只看到「服务器内部错误」,看不出是权限问题。这里原样透出状态码和原因。
     */
    @ExceptionHandler(ResponseStatusException.class)
    public ResponseEntity<ApiResponse<Void>> handleResponseStatus(ResponseStatusException exception) {
        int status = exception.getStatusCode().value();
        String reason = exception.getReason() == null || exception.getReason().isBlank()
                ? "请求被拒绝" : exception.getReason();
        return ResponseEntity.status(status).body(ApiResponse.failure(status, reason));
    }
    /**
     * 唯一键冲突(例如同一微信换绑并发、卡号/证件号重复)以前会掉进下面的兜底分支,前端只看到
     * 「服务器内部错误」,用户除了反复重试没有别的办法。这里按约束名给出可读提示,返回 409。
     */
    @ExceptionHandler(DuplicateKeyException.class)
    public ResponseEntity<ApiResponse<Void>> handleDuplicateKey(DuplicateKeyException exception) {
        Throwable cause = exception.getMostSpecificCause();
        log.warn("Duplicate key conflict: {}", cause == null ? exception.getMessage() : cause.getMessage());
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(ApiResponse.failure(HttpStatus.CONFLICT.value(), duplicateKeyMessage(exception)));
    }
    private String duplicateKeyMessage(DuplicateKeyException exception) {
        Throwable cause = exception.getMostSpecificCause();
        String detail = cause == null ? "" : String.valueOf(cause.getMessage());
        if (detail.contains("uk_openid")) return "该微信已绑定到其它账号,请稍后重试或联系管理员处理";
        if (detail.contains("uk_mobile")) return "该手机号已存在账号,请改用原手机号登录或联系管理员";
        // 先判 id_card:'uk_id_card_no' 里也含 'card_no',顺序反了会把它当成卡号。
        if (detail.contains("id_card")) return "该证件号已存在账号,请核对后重试";
        if (detail.contains("card_no")) return "该人才卡号已被使用,请核对后重试";
        return "数据已存在,请核对后重试";
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleException(Exception exception) {
        log.error("Unhandled request exception", exception);
talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java
@@ -2,6 +2,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -230,7 +231,7 @@
                       c.valid_start as validStart,
                       c.valid_end as validEnd,
                       c.issue_time as issueTime,
                       c.family_card_limit as familyCardLimit,
                       c.card_level as cardLevel,
                       (select count(*) from dependent_card_apply approved
                          where approved.deleted = 0
                            and approved.talent_card_id = a.talent_card_id
@@ -242,7 +243,26 @@
                where a.id = ? and a.deleted = 0
                limit 1
                """, apply.getId());
        return rows.isEmpty() ? Map.of() : rows.get(0);
        if (rows.isEmpty()) {
            return Map.of();
        }
        Map<String, Object> extra = new LinkedHashMap<>(rows.get(0));
        // 名额按真正生效的规则算:审核/申请侧也是这个口径。之前直接读 talent_card.family_card_limit,
        // 那一列在库里基本是 0(按卡配置那条路没启用),页面就成了「1/0」。
        extra.put("familyCardLimit", familyCardLimit(string(extra.get("cardLevel"))));
        return extra;
    }
    /**
     * 家属附属卡名额:卓越人才卡 2 个({@link #MAX_FAMILY_CARD_COUNT}),其它等级没有名额。
     * 展示与限流都用这一个口径,避免「页面显示 0、实际能绑 2 个」。
     */
    private int familyCardLimit(String cardLevel) {
        return cardLevel != null && ELITE_CARD_LEVEL.equals(cardLevel.trim()) ? MAX_FAMILY_CARD_COUNT : 0;
    }
    private String string(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }
    private void ensureApprovalSlot(DependentCardApply apply) {
@@ -257,7 +277,7 @@
//        if (configuredLimit == null || configuredLimit <= 0) {
//            throw new BusinessException(ErrorCode.CONFLICT, "该主卡未配置家属附属卡名额");
//        }
        int familyCardLimit = Math.min(2, MAX_FAMILY_CARD_COUNT);
        int familyCardLimit = familyCardLimit(ownerCard.getCardLevel());
        long approvedCount = mapper.selectCount(
                Wrappers.<DependentCardApply>lambdaQuery()
                        .eq(DependentCardApply::getTalentCardId, apply.getTalentCardId())
talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java
@@ -45,6 +45,7 @@
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -59,6 +60,7 @@
    private final BenefitMerchantMapper merchantMapper;
    private final TicketPackageMerchantMapper packageMerchantMapper;
    private final TicketPackageLevelRuleMapper levelRuleMapper;
    private final JdbcTemplate jdbcTemplate;
    public TicketPackageAdminService(
            TicketPackageMapper packageMapper,
@@ -66,7 +68,8 @@
            TicketPackageClaimMapper claimMapper,
            BenefitMerchantMapper merchantMapper,
            TicketPackageMerchantMapper packageMerchantMapper,
            TicketPackageLevelRuleMapper levelRuleMapper
            TicketPackageLevelRuleMapper levelRuleMapper,
            JdbcTemplate jdbcTemplate
    ) {
        this.packageMapper = packageMapper;
        this.codeMapper = codeMapper;
@@ -74,6 +77,35 @@
        this.merchantMapper = merchantMapper;
        this.packageMerchantMapper = packageMerchantMapper;
        this.levelRuleMapper = levelRuleMapper;
        this.jdbcTemplate = jdbcTemplate;
    }
    /**
     * 票包表单的「适用商家」下拉选项:只回启用的观影服务商家(id + 名称)。
     *
     * <p>单独开这个只读接口,是为了让「电影票管理」这个权限自己够用——之前页面复用
     * {@code /api/admin/business/merchants},票包运营账号没有「商家查询」权限就整页报错。
     */
    @Transactional(readOnly = true)
    public List<Map<String, Object>> merchantOptions(String keyword) {
        StringBuilder sql = new StringBuilder("""
                select id, merchant_name, merchant_type
                from merchant
                where deleted = 0 and status = 1 and merchant_type_code = 'cinema'
                """);
        List<Object> arguments = new ArrayList<>();
        if (keyword != null && !keyword.isBlank()) {
            sql.append(" and merchant_name like ?");
            arguments.add("%" + keyword.trim() + "%");
        }
        sql.append(" order by sort_no desc, id asc");
        return jdbcTemplate.queryForList(sql.toString(), arguments.toArray()).stream().map(row -> {
            Map<String, Object> option = new LinkedHashMap<>();
            option.put("id", row.get("id"));
            option.put("merchantName", row.get("merchant_name"));
            option.put("merchantType", row.get("merchant_type"));
            return option;
        }).toList();
    }
    @Transactional(readOnly = true)