| | |
| | | <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> |
| | |
| | | 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 => { |
| | |
| | | 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) }) } |
| | |
| | | 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> |
| | |
| | | .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; } |
| | |
| | | .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> |
| | |
| | | <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> |
| | |
| | | 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') |
| | |
| | | .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> |
| | |
| | | <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> |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | </scroll-view> |
| | | <view class="merchant-tabbar-host"><merchant-tabbar active="merchant"></merchant-tabbar></view> |
| | | </view> |
| | | </template> |
| | |
| | | |
| | | <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%); } |
| | |
| | | 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 ? '已完成' : '待核销' |
| New file |
| | |
| | | <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> |
| | |
| | | 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)) |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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 |
| | | } |
| | |
| | | |
| | | onLoad(() => { |
| | | if (hasActiveSession()) loadMerchantBusinessDate((value: string) => applyBusinessDate(value), () => {}) |
| | | }) |
| | | |
| | | // 列表用 onShow 拉:核销成功后从结果页/详情页返回时,状态才是最新的。 |
| | | // 之前只在 onLoad 拉一次,返回列表还停在「待核销」,要切一次分类标签才刷新。 |
| | | onShow(() => { |
| | | reloadOrders() |
| | | }) |
| | | |
| | |
| | | <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"> |
| | |
| | | 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[] = [] |
| | |
| | | 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) |
| | |
| | | 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) |
| | | } |
| | |
| | | color: #8a94a6; |
| | | font-size: 12px; |
| | | line-height: 18px; |
| | | /* 图片只有一张时没有"移除"按钮,提示会落到 72px 的按钮列里被挤成"一字一行", |
| | | 所以这里固定让它跨满整行、单独一行显示。 */ |
| | | grid-column: 1 / -1; |
| | | } |
| | | |
| | | /* 上传图片处的推荐尺寸提示:各处复用同一套样式,避免每个页面各写一份。 */ |
| | |
| | | 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 { |
| | |
| | | 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[]>([]) |
| | |
| | | import { |
| | | createTicketPackage, |
| | | generateTicketPackageCodes, |
| | | getAdminMerchants, |
| | | getTicketPackageClaims, |
| | | getTicketPackageCodes, |
| | | getTicketPackageMerchantOptions, |
| | | getTicketPackages, |
| | | importTicketPackageCodes, |
| | | updateTicketPackage, |
| | |
| | | |
| | | 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 = [] |
| | | } |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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> |
| New file |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | /** 详情页轮播图按商家配置的服务类型:其余类型顶部图仍用封面图或团单封面。 */ |
| | | 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"); |
| | | |
| | | /** |
| | | * 同步商家详情页轮播图。 |
| | |
| | | package com.talentcard.server.business.admin; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.talentcard.server.business.PageResult; |
| | |
| | | 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) { |
| | |
| | | """, 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) { |
| | |
| | | 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) |
| | |
| | | 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")); |
| | |
| | | && 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(); |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | 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); |
| | |
| | | ? 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) { |
| | |
| | | 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")); |
| | |
| | | // 标签只用后台配的:先取商家权益里第一个启用等级的标签,再退回商家服务标签; |
| | | // 都没配就留空(后台标签是必填项,不再兜底"人才专享")。 |
| | | 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; |
| | |
| | | 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", "")); |
| | |
| | | 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; |
| | |
| | | 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); |
| | |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | |
| | | 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 |
| | |
| | | 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) { |
| | |
| | | // 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()) |
| | |
| | | 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; |
| | |
| | | private final BenefitMerchantMapper merchantMapper; |
| | | private final TicketPackageMerchantMapper packageMerchantMapper; |
| | | private final TicketPackageLevelRuleMapper levelRuleMapper; |
| | | private final JdbcTemplate jdbcTemplate; |
| | | |
| | | public TicketPackageAdminService( |
| | | TicketPackageMapper packageMapper, |
| | |
| | | TicketPackageClaimMapper claimMapper, |
| | | BenefitMerchantMapper merchantMapper, |
| | | TicketPackageMerchantMapper packageMerchantMapper, |
| | | TicketPackageLevelRuleMapper levelRuleMapper |
| | | TicketPackageLevelRuleMapper levelRuleMapper, |
| | | JdbcTemplate jdbcTemplate |
| | | ) { |
| | | this.packageMapper = packageMapper; |
| | | this.codeMapper = codeMapper; |
| | |
| | | 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) |