fix: connect miniapp benefit pages to admin config
| | |
| | | const navRight = ref(10) |
| | | const activeTab = ref(0) |
| | | const serviceId = ref('') |
| | | const talentId = ref('') |
| | | const heroImage = ref('') |
| | | const benefitTitle = ref('子女托管') |
| | | const applicableCards = ref('') |
| | |
| | | navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | serviceId.value = options != null && options['serviceId'] != null ? options['serviceId'] as string : '' |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2' |
| | | loadBenefit() |
| | | loadSchools() |
| | | }) |
| | |
| | | if (serviceId.value == '') { benefitLoading.value = false; benefitError.value = true; return } |
| | | benefitLoading.value = true |
| | | benefitError.value = false |
| | | getChildrenEntrustmentBenefit(serviceId.value, (data: any) => { |
| | | getChildrenEntrustmentBenefit(serviceId.value, talentId.value, (data: any) => { |
| | | benefitTitle.value = data.title || '子女托管' |
| | | applicableCards.value = data.applicableCards || '以服务说明为准' |
| | | heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : '' |
| | |
| | | |
| | | <text class="field-label field-spacing">性别</text> |
| | | <picker class="field-picker" :range="genders" :value="genderIndex" @change="changeGender"> |
| | | <view class="field-box picker-box"><text>{{gender}}</text><text class="picker-arrow">⌄</text></view> |
| | | <view class="field-box picker-box"><text>{{gender}}</text><view class="picker-arrow"></view></view> |
| | | </picker> |
| | | |
| | | <text class="field-label field-spacing">出生年月</text> |
| | |
| | | const editMode = ref(false) |
| | | const editId = ref(0) |
| | | const talentId = ref('') |
| | | const submitText = ref('提交修改') |
| | | const submitText = ref('提交') |
| | | const isSubmitting = ref(false) |
| | | const formLoading = ref(false) |
| | | const formError = ref(false) |
| | |
| | | if (options != null && options['mode'] == 'edit') { |
| | | editMode.value = true |
| | | editId.value = parseInt(options['id'] as string) |
| | | submitText.value = '重新提交' |
| | | submitText.value = '提交' |
| | | loadChild() |
| | | } |
| | | submitKey.value = createIdempotencyKey(editMode.value ? 'child-resubmit' : 'child-create') |
| | |
| | | .field-box input { width:100%; height:88rpx; color:#475365; font-size:29rpx; line-height:40rpx; font-weight:600; } |
| | | .field-picker { display:block; } |
| | | .picker-box { color:#475365; font-size:29rpx; line-height:40rpx; font-weight:600; justify-content:space-between; } |
| | | .picker-arrow { color:#8E9AAA; font-size:38rpx; line-height:42rpx; } |
| | | .picker-arrow { width:14rpx; height:14rpx; margin-right:5rpx; border-right:3rpx solid #8E9AAA; border-bottom:3rpx solid #8E9AAA; transform:rotate(45deg) translateY(-3rpx); } |
| | | .field-placeholder, .field-placeholder-text { color:#AAB3C0; font-size:28rpx; font-weight:400; } |
| | | .proof-title { display:block; margin:49rpx 5rpx 0; color:#707C8D; font-size:27rpx; line-height:38rpx; font-weight:600; } |
| | | .proof-note { display:block; margin:29rpx 10rpx 0; color:#7E8896; font-size:23rpx; line-height:35rpx; } |
| | |
| | | .proof-delete { display:flex; width:39rpx; height:39rpx; position:absolute; z-index:2; right:-9rpx; top:-18rpx; border-radius:50%; background-color:#E26767; justify-content:center; align-items:center; } |
| | | .proof-delete text { color:#FFFFFF; font-size:30rpx; line-height:34rpx; font-weight:300; } |
| | | .proof-delete-active { opacity:.65; } |
| | | .proof-add { display:flex; background-color:#F7F8FA; justify-content:center; align-items:center; } |
| | | .proof-add { display:flex; box-sizing:border-box; border:2rpx dashed #B8C2CF; background-color:#FFFFFF; justify-content:center; align-items:center; } |
| | | .proof-add image { width:48rpx; height:48rpx; } |
| | | .proof-add-active { background-color:#EEF0F3; } |
| | | .submit-button { display:flex; height:100rpx; margin:37rpx 2rpx 0; border-radius:52rpx; background-image:linear-gradient(90deg,#FF4B32 0%,#FF8A1E 100%); box-shadow:0 12rpx 25rpx rgba(255,105,41,.16); justify-content:center; align-items:center; } |
| | |
| | | merchantAvailable.value = data != null && (data.available === true || Number(data.available) == 1) |
| | | merchantUnavailableReason.value = data != null && data.unavailableReason != null && data.unavailableReason != '' ? data.unavailableReason as string : '该停车权益暂不可用' |
| | | merchantServiceLabel.value = data != null && data.serviceLabel != null ? data.serviceLabel as string : '停车服务' |
| | | parkingBenefitText.value = data != null && data.merchantRight.parkingType == 'free' ? '免费' : '付费' |
| | | parkingBenefitText.value = data != null && data.parkingType == 'free' ? '免费' : '付费' |
| | | applicableCards.value = data != null && data.applicableCards != null ? data.applicableCards as string : '请咨询商户' |
| | | const parkingRightDetail = data != null && data.merchantRight != null && data.merchantRight.detail != null |
| | | ? String(data.merchantRight.detail).trim() |
| | |
| | | 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]; 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: item.benefitText != null ? item.benefitText 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 }) |
| | | 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]; 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: activeTab.value == '免费体检' || activeTab.value == '配偶推荐就业' ? '免费' : (item.benefitText != null ? item.benefitText 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') |
| | |
| | | v-for="item in visibleServices" :key="item.id" @tap="openService(item)"> |
| | | <image class="service-image" :src="item.image" mode="aspectFill" |
| | | @error="replaceServiceImage(item)"></image> |
| | | <view class="service-info"><text class="service-name">{{item.name}}</text><text |
| | | class="discount-tag">{{item.available ? item.discount : '已下架'}}</text> |
| | | <view class="service-info"><text class="service-name">{{item.name}}</text><view v-if="item.available" class="discount-tags"><text v-for="tag in item.discountTags" :key="tag" class="discount-tag">{{tag}}</text></view><text v-else class="discount-tag">已下架</text> |
| | | <view v-if="item.merchantType != 'apartment'" class="address"> |
| | | <image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text> |
| | | </view> |
| | |
| | | import { getStayTopic, getStayTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = '' |
| | | const activeIndex = ref(-1); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644); const scenes = ref<any[]>([]); const currentScene = ref({ id: 'fallback', name: '', image: fallbackImage }); const topicDescription = ref('') |
| | | const activeTab = ref('hotel'); const visibleServices = ref<any[]>([]); const initialLoading = ref(true); const initialError = ref(false); const loadingMore = ref(false); const loadMoreError = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false) |
| | | const talentId = ref('') |
| | | |
| | | onLoad((options : any) => { |
| | | // #ifdef MP-WEIXIN |
| | | const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); windowWidth.value = info.windowWidth; navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right |
| | | // #endif |
| | | if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' } |
| | | const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | loadStayTopic(); loadInitialServices() |
| | | }) |
| | | const readText = (primary : any, fallback : any) : string => { |
| | |
| | | loadMoreError.value = true |
| | | return |
| | | } |
| | | getStayTopicMerchants(activeTab.value, currentPage.value + 1, 4, (data : any) => { |
| | | getStayTopicMerchants(activeTab.value, currentPage.value + 1, 4, talentId.value, (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() : [] |
| | |
| | | merchantType: merchantType != '' ? merchantType : activeTab.value, |
| | | name: merchantName != '' ? merchantName : '\u4f4f\u5bbf\u670d\u52a1', |
| | | discount: discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab', |
| | | discountTags: item.benefitTags instanceof Array && (item.benefitTags as any[]).length > 0 ? item.benefitTags as any[] : [discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab'], |
| | | address: readText(item.address, item['merchant_address']), |
| | | image: coverUrl != '' ? resolveApiUrl(coverUrl) : fallbackImage, |
| | | available: readAvailable(item) |
| | |
| | | font-size: 21rpx; |
| | | line-height: 29rpx; |
| | | } |
| | | .discount-tags { display:flex; flex-direction:row; flex-wrap:wrap; gap:8rpx; } |
| | | .address { |
| | | display: flex; |
| | | max-width: 100%; |
| | |
| | | export const getTravelTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTravelTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getTravelTopicMerchants = (page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTravelTopicMerchants'), method: 'GET', skipAuth: true, data: { page: page, size: size } }, onSuccess, onError) |
| | | export const getStayTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appStayTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getStayTopicMerchants = (type: string, page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appStayTopicMerchants'), method: 'GET', skipAuth: true, data: { type: type, page: page, size: size } }, onSuccess, onError) |
| | | export const getStayTopicMerchants = (type: string, page: number, size: number, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = { type: type, page: page, size: size } |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appStayTopicMerchants'), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const getEntertainmentTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appEntertainmentTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getEntertainmentTopicMerchants = (page: number, size: number, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appEntertainmentTopicMerchants'), method: 'GET', skipAuth: true, data: { page: page, size: size } }, onSuccess, onError) |
| | | export const getTransportTopic = (onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appTransportTopic'), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appFinanceMerchantDetail') + encodeURIComponent(merchantId), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const getChildrenEntrustmentBenefit = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getChildrenEntrustmentBenefit = (serviceId: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | const data: any = {} |
| | | if (talentId != '') data.talentId = Number(talentId) |
| | | return request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId), method: 'GET', skipAuth: true, data: data }, onSuccess, onError) |
| | | } |
| | | export const getChildrenEntrustmentSchools = (serviceId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentBenefit') + encodeURIComponent(serviceId) + '/schools', method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getChildrenEntrustmentSchool = (schoolId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildrenEntrustmentSchool') + encodeURIComponent(schoolId), method: 'GET', skipAuth: true }, onSuccess, onError) |
| | | export const getChildren = (talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appChildren'), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | }, |
| | | { |
| | | level: '青年人才卡', |
| | | enabled: true, |
| | | tag: '', |
| | | detail: '', |
| | | storeConsume: false, |
| | | discount: '', |
| | | governmentSubsidy: false, |
| | | consumeDesc: '', |
| | | consumeEnabled: false, |
| | | }, |
| | | ] |
| | | } |
| | | |
| | |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | |
| | | } |
| | | |
| | | @GetMapping("/{serviceId}") |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long serviceId) { |
| | | return ApiResponse.success(service.detail(serviceId)); |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long serviceId, |
| | | @RequestParam(required = false) Long talentId) { |
| | | return ApiResponse.success(service.detail(serviceId, talentId)); |
| | | } |
| | | |
| | | @GetMapping("/{serviceId}/schools") |
| | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long serviceId) { |
| | | approvalService(serviceId); |
| | | public Map<String, Object> detail(Long serviceId, Long talentId) { |
| | | Map<String, Object> merchant = approvalService(serviceId); |
| | | Map<String, Object> benefit = benefit(); |
| | | boolean available = number(benefit.get("enabled")) == 1; |
| | | Map<String, Object> matchedRight = matchedRight(merchant.get("merchantRightsConfig"), activeCardLevel(talentId)); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("benefitId", benefit.get("id")); |
| | | result.put("serviceId", serviceId); |
| | | result.put("title", benefit.get("title")); |
| | | result.put("applicableCards", benefit.get("applicableCards")); |
| | | result.put("heroImage", benefit.get("heroImage")); |
| | | result.put("rightsText", benefit.get("rightsText")); |
| | | result.put("rightsText", text(matchedRight.get("detail"), text(benefit.get("rightsText"), "暂无使用说明"))); |
| | | result.put("available", available); |
| | | result.put("unavailableReason", available ? "" : text(benefit.get("unavailableReason"), "该权益暂未开放")); |
| | | return result; |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public List<Map<String, Object>> schools(Long serviceId) { |
| | | approvalService(serviceId); |
| | | Map<String, Object> merchant = approvalService(serviceId); |
| | | List<Map<String, Object>> configuredSchools = configuredSchools(merchant.get("usageInstructions")); |
| | | if (!configuredSchools.isEmpty()) return configuredSchools; |
| | | Map<String, Object> benefit = benefit(); |
| | | return query(""" |
| | | select id, school_name, address, latitude, longitude, contact_mobile, detail_available |
| | |
| | | school.put("phone", text(row.get("contactMobile"), "")); |
| | | school.put("detailAvailable", number(row.get("detailAvailable")) == 1); |
| | | return school; |
| | | }).toList(); |
| | | }).toList(); |
| | | } |
| | | |
| | | private List<Map<String, Object>> configuredSchools(Object value) { |
| | | String json = text(value, ""); |
| | | if (json.isBlank()) return List.of(); |
| | | try { |
| | | List<Map<String, Object>> rows = JSON.parseObject(json, new com.alibaba.fastjson.TypeReference<>() {}); |
| | | if (rows == null) return List.of(); |
| | | return rows.stream().map(row -> { |
| | | Map<String, Object> school = new LinkedHashMap<>(); |
| | | school.put("id", row.getOrDefault("id", row.get("name"))); |
| | | school.put("name", text(row.get("name"), "未命名学校")); |
| | | school.put("address", text(row.get("address"), "暂未配置学校地址")); |
| | | school.put("latitude", row.get("latitude")); |
| | | school.put("longitude", row.get("longitude")); |
| | | school.put("phone", text(row.get("phone"), text(row.get("contactMobile"), ""))); |
| | | school.put("detailAvailable", !text(row.get("detail"), "").isBlank()); |
| | | return school; |
| | | }).toList(); |
| | | } catch (RuntimeException ignored) { |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | return result; |
| | | } |
| | | |
| | | private void approvalService(Long serviceId) { |
| | | private Map<String, Object> approvalService(Long serviceId) { |
| | | if (serviceId == null || serviceId <= 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "学生托付服务信息不能为空"); |
| | | List<Map<String, Object>> rows = query(""" |
| | | select m.id |
| | | select m.id, m.merchant_rights_config, m.usage_instructions |
| | | from merchant m join premium_topic_merchant mapping on mapping.merchant_id = m.id |
| | | where m.id = ? and m.deleted = 0 and mapping.deleted = 0 and mapping.category_key = 'children' |
| | | """, serviceId); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "学生托付服务不存在"); |
| | | return rows.get(0); |
| | | } |
| | | |
| | | private Map<String, Object> matchedRight(Object value, String cardLevel) { |
| | | if (cardLevel.isBlank() || value == null) return Map.of(); |
| | | try { |
| | | List<Map<String, Object>> rights = JSON.parseObject(String.valueOf(value), new com.alibaba.fastjson.TypeReference<>() {}); |
| | | if (rights != null) { |
| | | for (Map<String, Object> right : rights) { |
| | | if (booleanValue(right.get("enabled")) && cardLevel.equals(text(right.get("level"), ""))) return right; |
| | | } |
| | | } |
| | | } catch (RuntimeException ignored) { |
| | | // Fall back to the legacy children entrustment description when the configuration is invalid. |
| | | } |
| | | return Map.of(); |
| | | } |
| | | |
| | | private String activeCardLevel(Long talentId) { |
| | | if (talentId == null || talentId <= 0) return ""; |
| | | List<Map<String, Object>> cards = query(""" |
| | | select card_level from talent_card |
| | | where talent_id = ? and deleted = 0 and card_type = 'PERSONAL' and status = 1 |
| | | and (valid_end is null or valid_end >= current_date) |
| | | order by id desc limit 1 |
| | | """, talentId); |
| | | return cards.isEmpty() ? "" : text(cards.get(0).get("cardLevel"), ""); |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String result = text(value, ""); |
| | | return "true".equalsIgnoreCase(result) || "1".equals(result) || "yes".equalsIgnoreCase(result); |
| | | } |
| | | |
| | | private Map<String, Object> benefit() { |
| | |
| | | result.put("applicableCards", applicableCards(right, benefit)); |
| | | result.put("usageInstructions", text(merchant.get("usageInstructions"), "请咨询商户")); |
| | | result.put("rightsText", rightsText(merchant, right)); |
| | | result.put("actionMode", text(benefit.get("actionMode"), "plate")); |
| | | String parkingType = text(right == null ? null : right.get("parkingType"), "paid"); |
| | | result.put("actionMode", "free".equalsIgnoreCase(parkingType) ? "talentCode" : "plate"); |
| | | result.put("plateFormMode", text(benefit.get("plateFormMode"), "simple")); |
| | | result.put("benefit", benefitView(benefit)); |
| | | result.put("applicableStores", parkingStores(merchant.get("usageInstructions"))); |
| | | result.put("merchantRightsConfig", rights); |
| | | result.put("merchantRight", right == null ? Map.of() : right); |
| | | result.put("parkingType", text(right == null ? null : right.get("parkingType"), "paid")); |
| | | result.put("parkingType", parkingType); |
| | | result.put("parkingScope", text(right == null ? null : right.get("parkingScope"), "all")); |
| | | result.put("consumeDesc", text(right == null ? null : right.get("consumeDesc"), "")); |
| | | return result; |
| | |
| | | public ApiResponse<PageResult<Map<String, Object>>> stayMerchants( |
| | | @RequestParam(defaultValue = "hotel") String type, |
| | | @RequestParam(defaultValue = "1") int page, |
| | | @RequestParam(defaultValue = "4") int size |
| | | @RequestParam(defaultValue = "4") int size, |
| | | @RequestParam(required = false) Long talentId |
| | | ) { |
| | | return ApiResponse.success(portalService.stayMerchants(type, page, size)); |
| | | return ApiResponse.success(portalService.stayMerchants(type, page, size, talentId)); |
| | | } |
| | | |
| | | @GetMapping("/topics/entertainment") |
| | |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> stayMerchants(String type, int page, int size) { |
| | | return stayMerchants(type, page, size, null); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> stayMerchants(String type, int page, int size, Long talentId) { |
| | | String requestedType = type == null ? "" : type.trim(); |
| | | boolean apartment = switch (requestedType) { |
| | | case "hotel", "酒店服务" -> false; |
| | |
| | | : " where deleted = 0 and merchant_type in ('hotel', '酒店服务')"; |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select id, merchant_name, merchant_type, contact_name, contact_mobile, logo_url, cover_url, address, |
| | | status, sort_no, longitude, latitude, business_status, |
| | | status, sort_no, longitude, latitude, business_status, group_buying_enabled, merchant_rights_config, |
| | | (select min(p.sale_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as sale_price, |
| | | (select min(p.original_price) from merchant_product p where p.merchant_id = merchant.id and p.deleted = 0 and p.status = 1 and p.audit_status = 2) as original_price |
| | | from merchant |
| | |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> normalized = new LinkedHashMap<>(row); |
| | | normalized.put("merchantType", responseMerchantType); |
| | | String cardLevel = activeProductCardLevel(talentId); |
| | | Map<String, Object> right = matchingMerchantRight(merchantRightsConfigs(row.get("merchantRightsConfig")), cardLevel, activeTalentSourceAreaType(talentId), true); |
| | | normalized.put("benefitTags", stayBenefitTags(right, row)); |
| | | return enrichMerchantSearchRow(normalized); |
| | | }).toList(); |
| | | return new PageResult<>(records, result.page(), result.size(), result.total()); |
| | | } |
| | | |
| | | private List<String> stayBenefitTags(Map<String, Object> right, Map<String, Object> merchant) { |
| | | List<String> tags = new ArrayList<>(); |
| | | Object merchantId = merchant.get("id"); |
| | | if (merchantId instanceof Number id && hasActiveMerchantCoupon(id.longValue())) tags.add("惠"); |
| | | if (right == null) return tags; |
| | | if (booleanValue(right.get("freeVoucherEnabled"))) { |
| | | String times = textOrDefault(right.get("freeVoucherTimes"), ""); |
| | | if (!times.isBlank()) tags.add("免费" + times + "次"); |
| | | } |
| | | if (booleanValue(right.get("storeConsume")) && booleanValue(right.get("consumeEnabled"))) { |
| | | String type = textOrDefault(right.get("consumeType"), ""); |
| | | String discount = textOrDefault(right.get("consumeDiscount"), ""); |
| | | if ("折扣".equals(type) && !discount.isBlank()) tags.add(discount + "折"); |
| | | } |
| | | return tags; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | |
| | | where category_key = ? and enabled = 1 and deleted = 0 |
| | | """, Integer.class, categoryId); |
| | | if (categoryCount == 0) throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的购物服务分类"); |
| | | String merchantTypeFilter = switch (categoryId) { |
| | | case "housing" -> "('购房优惠')"; |
| | | case "car" -> "('购车服务')"; |
| | | case "appliance" -> "('家电服务')"; |
| | | case "digital" -> "('数码服务')"; |
| | | case "book" -> "('文化服务', '图书服务')"; |
| | | default -> "('')"; |
| | | }; |
| | | String merchantWhere = """ |
| | | where m.deleted = 0 and m.merchant_type = 'shopping' |
| | | and mapping.deleted = 0 and mapping.category_key = ? |
| | | """; |
| | | where m.deleted = 0 |
| | | and (m.merchant_type in %s or (m.merchant_type = 'shopping' and mapping.deleted = 0 and mapping.category_key = ?)) |
| | | """.formatted(merchantTypeFilter); |
| | | PageResult<Map<String, Object>> result = page(""" |
| | | select m.id, m.merchant_name, m.merchant_type, m.contact_name, m.contact_mobile, m.logo_url, m.cover_url, m.address, |
| | | m.service_label, m.status, m.sort_no, m.longitude, m.latitude, m.business_status |
| | | from merchant m |
| | | join shopping_topic_merchant mapping on mapping.merchant_id = m.id |
| | | left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? |
| | | """ + merchantWhere + " order by mapping.sort_no asc, m.sort_no asc, m.id asc", |
| | | "select count(*) from merchant m join shopping_topic_merchant mapping on mapping.merchant_id = m.id " + merchantWhere, |
| | | page, size, categoryId); |
| | | "select count(*) from merchant m left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 and mapping.category_key = ? " + merchantWhere, |
| | | page, size, categoryId, categoryId); |
| | | List<Map<String, Object>> records = result.records().stream().map(row -> { |
| | | Map<String, Object> item = enrichMerchantSearchRow(row); |
| | | item.put("discountText", textOrDefault(item.get("serviceLabel"), "人才专享")); |
| | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long merchantId) { |
| | | Map<String, Object> row = one(""" |
| | | select m.id, m.merchant_name, m.cover_url, m.service_label, m.address, m.contact_mobile, |
| | | m.longitude, m.latitude, m.status as merchant_status, mapping.category_key, |
| | | select m.id, m.merchant_name, m.cover_url, m.service_label, m.address, m.contact_mobile, m.contact_name, m.rights_text, |
| | | m.longitude, m.latitude, m.status as merchant_status, |
| | | coalesce(mapping.category_key, case m.merchant_type |
| | | when '购房优惠' then 'housing' when '购车服务' then 'car' |
| | | when '家电服务' then 'appliance' when '数码服务' then 'digital' |
| | | when '文化服务' then 'book' when '图书服务' then 'book' end) as category_key, |
| | | detail_config.talent_levels, detail_config.benefit_detail, detail_config.advisor_id, |
| | | detail_config.advisor_name, detail_config.advisor_department, detail_config.advisor_mobile, |
| | | detail_config.advisor_service_time, detail_config.advisor_available, |
| | | detail_config.unavailable_reason, detail_config.status as detail_status |
| | | from merchant m |
| | | join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 |
| | | join shopping_service_detail detail_config on detail_config.merchant_id = m.id and detail_config.deleted = 0 |
| | | where m.id = ? and m.merchant_type = 'shopping' and m.deleted = 0 |
| | | left join shopping_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 |
| | | left join shopping_service_detail detail_config on detail_config.merchant_id = m.id and detail_config.deleted = 0 |
| | | where m.id = ? and m.merchant_type in ('shopping', '购房优惠', '购车服务', '家电服务', '数码服务', '文化服务', '图书服务') and m.deleted = 0 |
| | | order by detail_config.id asc limit 1 |
| | | """, merchantId); |
| | | boolean available = number(row.get("merchantStatus")) == 1 && number(row.get("detailStatus")) == 1; |
| | | boolean available = number(row.get("merchantStatus")) == 1 && (row.get("detailStatus") == null || number(row.get("detailStatus")) == 1); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("merchantId", row.get("id")); |
| | | result.put("categoryId", row.get("categoryKey")); |
| | |
| | | result.put("contactMobile", text(row.get("contactMobile"), "")); |
| | | result.put("longitude", row.get("longitude")); |
| | | result.put("latitude", row.get("latitude")); |
| | | result.put("benefitDetail", text(row.get("benefitDetail"), "以服务专员说明为准")); |
| | | result.put("benefitDetail", text(row.get("benefitDetail"), text(row.get("rightsText"), "以服务专员说明为准"))); |
| | | result.put("available", available); |
| | | result.put("unavailableReason", available ? "" : text(row.get("unavailableReason"), "该购物服务当前不可用")); |
| | | Map<String, Object> advisor = new LinkedHashMap<>(); |
| | | advisor.put("advisorId", row.get("advisorId")); |
| | | advisor.put("name", text(row.get("advisorName"), "")); |
| | | advisor.put("name", text(row.get("advisorName"), text(row.get("contactName"), ""))); |
| | | advisor.put("department", text(row.get("advisorDepartment"), "")); |
| | | advisor.put("mobile", text(row.get("advisorMobile"), "")); |
| | | advisor.put("mobile", text(row.get("advisorMobile"), text(row.get("contactMobile"), ""))); |
| | | advisor.put("serviceTime", text(row.get("advisorServiceTime"), "")); |
| | | advisor.put("available", number(row.get("advisorAvailable")) == 1 && !text(row.get("advisorMobile"), "").isEmpty()); |
| | | advisor.put("available", (row.get("advisorAvailable") == null || number(row.get("advisorAvailable")) == 1) && !text(row.get("advisorMobile"), text(row.get("contactMobile"), "")).isEmpty()); |
| | | result.put("advisor", advisor); |
| | | return result; |
| | | } |