Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/java/talent-card
22个文件已删除
8个文件已添加
45个文件已修改
| | |
| | | export const developmentLoginBypassEnabled: boolean = false |
| | | |
| | | // 后台提供合法域名后仅在此处填写,业务页面不得硬编码域名。 |
| | | const developmentBaseUrl = 'http://192.168.110.13:8080' |
| | | const developmentBaseUrl = 'http://192.168.110.8:8080' |
| | | const stagingBaseUrl = '' |
| | | const productionBaseUrl = '' |
| | | |
| | |
| | | if (name == 'appCompletedOrders') return '/api/app/completed-orders/' |
| | | if (name == 'appCancelledOrders') return '/api/app/cancelled-orders/' |
| | | if (name == 'appProcessingApplications') return '/api/app/processing-applications/' |
| | | if (name == 'appFamilyCards') return '/api/app/family-cards' |
| | | if (name == 'appFamilyCards') return '/api/app/family-cards/applications' |
| | | if (name == 'appFreeBenefits') return '/api/app/free-benefits' |
| | | if (name == 'appAgreements') return '/api/app/agreements/' |
| | | if (name == 'appUploadImage') return '/api/app/uploads/images' |
| | |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const serviceId = ref('') |
| | | const heroImage = ref('/static/stay-assets/hotel-vienna.jpg') |
| | | const heroImage = ref('') |
| | | const benefitTitle = ref('行政审批') |
| | | const freeLabel = ref('免费') |
| | | const benefitAvailable = ref(true) |
| | |
| | | getAdminApprovalBenefit(serviceId.value, (data: any) => { |
| | | benefitTitle.value = data.title || '行政审批' |
| | | freeLabel.value = data.freeLabel || '免费' |
| | | heroImage.value = resolveApiUrl(data.heroImage || '/static/stay-assets/hotel-vienna.jpg') |
| | | heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : '' |
| | | benefitAvailable.value = data.available === true |
| | | unavailableReason.value = data.unavailableReason || '该权益暂未开放' |
| | | orgs.value = data.organizations instanceof Array ? data.organizations as any[] : [] |
| | |
| | | } |
| | | const goBack = () => safeBack('/pages/premium-benefits/premium-benefits') |
| | | const noop = () => {} |
| | | const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' } |
| | | const replaceHero = () => { heroImage.value = '' } |
| | | const callOrg = (item: any) => { if (item.phone == null || item.phone == '') { uni.showToast({ title: '机构暂未提供咨询电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: item.phone, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) } |
| | | const showWindowInfo = (item: any) => { |
| | | if (!(item.windows instanceof Array) || item.windows.length == 0) return |
| | |
| | | const navHeight = ref(32) |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const fallbackImage = '/static/common/image-fallback.svg' |
| | | const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage } |
| | | const fallbackImage = '' |
| | | const replaceImage = (item: any) => { if (item.image != '') item.image = '' } |
| | | const keyword = ref('') |
| | | const hasSearched = ref(false) |
| | | const isAllMode = ref(false) |
| | |
| | | merchantType: rawType, |
| | | discount: item.discountText != null ? item.discountText as string : formatRemoteDiscount(item), |
| | | address: item.address != null ? item.address as string : '', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : '/static/common/image-fallback.svg', |
| | | image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : '', |
| | | sort: item.sort != null ? Number(item.sort) : 0, |
| | | detailType: item.detailType != null ? item.detailType as string : 'MERCHANT', |
| | | detailBusinessId: item.detailBusinessId != null ? '' + item.detailBusinessId : '' + item.id, |
| | |
| | | const navWidth = ref(88) |
| | | const navRight = ref(10) |
| | | const isSolidNav = ref(false) |
| | | const fallbackImage = '/static/common/image-fallback.svg' |
| | | const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage } |
| | | const fallbackImage = '' |
| | | const replaceImage = (item: any) => { if (item.image != '') item.image = '' } |
| | | |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | |
| | | { id: 11, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg' }, |
| | | { id: 12, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg' } |
| | | ] |
| | | const visibleBenefits = ref([allBenefits[0], allBenefits[1], allBenefits[2], allBenefits[3]]) |
| | | const loadedBenefitCount = ref(4) |
| | | const visibleBenefits = ref<any[]>([]) |
| | | const loadedBenefitCount = ref(0) |
| | | const currentPage = ref(0) |
| | | const initialLoading = ref(true) |
| | | const loadingMore = ref(false) |
| | |
| | | const navRight = ref(10) |
| | | const activeTab = ref(0) |
| | | const serviceId = ref('') |
| | | const heroImage = ref('/static/stay-assets/hotel-vienna.jpg') |
| | | const heroImage = ref('') |
| | | const benefitTitle = ref('子女托管') |
| | | const applicableCards = ref('') |
| | | const description = ref('') |
| | |
| | | getChildrenEntrustmentBenefit(serviceId.value, (data: any) => { |
| | | benefitTitle.value = data.title || '子女托管' |
| | | applicableCards.value = data.applicableCards || '以服务说明为准' |
| | | heroImage.value = resolveApiUrl(data.heroImage || '/static/stay-assets/hotel-vienna.jpg') |
| | | heroImage.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : '' |
| | | description.value = data.rightsText || '暂无使用说明' |
| | | benefitLoading.value = false |
| | | }, () => { benefitLoading.value = false; benefitError.value = true }) |
| | |
| | | } |
| | | const goBack = () => safeBack('/pages/premium-benefits/premium-benefits') |
| | | const switchTab = (index: number) => { activeTab.value = index } |
| | | const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' } |
| | | const replaceHero = () => { heroImage.value = '' } |
| | | const openSchoolDetail = (item: any) => { |
| | | if (item.detailAvailable !== true) { uni.showToast({ title: '学校介绍暂未开放', icon: 'none' }); return } |
| | | uni.navigateTo({ url: '/pages/school-detail/school-detail?id=' + item.id + '&name=' + encodeURIComponent(item.name) + '&address=' + encodeURIComponent(item.address) }) |
| | |
| | | import { getSession } from '../../services/session.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const fallbackImage = '/static/entertainment-assets/wanda-cinema.webp' |
| | | const fallbackImage = '' |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const isSolidNav = ref(false); const currentTab = ref('purchase'); const merchantId = ref(''); const talentId = ref('') |
| | | const cinemaName = ref('影城服务'); const cinemaAddress = ref(''); const cinemaHero = ref(fallbackImage); const cinemaMobile = ref(''); const cinemaLatitude = ref(0); const cinemaLongitude = ref(0) |
| | |
| | | const navRight = ref(10) |
| | | const isSolidNav = ref(false) |
| | | const windowWidth = ref(375) |
| | | const fallbackImage = '/static/entertainment-assets/entertainment-hero.webp' |
| | | const fallbackImage = '' |
| | | const activeIndex = ref(-1) |
| | | const sceneScrollLeft = ref(0) |
| | | const sceneTrackWidth = ref(644) |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { createFamilyCardApplication } from '../../services/portal.uts' |
| | | import { createFamilyCardApplication, getTalentCardProfile } from '../../services/portal.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | const goBack = () => safeBack('/pages/family-card/family-card') |
| | | |
| | | const navTop = ref(28) |
| | | const navHeight = ref(32) |
| | |
| | | const relationIndex = ref(0) |
| | | const form = ref({ name: '', idCard: '', phone: '', relation: '配偶' }) |
| | | const talentId = ref('') |
| | | const talentCardId = ref('') |
| | | |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | |
| | | navRight.value = systemInfo.windowWidth - capsule.right |
| | | // #endif |
| | | talentId.value = options != null && options['talentId'] != null ? options['talentId'] as string : (apiEnvironment == 'development' ? '2' : '') |
| | | resolveTalentCardId() |
| | | }) |
| | | |
| | | const goBack = () => safeBack('/pages/family-card/family-card') |
| | | const resolveTalentCardId = () => { |
| | | if (talentId.value == '') return |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | | talentCardId.value = card != null && card.id != null ? '' + card.id : '' |
| | | }, () => {}) |
| | | } |
| | | const changeRelation = (event: any) => { |
| | | const selectedIndex = parseInt(event.detail.value as string) |
| | | relationIndex.value = selectedIndex |
| | |
| | | uni.showToast({ title: '请填写正确的手机号', icon: 'none' }) |
| | | return |
| | | } |
| | | if (talentCardId.value == '') { |
| | | isSubmitting.value = false |
| | | uni.showToast({ title: '未获取到主卡信息,请返回重试', icon: 'none' }) |
| | | return |
| | | } |
| | | isSubmitting.value = true |
| | | createFamilyCardApplication({ talentId: Number(talentId.value), name: form.value.name.trim(), idCard: form.value.idCard.trim(), mobile: form.value.phone.trim(), relationship: form.value.relation }, (data: any) => { |
| | | createFamilyCardApplication(talentCardId.value, form.value.name.trim(), form.value.idCard.trim(), form.value.phone.trim(), form.value.relation, (data: any) => { |
| | | uni.setStorageSync('familyCardApplicationResult', { |
| | | name: data.name || '', |
| | | relationship: data.relationship || '', |
| | | applicationNo: data.applicationNo || '', |
| | | estimatedProcessingTime: data.estimatedProcessingTime || '资料已提交,请耐心等待审核。' |
| | | name: form.value.name.trim(), |
| | | relationship: form.value.relation, |
| | | applicationNo: '', |
| | | estimatedProcessingTime: '资料已提交,请耐心等待审核。' |
| | | }) |
| | | uni.redirectTo({ url: '/pages/family-card-success/family-card-success' }) |
| | | }, (error) => { |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getFamilyCards, unbindFamilyCards } from '../../services/portal.uts' |
| | | import { getFamilyCards, getTalentCardProfile, unbindFamilyCards } from '../../services/portal.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | const listLoaded = ref(false) |
| | | const listError = ref(false) |
| | | const talentId = ref('') |
| | | const talentCardId = ref('') |
| | | |
| | | onLoad((options: any) => { |
| | | // #ifdef MP-WEIXIN |
| | |
| | | navRight.value = systemInfo.windowWidth - capsule.right |
| | | // #endif |
| | | talentId.value = options != null && options['talentId'] != null ? options['talentId'] as string : (apiEnvironment == 'development' ? '2' : '') |
| | | loadMembers() |
| | | resolveTalentCardId() |
| | | }) |
| | | const loadMembers = () => { |
| | | listLoading.value = true |
| | | listLoaded.value = false |
| | | listError.value = false |
| | | if (talentId.value == '') { listLoading.value = false; listError.value = true; return } |
| | | getFamilyCards(talentId.value, (data: any) => { |
| | | familyMembers.value = (data as any[]).filter((item: any) => item.canUnbind === true).map((item: any) => { item.id = '' + item.id; return item }) |
| | | if (talentCardId.value == '') { listLoading.value = false; listError.value = true; return } |
| | | getFamilyCards(talentCardId.value, (data: any) => { |
| | | const items = (data as any[]) || [] |
| | | familyMembers.value = items.filter((item: any) => item.status === 1).map((item: any) => ({ |
| | | id: '' + item.id, |
| | | name: item.dependentName != null ? item.dependentName : '', |
| | | mobile: item.dependentMobile != null ? item.dependentMobile : '', |
| | | relationship: item.relation != null ? item.relation : '' |
| | | })) |
| | | selectedIds.value = selectedIds.value.filter((id: string) => familyMembers.value.some((item: any) => item.id == id)) |
| | | listLoading.value = false |
| | | listLoaded.value = true |
| | | }, () => { listLoading.value = false; listError.value = true }) |
| | | } |
| | | |
| | | const goBack = () => safeBack('/pages/family-card/family-card') |
| | | const resolveTalentCardId = () => { |
| | | if (talentId.value == '') { listLoading.value = false; listError.value = true; return } |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | | talentCardId.value = card != null && card.id != null ? '' + card.id : '' |
| | | if (talentCardId.value == '') { listLoading.value = false; listError.value = true; return } |
| | | loadMembers() |
| | | }, () => { listLoading.value = false; listError.value = true }) |
| | | } |
| | | const isSelected = (id: string) => selectedIds.value.indexOf(id) >= 0 |
| | | const toggleMember = (id: string) => { |
| | | const next = selectedIds.value.slice() |
| | |
| | | }) |
| | | } |
| | | const unbindSelected = (ids: string[]) => { |
| | | unbindFamilyCards(ids, talentId.value, () => { isSubmitting.value = false; uni.showToast({ title: '已解除绑定', icon: 'success' }); setTimeout(() => safeBack('/pages/family-card/family-card'), 700) }, (error) => { isSubmitting.value = false; uni.showToast({ title: error.message, icon: 'none' }) }) |
| | | unbindFamilyCards(ids, talentCardId.value, () => { isSubmitting.value = false; uni.showToast({ title: '已解除绑定', icon: 'success' }); setTimeout(() => safeBack('/pages/family-card/family-card'), 700) }, (error) => { isSubmitting.value = false; uni.showToast({ title: error.message, icon: 'none' }) }) |
| | | } |
| | | </script> |
| | | |
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { apiEnvironment } from '../../config/api.uts' |
| | | import { getFamilyCards } from '../../services/portal.uts' |
| | | import { getFamilyCards, getTalentCardProfile } from '../../services/portal.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28) |
| | |
| | | const loading = ref(true) |
| | | const loadError = ref(false) |
| | | const talentId = ref('') |
| | | const talentCardId = ref('') |
| | | const canUnbind = ref(false) |
| | | |
| | | onLoad((options: any) => { |
| | |
| | | navRight.value = systemInfo.windowWidth - capsule.right |
| | | // #endif |
| | | talentId.value = options != null && options['talentId'] != null ? options['talentId'] as string : (apiEnvironment == 'development' ? '2' : '') |
| | | loadFamilyCards() |
| | | resolveTalentCardId() |
| | | }) |
| | | onShow(() => { |
| | | if (talentId.value != '') loadFamilyCards() |
| | | if (talentCardId.value != '') loadFamilyCards() |
| | | }) |
| | | |
| | | const goBack = () => safeBack('/pages/profile/profile') |
| | | const loadFamilyCards = () => { |
| | | |
| | | const resolveTalentCardId = () => { |
| | | if (talentId.value == '') { loading.value = false; loadError.value = true; return } |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | | talentCardId.value = card != null && card.id != null ? '' + card.id : '' |
| | | if (talentCardId.value == '') { loading.value = false; loadError.value = true; return } |
| | | loadFamilyCards() |
| | | }, () => { loading.value = false; loadError.value = true }) |
| | | } |
| | | const loadFamilyCards = () => { |
| | | if (talentCardId.value == '') { loading.value = false; loadError.value = true; return } |
| | | loading.value = true |
| | | loadError.value = false |
| | | getFamilyCards(talentId.value, (data: any) => { familyMembers.value = data as any[]; canUnbind.value = familyMembers.value.some((item: any) => item.canUnbind === true); loading.value = false }, () => { loading.value = false; loadError.value = true }) |
| | | getFamilyCards(talentCardId.value, (data: any) => { |
| | | const items = (data as any[]) || [] |
| | | familyMembers.value = items.map((item: any) => ({ |
| | | id: item.id, |
| | | name: item.dependentName != null ? item.dependentName : '', |
| | | initial: item.dependentName != null && item.dependentName.length > 0 ? item.dependentName.substring(0, 1) : '', |
| | | relationship: item.relation != null ? item.relation : '', |
| | | mobile: item.dependentMobile != null ? item.dependentMobile : '', |
| | | statusText: item.statusText != null ? item.statusText : '', |
| | | status: item.status, |
| | | canUnbind: item.status === 1 |
| | | })) |
| | | canUnbind.value = familyMembers.value.some((item: any) => item.canUnbind === true) |
| | | loading.value = false |
| | | }, () => { loading.value = false; loadError.value = true }) |
| | | } |
| | | const addApplication = () => uni.navigateTo({ url: '/pages/family-card-apply/family-card-apply?talentId=' + encodeURIComponent(talentId.value) }) |
| | | const unbindFamily = () => { |
| | |
| | | const discount = ref('') |
| | | const applicableCards = ref('') |
| | | const address = ref('') |
| | | const heroImage = ref('/static/stay-assets/hotel-hampton.jpg') |
| | | const heroImage = ref('') |
| | | const benefitDetail = ref('') |
| | | const merchantPhone = ref('') |
| | | const specialistPhone = ref('') |
| | |
| | | discount.value = data.benefitSummary || '请咨询服务专员' |
| | | applicableCards.value = data.applicableCards || '请咨询服务专员' |
| | | address.value = data.address || '暂未配置机构地址' |
| | | heroImage.value = resolveApiUrl(data.coverUrl || '/static/stay-assets/hotel-hampton.jpg') |
| | | heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : '' |
| | | benefitDetail.value = data.benefitDetail || '以服务专员说明为准' |
| | | merchantPhone.value = data.contactMobile || '' |
| | | longitude.value = Number(data.longitude) |
| | |
| | | }, () => { detailLoading.value = false; detailError.value = true }) |
| | | } |
| | | const goBack = () => safeBack('/pages/premium-benefits/premium-benefits') |
| | | const replaceHero = () => { heroImage.value = '/static/common/image-fallback.svg' } |
| | | const replaceHero = () => { heroImage.value = '' } |
| | | const openLocation = () => { if (!canNavigate.value) { uni.showToast({ title: '暂未配置机构位置', icon: 'none' }); return }; uni.openLocation({ latitude: latitude.value, longitude: longitude.value, name: merchantName.value, address: address.value, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) } |
| | | const callMerchant = () => { if (merchantPhone.value == '') return; uni.makePhoneCall({ phoneNumber: merchantPhone.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) } |
| | | const contactSpecialist = () => { |
| | |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const isSolidNav = ref(false) |
| | | const packageId = ref(''); const hospitalId = 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('/static/stay-assets/hotel-vienna.jpg'); const freeLabel = ref('免费'); const applicableCards = ref(''); const packageAvailable = ref(false); const unavailableReason = ref('该套餐暂不可预约') |
| | | 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 canNavigate = computed(() => latitude.value > 0 && longitude.value > 0) |
| | | const canBook = computed(() => !detailLoading.value && !detailError.value && packageAvailable.value) |
| | |
| | | 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 = resolveApiUrl(data.heroImage || '/static/stay-assets/hotel-vienna.jpg'); freeLabel.value = data.freeLabel || '免费'; applicableCards.value = data.applicableCards || '以医院说明为准'; 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 || '免费'; applicableCards.value = data.applicableCards || '以医院说明为准'; 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 }) |
| | | } |
| | | 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 = '/static/common/image-fallback.svg' } |
| | | const replaceHero = () => { heroImage.value = '' } |
| | | 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) }) } |
| | |
| | | const merchantId = ref('merchant-food-default') |
| | | const merchantType = ref('food') |
| | | const merchantName = ref('百杏厨房(中环天地店)') |
| | | const merchantHero = ref('/static/food-assets/food-1.jpg') |
| | | const fallbackImage = '/static/common/image-fallback.svg' |
| | | const replaceHeroImage = () => { merchantHero.value = fallbackImage } |
| | | const replaceImage = (item: any) => { if (item.image != fallbackImage) item.image = fallbackImage } |
| | | const merchantHero = ref('') |
| | | const fallbackImage = '' |
| | | const replaceHeroImage = () => { merchantHero.value = '' } |
| | | const replaceImage = (item: any) => { if (item.image != '') item.image = '' } |
| | | const merchantPrimaryTag = ref('券') |
| | | const merchantDiscount = ref('8.8折') |
| | | const merchantAddress = ref('四川省攀枝花市东区炳三区新福路20号中环天地负一层') |
| | |
| | | merchantType.value = 'medical' |
| | | isMedicalMerchant.value = true |
| | | merchantName.value = name |
| | | merchantHero.value = '/static/stay-assets/hotel-hampton.jpg' |
| | | merchantHero.value = '' |
| | | merchantPrimaryTag.value = '免费' |
| | | merchantDiscount.value = '' |
| | | merchantAddress.value = '攀枝花市东区益康街34号4号综合3楼体检中心' |
| | |
| | | if (routedMerchantType == 'parking' || name.indexOf('停车') >= 0) { |
| | | isParkingMerchant.value = true |
| | | merchantName.value = '停车服务' |
| | | merchantHero.value = '/static/home-assets/city-intro.jpg' |
| | | merchantHero.value = '' |
| | | merchantPrimaryTag.value = '免费' |
| | | merchantDiscount.value = '' |
| | | merchantAddress.value = '攀枝花市东区渡口公司管辖停车场' |
| | |
| | | usageInstructions.value = '登记信息后可享受渡口公司管辖的49处停车场免费停车服务' |
| | | } else { |
| | | merchantName.value = name == '加油服务' ? '中国石油东区新福路加油站' : name |
| | | merchantHero.value = '/static/home-assets/top.jpg' |
| | | merchantHero.value = '' |
| | | merchantPrimaryTag.value = '券' |
| | | merchantDiscount.value = '9.4折' |
| | | merchantAddress.value = '四川省攀枝花市东区新福路20号附3-21号' |
| | |
| | | isHotelMerchant.value = true |
| | | merchantType.value = 'hotel' |
| | | merchantName.value = name |
| | | merchantHero.value = routedImage == '' ? '/static/stay-assets/hotel-hampton.jpg' : routedImage |
| | | merchantHero.value = routedImage |
| | | merchantDiscount.value = routedDiscount == '' ? '6折' : routedDiscount |
| | | hasFreeVoucher.value = merchantDiscount.value.indexOf('免费') >= 0 |
| | | merchantPrimaryTag.value = hasFreeVoucher.value ? '券' : '惠' |
| | |
| | | const talentId = ref('') |
| | | const loading = ref(true) |
| | | const loadError = ref(false) |
| | | const productCoverUrl = ref('/static/home-assets/top.jpg') |
| | | const productCoverUrl = ref('') |
| | | const productName = ref('') |
| | | const originalAmount = ref('0.00') |
| | | const plannedPayAmount = ref('0.00') |
| | |
| | | loading.value = true |
| | | loadError.value = false |
| | | getCancelledOrder(orderId.value, talentId.value, (data: any) => { |
| | | productCoverUrl.value = resolveApiUrl(data.productCoverUrlSnapshot || '/static/home-assets/top.jpg') |
| | | productCoverUrl.value = data.productCoverUrlSnapshot != null && data.productCoverUrlSnapshot != '' ? resolveApiUrl(data.productCoverUrlSnapshot as string) : '' |
| | | productName.value = data.productNameSnapshot || '人才权益服务' |
| | | originalAmount.value = Number(data.originalAmount).toFixed(2) |
| | | plannedPayAmount.value = Number(data.payAmount).toFixed(2) |
| | |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const windowWidth = ref(375) |
| | | const isSolidNav = ref(false); const fallbackImage = '/static/common/image-fallback.svg'; const fallbackScenes = [{ id: 'fallback-1', name: '臻享礼遇', image: '/static/stay-assets/hotel-hampton.jpg' }, { id: 'fallback-2', name: '人才服务', image: '/static/stay-assets/hotel-vienna.jpg' }] |
| | | const isSolidNav = ref(false); const fallbackImage = ''; const fallbackScenes = [] |
| | | const topicTitle = ref('臻享礼遇'); const topicDescription = ref(''); const topicError = ref(false); const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>(fallbackScenes[0]); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644 + fallbackScenes.length * 137) |
| | | const serviceTabs = ref<any[]>([]); const activeTab = ref(''); const visibleServices = ref<any[]>([]); const initialLoading = ref(true); const loadingMore = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const initialError = ref(false); const loadMoreError = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false) |
| | | |
| | |
| | | const merchantId = ref('') |
| | | const productId = ref('') |
| | | const productName = ref('【川味盛宴】川菜12人餐') |
| | | const productImage = ref('/static/food-assets/food-1.jpg') |
| | | const fallbackImage = '/static/common/image-fallback.svg' |
| | | const replaceProductImage = () => { productImage.value = fallbackImage } |
| | | const productImage = ref('') |
| | | const fallbackImage = '' |
| | | const replaceProductImage = () => { productImage.value = '' } |
| | | const productPrice = ref('138') |
| | | const productOriginalPrice = ref('266') |
| | | const productDiscount = ref('6折') |
| | |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10); const windowWidth = ref(375) |
| | | const isSolidNav = ref(false); const fallbackImage = '/static/common/image-fallback.svg' |
| | | const fallbackScenes = [{ id: 'fallback-1', name: '惠享好物', image: '/static/home-assets/top.jpg' }, { id: 'fallback-2', name: '品质生活', image: '/static/home-assets/city-intro.jpg' }] |
| | | const isSolidNav = ref(false); const fallbackImage = '' |
| | | const fallbackScenes: any[] = [] |
| | | const topicTitle = ref('惠享好物'); const topicDescription = ref(''); const topicError = ref(false) |
| | | const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>(fallbackScenes[0]); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644 + fallbackScenes.length * 137) |
| | | const scenes = ref<any[]>(fallbackScenes); const activeIndex = ref(0); const activeScene = ref<any>({ id: '', name: '', image: '' }); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644) |
| | | const serviceTabs = ref<any[]>([]); const activeTab = ref(''); const visibleServices = ref<any[]>([]) |
| | | const initialLoading = ref(true); const loadingMore = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const initialError = ref(false); const loadMoreError = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false) |
| | | |
| | |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | |
| | | const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10) |
| | | const merchantId = ref(''); const heroImage = ref('/static/common/image-fallback.svg'); const merchantName = ref(''); const discount = ref(''); const applicableCards = ref(''); const address = ref(''); const benefitDetail = ref('') |
| | | const merchantId = ref(''); const heroImage = ref(''); const merchantName = ref(''); const discount = ref(''); const applicableCards = ref(''); const address = ref(''); const benefitDetail = ref('') |
| | | const merchantPhone = ref(''); const latitude = ref(0); const longitude = ref(0); const available = ref(false); const unavailableReason = ref('该购物服务当前不可用'); const advisorAvailable = ref(false); const advisorPhone = ref('') |
| | | const detailLoading = ref(true); const detailError = ref(false) |
| | | const canNavigate = () => latitude.value != 0 && longitude.value != 0 |
| | |
| | | if (options != null && options['merchantId'] != null) merchantId.value = decodeURIComponent(options['merchantId'] as string) |
| | | loadMerchantDetail() |
| | | }) |
| | | const replaceHeroImage = () => { heroImage.value = '/static/common/image-fallback.svg' } |
| | | const replaceHeroImage = () => { heroImage.value = '' } |
| | | const loadMerchantDetail = () => { |
| | | detailLoading.value = true; detailError.value = false; available.value = false |
| | | if (merchantId.value == '' || !isApiConfigured()) { detailLoading.value = false; detailError.value = true; return } |
| | |
| | | discount.value = data.discountText != null ? data.discountText as string : '请咨询服务专员' |
| | | applicableCards.value = data.applicableCards != null ? data.applicableCards as string : '请咨询服务专员' |
| | | address.value = data.address != null ? data.address as string : '请咨询服务专员' |
| | | heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : '/static/common/image-fallback.svg' |
| | | heroImage.value = data.coverUrl != null && data.coverUrl != '' ? resolveApiUrl(data.coverUrl as string) : '' |
| | | benefitDetail.value = data.benefitDetail != null ? data.benefitDetail as string : '以服务专员说明为准' |
| | | merchantPhone.value = data.contactMobile != null ? data.contactMobile as string : '' |
| | | latitude.value = data.latitude != null ? Number(data.latitude) : 0; longitude.value = data.longitude != null ? Number(data.longitude) : 0 |
| | |
| | | import { safeBack } from '../../utils/navigation.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 = '/static/stay-assets/hotel-vienna.jpg' |
| | | 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) |
| | | |
| | |
| | | import { safeBack } from '../../utils/navigation.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 = '/static/home-assets/top.jpg' |
| | | const fallbackScenes = [{ id: 'fallback-fuel', name: '人才加油服务', image: '/static/home-assets/top.jpg' }, { id: 'fallback-parking', name: '人才停车服务', image: '/static/home-assets/city-intro.jpg' }] |
| | | const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = '' |
| | | const fallbackScenes: any[] = [] |
| | | const topicTitle = ref('行--畅行无忧'); const topicDescription = ref('整合加油、停车、洗车等出行服务资源,为人才提供专属折扣、停车优惠和便捷洗车服务,全方位保障人才出行便捷。'); const topicError = ref(false) |
| | | const activeIndex = ref(0); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644 + fallbackScenes.length * 137); const scenes = ref<any[]>(fallbackScenes); const currentScene = ref(fallbackScenes[0]) |
| | | const activeIndex = ref(0); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644); const scenes = ref<any[]>(fallbackScenes); const currentScene = ref({ id: '', name: '', image: '' }) |
| | | const serviceTabs = [{ key: 'fuel', label: '加油服务' }, { key: 'parking', label: '停车服务' }, { key: 'carWash', label: '自助洗车服务' }] |
| | | const activeTab = ref('fuel'); 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 navRight = ref(10) |
| | | const isSolidNav = ref(false) |
| | | const windowWidth = ref(375) |
| | | const fallbackImage = '/static/home-assets/top.jpg' |
| | | const fallbackImage = '' |
| | | const activeIndex = ref(-1) |
| | | const sceneScrollLeft = ref(0) |
| | | const sceneTrackWidth = ref(644) |
| | |
| | | export const getCompletedOrder = (orderNo: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appCompletedOrders') + encodeURIComponent(orderNo), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | | export const getCancelledOrder = (orderNo: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appCancelledOrders') + encodeURIComponent(orderNo), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | | export const getProcessingApplication = (applicationNo: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appProcessingApplications') + encodeURIComponent(applicationNo), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | | export const getFamilyCards = (talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards'), method: 'GET', skipAuth: true, data: { talentId: talentId } }, onSuccess, onError) |
| | | export const getFamilyCards = (talentCardId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards'), method: 'GET', skipAuth: true, data: { talentCardId: Number(talentCardId) } }, onSuccess, onError) |
| | | export const getFreeBenefit = (talentId: string, merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFreeBenefits'), method: 'GET', skipAuth: true, data: { talentId: talentId, merchantId: merchantId } }, onSuccess, onError) |
| | | export const issueFreeBenefitCode = (talentId: string, merchantId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFreeBenefits') + '/dynamic-code', method: 'POST', skipAuth: true, data: { talentId: Number(talentId), merchantId: Number(merchantId) } }, onSuccess, onError) |
| | | export const createFamilyCardApplication = (payload: any, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards') + '/applications', method: 'POST', skipAuth: true, data: payload, idempotencyKey: 'family-card-apply-' + Date.now() }, onSuccess, onError) |
| | | export const unbindFamilyCard = (applicationId: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards') + '/' + encodeURIComponent(applicationId) + '/unbind', method: 'POST', skipAuth: true, data: { talentId: Number(talentId) }, idempotencyKey: 'family-card-unbind-' + applicationId }, onSuccess, onError) |
| | | export const unbindFamilyCards = (applicationIds: string[], talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards') + '/unbind', method: 'POST', skipAuth: true, data: { talentId: Number(talentId), applicationIds: applicationIds.map((id: string) => Number(id)) }, idempotencyKey: 'family-card-unbind-' + applicationIds.join('-') }, onSuccess, onError) |
| | | export const createFamilyCardApplication = (talentCardId: string, dependentName: string, dependentIdCardNo: string, dependentMobile: string, relation: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards'), method: 'POST', skipAuth: true, data: { talentCardId: Number(talentCardId), dependentName: dependentName, dependentIdCardNo: dependentIdCardNo, dependentMobile: dependentMobile, relation: relation }, idempotencyKey: 'family-card-apply-' + talentCardId + '-' + Date.now() }, onSuccess, onError) |
| | | export const unbindFamilyCard = (applicationId: string, talentCardId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appFamilyCards') + '/' + encodeURIComponent(applicationId) + '/unbind', method: 'POST', skipAuth: true, data: { talentCardId: Number(talentCardId) }, idempotencyKey: 'family-card-unbind-' + applicationId }, onSuccess, onError) |
| | | export const unbindFamilyCards = (applicationIds: string[], talentCardId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => { |
| | | let index = 0 |
| | | const runNext = (): void => { |
| | | if (index >= applicationIds.length) { onSuccess(null); return } |
| | | const id = applicationIds[index] |
| | | index = index + 1 |
| | | unbindFamilyCard(id, talentCardId, () => runNext(), onError) |
| | | } |
| | | runNext() |
| | | return () => {} |
| | | } |
| | | export const issueOrderVerificationCode = (orderNo: string, talentId: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPendingUseOrders') + encodeURIComponent(orderNo) + '/verification-code', method: 'POST', skipAuth: true, data: { talentId: Number(talentId) } }, onSuccess, onError) |
| | | export const refundPendingUseOrder = (orderNo: string, talentId: string, reason: string, onSuccess: (data: any) => void, onError: (error: HttpError) => void): (() => void) => request<any>({ path: getApiEndpoint('appPendingUseOrders') + encodeURIComponent(orderNo) + '/refund', method: 'POST', skipAuth: true, data: { talentId: Number(talentId), reason: reason }, idempotencyKey: 'refund-' + orderNo }, onSuccess, onError) |
| | |
| | | submitting.value = true |
| | | const contactName = merchantType.value == 'enterprise' ? administratorName.value.trim() : subjectName.value.trim() |
| | | const contactMobile = merchantType.value == 'enterprise' ? administratorPhone.value.trim() : personalPhone.value.trim() |
| | | submitOnboardingRemote({ subjectType: merchantType.value, subjectName: subjectName.value.trim(), creditCode: creditCode.value.trim(), subjectIdNumber: subjectIdNumber.value.trim(), registeredRegion: registeredRegion.value.trim(), registeredAddress: registeredAddress.value.trim(), contactName: contactName, contactMobile: contactMobile }, 'onboarding-' + Date.now(), () => { |
| | | submitOnboardingRemote({ subjectType: merchantType.value, subjectName: subjectName.value.trim(), creditCode: creditCode.value.trim(), subjectIdNumber: subjectIdNumber.value.trim(), subjectCertificateValidityType: subjectValidity.value == '长期' ? 'long_term' : 'fixed', subjectCertificateBeginDate: subjectStartDate.value, registeredRegion: registeredRegion.value.trim(), registeredAddress: registeredAddress.value.trim(), contactName: contactName, contactMobile: contactMobile }, 'onboarding-' + Date.now(), () => { |
| | | submitting.value = false |
| | | uni.redirectTo({ url: '/pages/merchant-onboarding-pending/merchant-onboarding-pending' }) |
| | | }, (error: any) => { |
| | |
| | | meta: { title: '人才政策管理-发布', businessKind: 'talentPolicy' }, |
| | | }, |
| | | { |
| | | path: 'talent-policy-categories', |
| | | path: 'talent-policy-categories/:id', |
| | | name: 'talent-policy-categories', |
| | | component: () => import('../views/TalentPolicyEditView.vue'), |
| | | meta: { title: '人才政策管理-修改', businessKind: 'talentPolicyCategory' }, |
| | |
| | | conditionAmount?: number |
| | | couponAmount?: number |
| | | talentLevels?: string |
| | | talentLevelQuantities?: Record<string, number> | string |
| | | validStart?: string |
| | | validEnd?: string |
| | | productScope?: string |
| | |
| | | conditionAmount?: number |
| | | couponAmount?: number |
| | | talentLevels: string[] |
| | | talentLevelQuantities: Record<string, number> |
| | | validStart?: string |
| | | validEnd?: string |
| | | productScope: string |
| | |
| | | const merchantCoupons = ref<Coupon[]>([]) |
| | | const couponProducts = ref<Product[]>([]) |
| | | const selectedCouponProductIds = ref<Array<number | string>>([]) |
| | | const couponLevelQuantities = reactive<Record<string, number>>({}) |
| | | const couponTalentOptions = ['卓越人才卡', '杰出人才卡', '菁英人才卡'] |
| | | |
| | | const businessHours = reactive<BusinessHour[]>([{ id: 1, value: [] }]) |
| | | |
| | |
| | | couponEditForm.governmentSubsidy = true |
| | | couponEditForm.status = 1 |
| | | selectedCouponProductIds.value = [] |
| | | Object.keys(couponLevelQuantities).forEach((key) => delete couponLevelQuantities[key]) |
| | | couponEditForm.talentLevels.forEach((level) => { couponLevelQuantities[level] = 1 }) |
| | | } |
| | | |
| | | function parseTalentLevels(value?: string) { |
| | |
| | | couponEditForm.couponAmount = |
| | | detail.couponAmount === undefined || detail.couponAmount === null ? '' : String(detail.couponAmount) |
| | | couponEditForm.talentLevels = parseTalentLevels(detail.talentLevels) |
| | | Object.keys(couponLevelQuantities).forEach((key) => delete couponLevelQuantities[key]) |
| | | let savedQuantities: Record<string, number> = {} |
| | | try { savedQuantities = typeof detail.talentLevelQuantities === 'string' ? JSON.parse(detail.talentLevelQuantities) : (detail.talentLevelQuantities || {}) } catch { savedQuantities = {} } |
| | | couponEditForm.talentLevels.forEach((level) => { couponLevelQuantities[level] = Number(savedQuantities[level] || 1) }) |
| | | couponEditForm.validTime = detail.validStart && detail.validEnd ? [detail.validStart, detail.validEnd] : [] |
| | | couponEditForm.productScope = detail.productScope === 'ALL' ? 'all' : 'selected' |
| | | couponEditForm.storeConsume = detail.storeConsume === true || detail.storeConsume === 1 |
| | |
| | | selectedCouponProductIds.value = selectedCouponProductIds.value.filter((id) => String(id) !== String(row.id)) |
| | | } |
| | | |
| | | function toggleCouponTalentLevel(level: string, enabled: boolean) { |
| | | if (enabled) { |
| | | if (!couponEditForm.talentLevels.includes(level)) couponEditForm.talentLevels.push(level) |
| | | if (!couponLevelQuantities[level]) couponLevelQuantities[level] = 1 |
| | | return |
| | | } |
| | | couponEditForm.talentLevels = couponEditForm.talentLevels.filter((item) => item !== level) |
| | | } |
| | | |
| | | function couponPayload(): CouponPayload { |
| | | return { |
| | | merchantId: currentMerchantId(), |
| | |
| | | : Number(couponEditForm.fullAmount), |
| | | couponAmount: couponEditForm.couponAmount === '' ? undefined : Number(couponEditForm.couponAmount), |
| | | talentLevels: couponEditForm.talentLevels, |
| | | talentLevelQuantities: Object.fromEntries(couponEditForm.talentLevels.map((level) => [level, Number(couponLevelQuantities[level] || 1)])), |
| | | validStart: couponEditForm.validTime?.[0], |
| | | validEnd: couponEditForm.validTime?.[1], |
| | | productScope: couponEditForm.productScope === 'all' ? 'ALL' : 'SELECTED', |
| | |
| | | return |
| | | } |
| | | const couponAmount = Number(couponEditForm.couponAmount) |
| | | if (couponEditForm.couponAmount === '' || !Number.isFinite(couponAmount) || couponAmount <= 0) { |
| | | ElMessage.warning('请输入大于0的优惠券金额') |
| | | if (couponEditForm.couponAmount === '' || !Number.isFinite(couponAmount) || couponAmount < 0.1 || couponAmount > 9999.9) { |
| | | ElMessage.warning('优惠券金额必须在0.1-9999.9元之间') |
| | | return |
| | | } |
| | | if (couponEditForm.useRequirement === 'full' && couponEditForm.fullAmount === '') { |
| | |
| | | |
| | | <div class="coupon-edit-row"> |
| | | <label>可用人才</label> |
| | | <el-checkbox-group v-model="couponEditForm.talentLevels" class="coupon-talent-checkboxes"> |
| | | <el-checkbox-group v-model="couponEditForm.talentLevels" class="coupon-talent-checkboxes" style="display: none"> |
| | | <el-checkbox label="卓越人才卡">卓越人才卡</el-checkbox> |
| | | <el-checkbox label="杰出人才卡">杰出人才卡</el-checkbox> |
| | | <el-checkbox label="菁英人才卡">菁英人才卡</el-checkbox> |
| | | </el-checkbox-group> |
| | | <div class="coupon-talent-rows" style="display: grid; gap: 22px; padding: 4px 0"> |
| | | <div v-for="level in couponTalentOptions" :key="level" class="coupon-talent-row" style="display: flex; align-items: center; gap: 18px; min-height: 32px"> |
| | | <strong style="width: 90px; font-weight: 500">{{ level }}</strong> |
| | | <el-switch :model-value="couponEditForm.talentLevels.includes(level)" @change="(enabled: boolean) => toggleCouponTalentLevel(level, enabled)" /> |
| | | <el-input v-model="couponLevelQuantities[level]" :disabled="!couponEditForm.talentLevels.includes(level)" placeholder="请输入" style="width: 108px" /> |
| | | <span>张</span> |
| | | </div> |
| | | </div> |
| | | <div v-for="level in couponEditForm.talentLevels" :key="level" class="coupon-level-quantity" style="display: none"> |
| | | <span>{{ level }}</span> |
| | | <el-input-number v-model="couponLevelQuantities[level]" :min="1" :max="99" :precision="0" :step="1" controls-position="right" /> |
| | | <span>张</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="coupon-edit-row"> |
| | |
| | | |
| | | type Kind = 'onboardings' | 'bank-cards' | 'withdrawals' |
| | | |
| | | const kind = ref<Kind>('withdrawals') |
| | | const kind = ref<Kind>('onboardings') |
| | | const loading = ref(false) |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const total = ref(0) |
| | |
| | | const pageSize = ref(20) |
| | | const reviewVisible = ref(false) |
| | | const reviewing = ref<Record<string, any> | null>(null) |
| | | const onboardingDetailVisible = ref(false) |
| | | const onboardingDetail = ref<Record<string, any> | null>(null) |
| | | |
| | | const filters = reactive({ |
| | | merchantName: '', |
| | |
| | | } |
| | | |
| | | function openReview(row: Record<string, any>) { |
| | | if (kind.value === 'onboardings') { |
| | | onboardingDetail.value = row |
| | | onboardingDetailVisible.value = true |
| | | return |
| | | } |
| | | reviewing.value = row |
| | | reviewVisible.value = true |
| | | } |
| | |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | <el-dialog v-model="onboardingDetailVisible" title="商户进件详情" width="560px" :close-on-click-modal="false"> |
| | | <div v-if="onboardingDetail" class="detail-grid"> |
| | | <div><span>进件编号</span><strong>{{ onboardingDetail.applicationNo || '-' }}</strong></div> |
| | | <div><span>进件状态</span><el-tag :type="statusType(onboardingDetail.status)" effect="light">{{ statusLabel(onboardingDetail.status) }}</el-tag></div> |
| | | <div><span>商家名称</span><strong>{{ onboardingDetail.merchantName || '-' }}</strong></div> |
| | | <div><span>商户类型</span><strong>{{ onboardingDetail.merchantType || '-' }}</strong></div> |
| | | <div><span>进件主体</span><strong>{{ onboardingDetail.subjectName || '-' }}</strong></div> |
| | | <div><span>主体类型</span><strong>{{ onboardingDetail.subjectType === 'enterprise' ? '企业' : '个人' }}</strong></div> |
| | | <div><span>统一社会信用代码</span><strong>{{ onboardingDetail.creditCode || '-' }}</strong></div> |
| | | <div><span>联系人电话</span><strong>{{ onboardingDetail.contactMobile || '-' }}</strong></div> |
| | | <div class="detail-full"><span>汇付反馈</span><strong>{{ onboardingDetail.rejectReason || '等待汇付审核结果' }}</strong></div> |
| | | </div> |
| | | <template #footer><el-button type="primary" @click="onboardingDetailVisible = false">关闭</el-button></template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <style scoped> |
| | |
| | | .review-summary div { display: flex; justify-content: space-between; gap: 16px; } |
| | | .review-summary span { color: #909399; } |
| | | .review-summary strong { color: #303133; font-weight: 500; text-align: right; } |
| | | .detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; } |
| | | .detail-grid > div { display: grid; gap: 5px; } |
| | | .detail-grid span { color: #909399; font-size: 13px; } |
| | | .detail-grid strong { color: #303133; font-weight: 500; word-break: break-all; } |
| | | .detail-full { grid-column: 1 / -1; } |
| | | @media (max-width: 1280px) { .query-form { grid-template-columns: repeat(2, minmax(280px, 1fr)); } } |
| | | </style> |
| New file |
| | |
| | | -- Run only when an existing local MySQL database failed V92__admin_approval_organization_extra_info.sql. |
| | | -- V92 may have added columns before failing on the final update statement; delete the failed |
| | | -- Flyway history row so the fixed idempotent migration can run again. |
| | | |
| | | delete from flyway_schema_history |
| | | where version = '92' and success = 0; |
| | |
| | | import java.util.Map; |
| | | |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.business.merchant.auth.MerchantPasswordHasher; |
| | |
| | | ) { |
| | | StringBuilder sql = new StringBuilder(""" |
| | | select c.id, c.merchant_id, m.merchant_name, c.coupon_name, c.condition_type, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.valid_start, c.valid_end, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.talent_level_quantities, c.valid_start, c.valid_end, |
| | | c.product_scope, c.store_consume, c.government_subsidy, c.use_count, c.status, |
| | | c.created_at, c.updated_at, |
| | | (select count(*) from merchant_coupon_product cp where cp.coupon_id = c.id) as product_count, |
| | |
| | | public Map<String, Object> coupon(Long id) { |
| | | Map<String, Object> result = one(""" |
| | | select c.id, c.merchant_id, m.merchant_name, c.coupon_name, c.condition_type, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.valid_start, c.valid_end, |
| | | c.condition_amount, c.coupon_amount, c.talent_levels, c.talent_level_quantities, c.valid_start, c.valid_end, |
| | | c.product_scope, c.store_consume, c.government_subsidy, c.use_count, c.status, |
| | | c.created_at, c.updated_at |
| | | from merchant_coupon c |
| | |
| | | public Map<String, Object> createCoupon(CouponCreateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateCouponConfiguration(request.validStart(), request.validEnd(), request.productScope(), request.productIds()); |
| | | validateCouponAmount(request.couponAmount()); |
| | | validateTalentLevelQuantities(request.talentLevelQuantities()); |
| | | jdbcTemplate.update(""" |
| | | insert into merchant_coupon(merchant_id, coupon_name, condition_type, condition_amount, |
| | | coupon_amount, talent_levels, valid_start, valid_end, |
| | | coupon_amount, talent_levels, talent_level_quantities, valid_start, valid_end, |
| | | product_scope, store_consume, government_subsidy, status) |
| | | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) |
| | | values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1) |
| | | """, |
| | | request.merchantId(), |
| | | request.couponName(), |
| | |
| | | request.conditionAmount(), |
| | | valueOrZero(request.couponAmount()), |
| | | joinTalentLevels(request.talentLevels()), |
| | | levelQuantitiesJson(request.talentLevelQuantities()), |
| | | blankToNull(request.validStart()), |
| | | blankToNull(request.validEnd()), |
| | | normalizeProductScope(request.productScope()), |
| | |
| | | public Map<String, Object> updateCoupon(Long id, CouponUpdateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateCouponConfiguration(request.validStart(), request.validEnd(), request.productScope(), request.productIds()); |
| | | validateCouponAmount(request.couponAmount()); |
| | | validateTalentLevelQuantities(request.talentLevelQuantities()); |
| | | int updated = jdbcTemplate.update(""" |
| | | update merchant_coupon |
| | | set merchant_id = ?, coupon_name = ?, condition_type = ?, condition_amount = ?, |
| | | coupon_amount = ?, talent_levels = ?, valid_start = ?, valid_end = ?, |
| | | coupon_amount = ?, talent_levels = ?, talent_level_quantities = ?, valid_start = ?, valid_end = ?, |
| | | product_scope = ?, store_consume = ?, government_subsidy = ?, status = ? |
| | | where deleted = 0 and id = ? |
| | | """, |
| | |
| | | request.conditionAmount(), |
| | | valueOrZero(request.couponAmount()), |
| | | joinTalentLevels(request.talentLevels()), |
| | | levelQuantitiesJson(request.talentLevelQuantities()), |
| | | blankToNull(request.validStart()), |
| | | blankToNull(request.validEnd()), |
| | | normalizeProductScope(request.productScope()), |
| | |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private void validateCouponAmount(BigDecimal value) { |
| | | if (value == null || value.compareTo(new BigDecimal("0.1")) < 0 || value.compareTo(new BigDecimal("9999.9")) > 0) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "优惠券金额必须在0.1-9999.9元之间"); |
| | | } |
| | | } |
| | | private void validateTalentLevelQuantities(Map<String, Integer> values) { |
| | | if (values == null) return; |
| | | values.values().forEach(value -> { if (value == null || value < 1 || value > 99) throw new BusinessException(ErrorCode.BAD_REQUEST, "人才卡发放数量必须为1-99"); }); |
| | | } |
| | | private String levelQuantitiesJson(Map<String, Integer> values) { return values == null || values.isEmpty() ? null : JSON.toJSONString(values); } |
| | | |
| | | private String toJson(Object value) { |
| | | if (value == null) { |
| | | return null; |
| | |
| | | @GetMapping("/merchant-settlement/onboardings") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> onboardings(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.onboardings(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); } |
| | | @GetMapping("/merchant-settlement/bank-cards") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> bankCards(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.bankCards(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); } |
| | | @GetMapping("/merchant-settlement/withdrawals") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> withdrawals(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.withdrawals(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); } |
| | | @PatchMapping("/merchant-settlement/onboardings/{id}") public ApiResponse<Map<String,Object>> reviewOnboarding(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(settlementService.reviewOnboarding(id, body.get("status"), body.get("reason"), body.get("huifuId"))); } |
| | | @PatchMapping("/merchant-settlement/bank-cards/{id}") public ApiResponse<Map<String,Object>> reviewBankCard(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(settlementService.reviewBankCard(id, body.get("status"), body.get("reason"), body.get("tokenNo"))); } |
| | | @PatchMapping("/merchant-settlement/withdrawals/{id}") public ApiResponse<Map<String,Object>> reviewWithdrawal(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(settlementService.reviewWithdrawal(id, body.get("status"), body.get("reason"))); } |
| | | @GetMapping("/payment-reconciliation/issues") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> reconciliationIssues(@RequestParam(defaultValue = "open") String status, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return ApiResponse.success(reconciliationService.issues(status, page, size)); } |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.DecimalMax; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | |
| | | String conditionType, |
| | | BigDecimal conditionAmount, |
| | | @NotNull |
| | | @DecimalMin(value = "0.01") |
| | | @DecimalMin(value = "0.1") @DecimalMax(value = "9999.9") |
| | | BigDecimal couponAmount, |
| | | List<String> talentLevels, |
| | | Map<String, Integer> talentLevelQuantities, |
| | | String validStart, |
| | | String validEnd, |
| | | @NotBlank |
| | |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.DecimalMax; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | |
| | | String conditionType, |
| | | BigDecimal conditionAmount, |
| | | @NotNull |
| | | @DecimalMin(value = "0.01") |
| | | @DecimalMin(value = "0.1") @DecimalMax(value = "9999.9") |
| | | BigDecimal couponAmount, |
| | | List<String> talentLevels, |
| | | Map<String, Integer> talentLevelQuantities, |
| | | String validStart, |
| | | String validEnd, |
| | | @NotBlank |
| | |
| | | return page(select, count, "w", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate); |
| | | } |
| | | |
| | | @Transactional public Map<String, Object> reviewOnboarding(Long id, String status, String reason, String huifuId) { return reviewOnboardingInternal(id, status, reason, huifuId); } |
| | | @Transactional public Map<String, Object> reviewBankCard(Long id, String status, String reason, String tokenNo) { return reviewBankCardInternal(id, status, reason, tokenNo); } |
| | | public Map<String, Object> reviewWithdrawal(Long id, String status, String reason) { if ("paid".equals(normalizeWithdrawalStatus(status))) return withdrawalService.dispatch(id); return update("merchant_withdrawal", id, "failed", reason, "withdrawal"); } |
| | | |
| | | private Map<String,Object> reviewOnboardingInternal(Long id,String status,String reason,String huifuId) { String value=reviewStatus(status); if("approved".equals(value) && (huifuId==null || !huifuId.trim().matches("[A-Za-z0-9]{1,32}"))) throw new BusinessException(ErrorCode.BAD_REQUEST,"Huifu merchant ID is required when approving onboarding"); int affected=jdbc.update("update merchant_onboarding_application set status=?, reject_reason=?, provider_huifu_id=?, reviewed_at=now() where id=? and status='pending'",value,reason==null?"":reason.trim(),"approved".equals(value)?huifuId.trim():null,id); if(affected==0)throw new BusinessException(ErrorCode.CONFLICT,"Onboarding is already handled or does not exist");return Map.of("id",id,"status",value); } |
| | | private Map<String,Object> reviewBankCardInternal(Long id,String status,String reason,String tokenNo) { String value=reviewStatus(status); if("approved".equals(value) && (tokenNo==null || !tokenNo.trim().matches("[A-Za-z0-9]{1,32}"))) throw new BusinessException(ErrorCode.BAD_REQUEST,"Huifu withdrawal card token is required when approving a bank card"); int affected=jdbc.update("update merchant_bank_card set status=?, reject_reason=?, provider_token_no=?, reviewed_at=now() where id=? and status='pending'",value,reason==null?"":reason.trim(),"approved".equals(value)?tokenNo.trim():null,id); if(affected==0)throw new BusinessException(ErrorCode.CONFLICT,"Bank card is already handled or does not exist");return Map.of("id",id,"status",value); } |
| | | private String reviewStatus(String status) { String value=status==null?"":status.trim().toLowerCase(); if(!List.of("approved","rejected").contains(value))throw new BusinessException(ErrorCode.BAD_REQUEST,"Invalid review status");return value; } |
| | | private String normalizeWithdrawalStatus(String status) { String value=status==null?"":status.trim().toLowerCase(); if(!List.of("paid","failed").contains(value))throw new BusinessException(ErrorCode.BAD_REQUEST,"Invalid withdrawal status");return value; } |
| | |
| | | where mobile = ? and deleted = 0 and status = 1 |
| | | limit 1 for update |
| | | """, mobile); |
| | | if (talents.isEmpty()) throw new BusinessException(ErrorCode.FORBIDDEN, "当前手机号未录入人才服务名单,请联系管理员"); |
| | | if (talents.isEmpty()) return issueSession(registerVisitor(openId, mobile)); |
| | | Map<String, Object> talent = talents.get(0); |
| | | long talentId = number(talent.get("id")); |
| | | List<Map<String, Object>> binding = rows("select id from talent_user where openid = ? and deleted = 0 limit 1 for update", openId); |
| | | List<Map<String, Object>> binding = rows(""" |
| | | select t.id, |
| | | exists(select 1 from talent_card c where c.talent_id = t.id and c.deleted = 0 and c.card_type = 'PERSONAL') has_talent_card |
| | | from talent_user t |
| | | where t.openid = ? and t.deleted = 0 |
| | | limit 1 for update |
| | | """, openId); |
| | | if (!binding.isEmpty() && number(binding.get(0).get("id")) != talentId) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "当前微信已绑定其他人才账号,请联系管理员"); |
| | | if (number(binding.get(0).get("hasTalentCard")) != 0) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "当前微信已绑定其他人才账号,请联系管理员"); |
| | | } |
| | | jdbc.update("update talent_user set openid = null where id = ?", number(binding.get(0).get("id"))); |
| | | } |
| | | jdbc.update("update talent_user set openid = ? where id = ?", openId, talentId); |
| | | return issueSession(talentId); |
| | | } |
| | | |
| | | private long registerVisitor(String openId, String mobile) { |
| | | List<Map<String, Object>> bindings = rows(""" |
| | | select id |
| | | from talent_user |
| | | where openid = ? and deleted = 0 and status = 1 |
| | | limit 1 for update |
| | | """, openId); |
| | | if (!bindings.isEmpty()) { |
| | | long talentId = number(bindings.get(0).get("id")); |
| | | jdbc.update("update talent_user set mobile = ? where id = ?", mobile, talentId); |
| | | return talentId; |
| | | } |
| | | jdbc.update(""" |
| | | insert into talent_user(openid, name, mobile, talent_level, source_area_type, status) |
| | | values (?, '普通游客', ?, '普通游客', 'LOCAL', 1) |
| | | """, openId, mobile); |
| | | Long talentId = jdbc.queryForObject("select last_insert_id()", Long.class); |
| | | if (talentId == null) throw new BusinessException(ErrorCode.INTERNAL_ERROR, "普通游客注册失败"); |
| | | return talentId; |
| | | } |
| | | |
| | | private Map<String, Object> issueSession(long talentId) { |
| | | jdbc.update("update talent_session set revoked_at = now() where talent_id = ? and revoked_at is null", talentId); |
| | | |
| | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | |
| | | } |
| | | |
| | | private List<Map<String, Object>> rules(Long merchantId, String cardLevel) { |
| | | List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel); |
| | | if (!configured.isEmpty()) return configured; |
| | | return rows(""" |
| | | select id, card_level, rule_name, discount_rate, subsidy_enabled |
| | | from merchant_store_consume_rule |
| | |
| | | }).toList(); |
| | | } |
| | | |
| | | private List<Map<String, Object>> configuredRules(Long merchantId, String cardLevel) { |
| | | List<Map<String, Object>> merchants = rows("select merchant_rights_config from merchant where id = ? and deleted = 0 limit 1", merchantId); |
| | | if (merchants.isEmpty()) return List.of(); |
| | | Object raw = merchants.get(0).get("merchantRightsConfig"); |
| | | if (raw == null || string(raw).isBlank()) return List.of(); |
| | | List<Map<String, Object>> configs = new ArrayList<>(); |
| | | try { |
| | | if (raw instanceof List<?> list) { |
| | | for (Object item : list) if (item instanceof Map<?, ?> map) { |
| | | Map<String, Object> value = new LinkedHashMap<>(); |
| | | for (Map.Entry<?, ?> entry : map.entrySet()) value.put(String.valueOf(entry.getKey()), entry.getValue()); |
| | | configs.add(value); |
| | | } |
| | | } else { |
| | | for (Object item : JSON.parseArray(string(raw), Map.class)) if (item instanceof Map<?, ?> map) { |
| | | Map<String, Object> value = new LinkedHashMap<>(); |
| | | for (Map.Entry<?, ?> entry : map.entrySet()) value.put(String.valueOf(entry.getKey()), entry.getValue()); |
| | | configs.add(value); |
| | | } |
| | | } |
| | | } catch (Exception ignored) { |
| | | return List.of(); |
| | | } |
| | | return configs.stream().filter(right -> booleanValue(right.get("enabled")) && booleanValue(right.get("consumeEnabled")) && booleanValue(right.get("storeConsume")) && matchesCardLevel(string(right.get("level")), cardLevel)).map(this::configuredRule).filter(rule -> decimal(rule.get("discountRate")).signum() > 0).toList(); |
| | | } |
| | | |
| | | private Map<String, Object> configuredRule(Map<String, Object> right) { |
| | | String level = normalizedCardLevel(string(right.get("level"))); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("id", configRuleId(level)); |
| | | result.put("cardLevel", level); |
| | | result.put("ruleName", string(right.get("tag")).isBlank() ? "到店消费优惠" : right.get("tag")); |
| | | result.put("name", result.get("ruleName")); |
| | | result.put("discountRate", configuredDiscountRate(right.get("discount"))); |
| | | result.put("subsidyEnabled", booleanValue(right.get("governmentSubsidy")) ? 1 : 0); |
| | | result.put("consumeDesc", right.get("consumeDesc")); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> rule(Long merchantId, String cardLevel, Long ruleId) { |
| | | List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel); |
| | | for (Map<String, Object> candidate : configured) if (String.valueOf(candidate.get("id")).equals(String.valueOf(ruleId))) return candidate; |
| | | List<Map<String, Object>> found = rows(""" |
| | | select id, card_level, rule_name, discount_rate, subsidy_enabled |
| | | from merchant_store_consume_rule |
| | |
| | | and (valid_end is null or valid_end >= current_date) |
| | | limit 1 |
| | | """, ruleId, merchantId); |
| | | if (found.isEmpty() || !matchesCardLevel(string(found.get(0).get("cardLevel")), cardLevel)) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不可使用所选优惠方式"); |
| | | } |
| | | if (found.isEmpty() || !matchesCardLevel(string(found.get(0).get("cardLevel")), cardLevel)) throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡不可使用所选优惠方式"); |
| | | return found.get(0); |
| | | } |
| | | |
| | | private long configRuleId(String level) { |
| | | return switch (level) { case "EXCELLENT" -> -1L; case "OUTSTANDING" -> -2L; case "ELITE" -> -3L; default -> -9L; }; |
| | | } |
| | | |
| | | private BigDecimal configuredDiscountRate(Object value) { |
| | | try { |
| | | BigDecimal discount = decimal(string(value)); |
| | | return discount.signum() > 0 && discount.compareTo(BigDecimal.TEN) <= 0 ? discount.divide(BigDecimal.TEN, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO; |
| | | } catch (Exception ignored) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | } |
| | | |
| | | private boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String text = string(value).toLowerCase(); |
| | | return "true".equals(text) || "1".equals(text) || "yes".equals(text); |
| | | } |
| | | |
| | | private Map<String, Object> quote(Map<String, Object> rule, BigDecimal inputAmount) { |
| | | long originalAmount = inputAmount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact(); |
| | | BigDecimal rate = decimal(rule.get("discountRate")); |
| | | if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效"); |
| | | } |
| | | if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效"); |
| | | long payAmount = BigDecimal.valueOf(originalAmount).multiply(rate).setScale(0, RoundingMode.HALF_UP).longValueExact(); |
| | | long discountAmount = originalAmount - payAmount; |
| | | long subsidyAmount = number(rule.get("subsidyEnabled")) == 1 ? discountAmount : 0; |
| | |
| | | result.put("ruleName", rule.get("ruleName")); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, Object> merchant(MerchantPrincipal principal) { |
| | | List<Map<String, Object>> merchants = rows(""" |
| | | select merchant_name, address, contact_mobile, longitude, latitude |
| | |
| | | select card_level, benefit_text from merchant_card_benefit |
| | | where merchant_id = ? and status = 1 order by sort_no desc, id desc |
| | | """, merchantId); |
| | | String configuredDescription = configuredRules(merchantId, cardLevel).stream().map(rule -> string(rule.get("consumeDesc"))).filter(value -> !value.isBlank()).findFirst().orElse(""); |
| | | if (!configuredDescription.isBlank()) return configuredDescription; |
| | | return benefits.stream().filter(benefit -> matchesCardLevel(string(benefit.get("cardLevel")), cardLevel)) |
| | | .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准"); |
| | | } |
| | |
| | | @NotBlank @Size(max = 128) String subjectName, |
| | | @Size(max = 32) String creditCode, |
| | | @Size(max = 32) String subjectIdNumber, |
| | | @Pattern(regexp = "|fixed|long_term") String subjectCertificateValidityType, |
| | | @Pattern(regexp = "|\\d{4}-\\d{2}-\\d{2}") String subjectCertificateBeginDate, |
| | | @Size(max = 128) String registeredRegion, |
| | | @Size(max = 255) String registeredAddress, |
| | | @NotBlank @Size(max = 64) String contactName, |
| | | @NotBlank @Pattern(regexp = "1\\d{10}") String contactMobile |
| | | ) {} |
| | | ) {} |
| | |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.business.merchant.auth.MerchantPrincipal; |
| | | import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingCommand; |
| | | import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingCommand; |
| | | import com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingGateway; |
| | | import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException; |
| | | import com.talentcard.server.common.SensitiveDataCrypto; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | |
| | | @Service |
| | | public class MerchantSettlementService { |
| | | private final JdbcTemplate jdbc; |
| | | private final SensitiveDataCrypto sensitiveDataCrypto; |
| | | private final HuifuIndividualOnboardingGateway individualOnboardingGateway; |
| | | private final HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway; |
| | | |
| | | public MerchantSettlementService(JdbcTemplate jdbc) { this.jdbc = jdbc; } |
| | | public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway, HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway) { |
| | | this.jdbc = jdbc; |
| | | this.sensitiveDataCrypto = sensitiveDataCrypto; |
| | | this.individualOnboardingGateway = individualOnboardingGateway; |
| | | this.enterpriseOnboardingGateway = enterpriseOnboardingGateway; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> onboardingStatus(MerchantPrincipal principal) { |
| | |
| | | if (!active.isEmpty()) return onboarding(active.get(0)); |
| | | boolean enterprise = "enterprise".equals(request.subjectType()); |
| | | if (enterprise && (request.creditCode() == null || !request.creditCode().trim().matches("[0-9A-Z]{18}"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入18位统一社会信用代码"); |
| | | if (enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入法人正确的身份证号"); |
| | | if (enterprise && (request.subjectCertificateBeginDate() == null || request.subjectCertificateBeginDate().isBlank())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入营业执照成立日期"); |
| | | if (!enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的身份证号"); |
| | | String no = BusinessIdGenerator.next("MO"); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, subject_id_no_masked, registered_region, registered_address, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), maskId(request.subjectIdNumber()), clean(request.registeredRegion()), clean(request.registeredAddress()), request.contactName().trim(), request.contactMobile().trim(), key.trim()); |
| | | String certificateValidityType = certificateValidityType(request.subjectCertificateValidityType()); |
| | | String certificateBeginDate = certificateBeginDate(request.subjectCertificateBeginDate()); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, registered_region, registered_address, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, clean(request.registeredRegion()), clean(request.registeredAddress()), request.contactName().trim(), request.contactMobile().trim(), key.trim()); |
| | | if ("individual".equals(request.subjectType())) submitIndividualOnboarding(no, request); |
| | | else submitEnterpriseOnboarding(no, request); |
| | | return onboarding(rows("select * from merchant_onboarding_application where application_no = ?", no).get(0)); |
| | | } |
| | | |
| | | private void submitEnterpriseOnboarding(String applicationNo, MerchantOnboardingRequest request) { |
| | | try { |
| | | HuifuEnterpriseOnboardingGateway.Result result = enterpriseOnboardingGateway.create(new HuifuEnterpriseOnboardingCommand( |
| | | applicationNo, request.subjectName().trim(), request.creditCode().trim(), |
| | | certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), |
| | | clean(request.registeredRegion()), clean(request.registeredAddress()), request.contactName().trim(), |
| | | clean(request.subjectIdNumber()).toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), |
| | | certificateBeginDate(request.subjectCertificateBeginDate()), request.contactName().trim(), request.contactMobile().trim())); |
| | | if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo); |
| | | else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result.code(), result.message()), applicationNo); |
| | | else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | | } |
| | | } |
| | | |
| | | private void submitIndividualOnboarding(String applicationNo, MerchantOnboardingRequest request) { |
| | | try { |
| | | HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand(applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), request.contactMobile().trim(), clean(request.registeredAddress()))); |
| | | if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo); |
| | | else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result), applicationNo); |
| | | else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | | } |
| | | } |
| | | @Transactional(readOnly = true) |
| | | public List<Map<String, Object>> bankCards(MerchantPrincipal principal) { |
| | | store(principal); |
| | |
| | | private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); } |
| | | private Long number(String value) { try { return Long.valueOf(value); } catch (Exception ignored) { throw new BusinessException(ErrorCode.BAD_REQUEST, "编号无效"); } } |
| | | private String clean(Object value) { return value == null ? "" : String.valueOf(value).trim(); } |
| | | private String certificateValidityType(String value) { return "long_term".equals(value) ? "1" : "0"; } |
| | | private String certificateBeginDate(String value) { return clean(value).replace("-", ""); } |
| | | private String providerReason(HuifuIndividualOnboardingGateway.Result result) { return providerReason(result.code(), result.message()); } |
| | | private String providerReason(String code, String message) { String value = clean(message); return value.isEmpty() ? "Huifu onboarding rejected: " + clean(code) : value; } |
| | | private String maskId(String id) { String value = clean(id); return value.length() < 8 ? "" : value.substring(0, 3) + "********" + value.substring(value.length() - 4); } |
| | | private String sha256(String value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(); for (byte valueByte : digest) result.append(String.format("%02x", valueByte)); return result.toString(); } catch (Exception exception) { throw new IllegalStateException("Unable to hash bank card", exception); } } |
| | | } |
| | |
| | | Map<String, Object> response; |
| | | try { |
| | | response = gateway.createMiniProgramPayment(new HuifuMiniProgramPaymentCommand( |
| | | prepared.paymentNo(), prepared.openId(), prepared.amountInCent(), prepared.goodsDescription(), prepared.expiresAt())); |
| | | prepared.paymentNo(), prepared.huifuId(), prepared.openId(), prepared.amountInCent(), prepared.goodsDescription(), prepared.expiresAt())); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | if (exception.isOutcomeUnknown()) { |
| | | transactions.executeWithoutResult(status -> markCreateUnknown(prepared.paymentNo(), "GATEWAY_UNKNOWN", exception.getMessage())); |
| | |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少有效支付幂等键"); |
| | | } |
| | | List<Map<String, Object>> orders = rows(""" |
| | | select o.id, o.order_no, o.talent_id, o.merchant_id, o.pay_amount, o.order_status, o.price_version, |
| | | select o.id, o.order_no, o.talent_id, o.merchant_id, o.store_id, o.pay_amount, o.order_status, o.price_version, |
| | | o.expires_at, coalesce(o.product_name_snapshot, o.order_type, '人才权益服务') goods_description, |
| | | t.openid |
| | | t.openid, |
| | | coalesce((select a.provider_huifu_id |
| | | from merchant_onboarding_application a |
| | | where a.merchant_id = o.merchant_id |
| | | and a.status = 'approved' |
| | | and a.provider_huifu_id is not null |
| | | and trim(a.provider_huifu_id) <> '' |
| | | order by a.id desc limit 1), '') provider_huifu_id |
| | | from benefit_order o |
| | | join talent_user t on t.id = o.talent_id and t.deleted = 0 |
| | | where o.deleted = 0 and o.order_no = ? and o.talent_id = ? |
| | |
| | | if (orders.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在或无权支付"); |
| | | Map<String, Object> order = orders.get(0); |
| | | long payAmount = number(order.get("payAmount")); |
| | | if (number(order.get("orderStatus")) == 1) return new PreparedPayment("", "", 0, "", LocalDateTime.now(), false, true, "", false); |
| | | if (number(order.get("orderStatus")) == 1) return new PreparedPayment("", "", "", 0, "", LocalDateTime.now(), false, true, "", false); |
| | | if (number(order.get("orderStatus")) != 0) throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可支付"); |
| | | if (number(order.get("priceVersion")) != priceVersion) throw new BusinessException(ErrorCode.CONFLICT, "订单金额已更新,请重新确认"); |
| | | LocalDateTime expiresAt = localDateTime(order.get("expiresAt")); |
| | |
| | | verify_status = case when order_type = 'STORE_CONSUME' then 1 else verify_status end |
| | | where id = ? and order_status = 0 |
| | | """, number(order.get("id"))); |
| | | return new PreparedPayment("", "", 0, "", LocalDateTime.now(), false, true, "", false); |
| | | return new PreparedPayment("", "", "", 0, "", LocalDateTime.now(), false, true, "", false); |
| | | } |
| | | String openId = string(order.get("openid")); |
| | | if (openId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "当前用户未完成微信支付授权"); |
| | | String huifuId = string(order.get("providerHuifuId")); |
| | | if (huifuId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "商户汇付商户号未配置或尚未审核通过"); |
| | | |
| | | List<Map<String, Object>> existingRows = rows(""" |
| | | select payment_no, order_no, talent_id, status, attempt_no, client_payment_payload |
| | |
| | | } |
| | | String status = string(existing.get("status")); |
| | | String paymentNo = string(existing.get("paymentNo")); |
| | | if ("SUCCEEDED".equals(status)) return new PreparedPayment(paymentNo, "", 0, "", LocalDateTime.now(), false, true, "", false); |
| | | if ("SUCCEEDED".equals(status)) return new PreparedPayment(paymentNo, "", "", 0, "", LocalDateTime.now(), false, true, "", false); |
| | | if ("PREPAY_CREATED".equals(status)) { |
| | | return new PreparedPayment(paymentNo, "", 0, "", LocalDateTime.now(), false, false, string(existing.get("clientPaymentPayload")), false); |
| | | return new PreparedPayment(paymentNo, "", "", 0, "", LocalDateTime.now(), false, false, string(existing.get("clientPaymentPayload")), false); |
| | | } |
| | | if ("CREATED".equals(status) || "CREATE_UNKNOWN".equals(status)) { |
| | | return new PreparedPayment(paymentNo, "", 0, "", LocalDateTime.now(), false, false, "", true); |
| | | return new PreparedPayment(paymentNo, "", "", 0, "", LocalDateTime.now(), false, false, "", true); |
| | | } |
| | | int nextAttempt = (int) number(existing.get("attemptNo")) + 1; |
| | | String retryPaymentNo = BusinessIdGenerator.next("P"); |
| | | insertPaymentTransaction(retryPaymentNo, order, orderNo, talentId, payAmount, idempotencyKey, nextAttempt); |
| | | return new PreparedPayment(retryPaymentNo, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false); |
| | | return new PreparedPayment(retryPaymentNo, huifuId, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false); |
| | | } |
| | | |
| | | String paymentNo = BusinessIdGenerator.next("P"); |
| | | insertPaymentTransaction(paymentNo, order, orderNo, talentId, payAmount, idempotencyKey, 1); |
| | | return new PreparedPayment(paymentNo, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false); |
| | | return new PreparedPayment(paymentNo, huifuId, openId, payAmount, string(order.get("goodsDescription")), expiryOrDefault(expiresAt), true, false, "", false); |
| | | } |
| | | |
| | | private void insertPaymentTransaction( |
| | |
| | | |
| | | private record PreparedPayment( |
| | | String paymentNo, |
| | | String huifuId, |
| | | String openId, |
| | | long amountInCent, |
| | | String goodsDescription, |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | public record HuifuEnterpriseOnboardingCommand( |
| | | String applicationNo, |
| | | String companyName, |
| | | String creditCode, |
| | | String licenseValidityType, |
| | | String licenseBeginDate, |
| | | String registeredRegion, |
| | | String registeredAddress, |
| | | String legalName, |
| | | String legalIdNumber, |
| | | String legalValidityType, |
| | | String legalBeginDate, |
| | | String contactName, |
| | | String contactMobile |
| | | ) {} |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.huifu.bspay.sdk.opps.client.BasePayClient; |
| | | import com.huifu.bspay.sdk.opps.core.BasePay; |
| | | import com.huifu.bspay.sdk.opps.core.config.MerConfig; |
| | | import com.huifu.bspay.sdk.opps.core.request.V2UserBasicdataEntRequest; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.Map; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuEnterpriseOnboardingGateway { |
| | | private static final Object SDK_LOCK = new Object(); |
| | | private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); |
| | | private final HuifuPaymentProperties properties; |
| | | |
| | | public HuifuEnterpriseOnboardingGateway(HuifuPaymentProperties properties) { this.properties = properties; } |
| | | |
| | | public Result create(HuifuEnterpriseOnboardingCommand command) { |
| | | ensureConfigured(); |
| | | V2UserBasicdataEntRequest request = new V2UserBasicdataEntRequest(); |
| | | request.setReqSeqId(command.applicationNo()); |
| | | request.setReqDate(DATE.format(LocalDate.now())); |
| | | request.setRegName(command.companyName()); |
| | | request.setLicenseCode(command.creditCode()); |
| | | request.setLicenseValidityType(command.licenseValidityType()); |
| | | request.setLicenseBeginDate(command.licenseBeginDate()); |
| | | request.setRegDetail(joinAddress(command.registeredRegion(), command.registeredAddress())); |
| | | request.setLegalName(command.legalName()); |
| | | request.setLegalCertType("00"); |
| | | request.setLegalCertNo(command.legalIdNumber()); |
| | | request.setLegalCertValidityType(command.legalValidityType()); |
| | | request.setLegalCertBeginDate(command.legalBeginDate()); |
| | | request.setLegalCertNationality("CHN"); |
| | | request.setContactName(command.contactName()); |
| | | request.setContactMobile(command.contactMobile()); |
| | | try { |
| | | Map<String, Object> response; |
| | | synchronized (SDK_LOCK) { |
| | | initializeSdk(); |
| | | response = BasePayClient.request(request, false); |
| | | } |
| | | return Result.from(response); |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | throw new HuifuPaymentGatewayException("Huifu enterprise onboarding request failed", outcomeUnknown(exception), exception); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "Huifu onboarding configuration is incomplete"); |
| | | } |
| | | } |
| | | |
| | | private void initializeSdk() throws Exception { |
| | | BasePay.debug = false; |
| | | BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST; |
| | | MerConfig config = new MerConfig(); |
| | | config.setSysId(properties.getSystemId().trim()); |
| | | config.setProductId(properties.getProductId().trim()); |
| | | config.setRsaPrivateKey(properties.getPrivateKey().trim()); |
| | | config.setRsaPublicKey(properties.getPlatformPublicKey().trim()); |
| | | BasePay.initWithMerConfig(config); |
| | | } |
| | | |
| | | private String joinAddress(String region, String address) { |
| | | String left = region == null ? "" : region.trim(); |
| | | String right = address == null ? "" : address.trim(); |
| | | return left.isEmpty() ? right : right.isEmpty() ? left : left + right; |
| | | } |
| | | |
| | | private boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public record Result(boolean accepted, String huifuId, String code, String message) { |
| | | static Result from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | String message = value(data, "sub_resp_desc", "resp_desc", "message"); |
| | | String huifuId = value(data, "huifu_id", "huifuId"); |
| | | return new Result("00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code), huifuId, code, message); |
| | | } |
| | | private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | public record HuifuIndividualOnboardingCommand(String applicationNo, String name, String idCardNo, String certificateValidityType, String certificateBeginDate, String mobileNo, String address) {} |
| New file |
| | |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.huifu.bspay.sdk.opps.client.BasePayClient; |
| | | import com.huifu.bspay.sdk.opps.core.BasePay; |
| | | import com.huifu.bspay.sdk.opps.core.config.MerConfig; |
| | | import com.huifu.bspay.sdk.opps.core.request.V2UserBasicdataIndvRequest; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.Map; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | @Service |
| | | public class HuifuIndividualOnboardingGateway { |
| | | private static final Object SDK_LOCK = new Object(); |
| | | private final HuifuPaymentProperties properties; |
| | | |
| | | public HuifuIndividualOnboardingGateway(HuifuPaymentProperties properties) { this.properties = properties; } |
| | | |
| | | public Result create(HuifuIndividualOnboardingCommand command) { |
| | | ensureConfigured(); |
| | | V2UserBasicdataIndvRequest request = new V2UserBasicdataIndvRequest(); |
| | | request.setReqSeqId(command.applicationNo()); |
| | | request.setReqDate(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); |
| | | request.setName(command.name()); |
| | | request.setCertType("00"); |
| | | request.setCertNo(command.idCardNo()); |
| | | request.setCertValidityType(command.certificateValidityType()); |
| | | request.setCertBeginDate(command.certificateBeginDate()); |
| | | request.setCertNationality("CHN"); |
| | | request.setMobileNo(command.mobileNo()); |
| | | if (!blank(command.address())) request.setAddress(command.address()); |
| | | try { |
| | | Map<String, Object> response; |
| | | synchronized (SDK_LOCK) { |
| | | initializeSdk(); |
| | | response = BasePayClient.request(request, false); |
| | | } |
| | | return Result.from(response); |
| | | } catch (BusinessException exception) { |
| | | throw exception; |
| | | } catch (Exception exception) { |
| | | throw new HuifuPaymentGatewayException("Huifu individual onboarding request failed", outcomeUnknown(exception), exception); |
| | | } |
| | | } |
| | | |
| | | private void ensureConfigured() { |
| | | if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); |
| | | if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu onboarding configuration is incomplete"); |
| | | } |
| | | |
| | | private void initializeSdk() throws Exception { |
| | | BasePay.debug = false; |
| | | BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST; |
| | | MerConfig config = new MerConfig(); |
| | | config.setSysId(properties.getSystemId().trim()); |
| | | config.setProductId(properties.getProductId().trim()); |
| | | config.setRsaPrivateKey(properties.getPrivateKey().trim()); |
| | | config.setRsaPublicKey(properties.getPlatformPublicKey().trim()); |
| | | BasePay.initWithMerConfig(config); |
| | | } |
| | | |
| | | private boolean blank(String value) { return value == null || value.isBlank(); } |
| | | private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | public record Result(boolean accepted, String huifuId, String code, String message) { |
| | | static Result from(Map<String, Object> response) { |
| | | Object raw = response.get("data"); |
| | | Map<String, Object> data = raw instanceof Map<?, ?> value ? (Map<String, Object>) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | String message = value(data, "sub_resp_desc", "resp_desc", "message"); |
| | | String huifuId = value(data, "huifu_id", "huifuId"); |
| | | return new Result("00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code), huifuId, code, message); |
| | | } |
| | | |
| | | private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } |
| | | } |
| | | } |
| | |
| | | |
| | | public record HuifuMiniProgramPaymentCommand( |
| | | String paymentNo, |
| | | String huifuId, |
| | | String openId, |
| | | long amountInCent, |
| | | String goodsDescription, |
| | |
| | | V3TradePaymentJspayRequest request = new V3TradePaymentJspayRequest(); |
| | | request.setReqDate(REQUEST_DATE.format(LocalDateTime.now())); |
| | | request.setReqSeqId(command.paymentNo()); |
| | | request.setHuifuId(properties.getMerchantId().trim()); |
| | | request.setHuifuId(command.huifuId().trim()); |
| | | request.setGoodsDesc(command.goodsDescription()); |
| | | request.setTransAmt(BigDecimal.valueOf(command.amountInCent(), 2).toPlainString()); |
| | | request.setTradeType("T_MINIAPP"); |
| | |
| | | import com.talentcard.server.request.DependentCardApplyRequest; |
| | | import com.talentcard.server.request.DependentCardUnbindRequest; |
| | | import com.talentcard.server.response.DependentCardApplyItemResponse; |
| | | import com.talentcard.server.service.DependentCardApplyService; |
| | | |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.responses.ApiResponses; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | |
| | | |
| | | @Tag(name = "家属附属卡申请", description = "小程序家属附属卡申请、查询与解绑") |
| | | @RestController |
| | | @RequestMapping("/api/app/dependent-card-applies") |
| | | @RequestMapping("/api/app/family-cards/applications") |
| | | public class DependentCardApplyController { |
| | | |
| | | private final DependentCardApplyService service; |
| | | private final FamilyCardPortalService service; |
| | | |
| | | public DependentCardApplyController(DependentCardApplyService service) { |
| | | public DependentCardApplyController(FamilyCardPortalService service) { |
| | | this.service = service; |
| | | } |
| | | |
| | |
| | | public ApiResponse<Void> submit( |
| | | @Valid @RequestBody DependentCardApplyRequest request |
| | | ) { |
| | | service.submit(request); |
| | | service.createByTalentCardId(request); |
| | | return ApiResponse.success(); |
| | | } |
| | | |
| | |
| | | public ApiResponse<List<DependentCardApplyItemResponse>> list( |
| | | @RequestParam Long talentCardId |
| | | ) { |
| | | return ApiResponse.success(service.list(talentCardId)); |
| | | return ApiResponse.success(service.listByTalentCardId(talentCardId)); |
| | | } |
| | | |
| | | @Operation(summary = "解绑家属附属卡", description = "仅已通过审核的家属附属卡可以解绑") |
| | |
| | | @PathVariable Long id, |
| | | @Valid @RequestBody DependentCardUnbindRequest request |
| | | ) { |
| | | return ApiResponse.success(service.unbind(id, request)); |
| | | return ApiResponse.success(service.unbindByTalentCardId(id, request)); |
| | | } |
| | | } |
| | | } |
| | |
| | | private final FamilyCardPortalService service; |
| | | public FamilyCardPortalController(FamilyCardPortalService service) { this.service = service; } |
| | | @GetMapping public ApiResponse<List<Map<String, Object>>> list(@RequestParam Long talentId) { return ApiResponse.success(service.list(talentId)); } |
| | | @PostMapping("/applications") public ApiResponse<Map<String, Object>> create(@Valid @RequestBody FamilyCardApplicationRequest request) { return ApiResponse.success(service.create(request)); } |
| | | @PostMapping("/unbind") public ApiResponse<Map<String, Object>> unbindMany(@Valid @RequestBody FamilyCardBatchUnbindRequest request) { return ApiResponse.success(service.unbindMany(request)); } |
| | | @PostMapping("/{applicationId}/unbind") public ApiResponse<Map<String, Object>> unbind(@PathVariable Long applicationId, @Valid @RequestBody FamilyCardUnbindRequest request) { return ApiResponse.success(service.unbind(applicationId, request)); } |
| | | } |
| | |
| | | |
| | | import com.talentcard.server.business.BusinessIdGenerator; |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.request.DependentCardApplyRequest; |
| | | import com.talentcard.server.request.DependentCardUnbindRequest; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.response.DependentCardApplyItemResponse; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | """, talentId).stream().map(DatabaseRows::normalize).map(this::view).toList(); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public List<DependentCardApplyItemResponse> listByTalentCardId(Long talentCardId) { |
| | | Long talentId = talentIdByCardId(talentCardId); |
| | | return jdbc.queryForList(""" |
| | | select f.id, f.family_name, f.family_mobile, f.relationship, f.status |
| | | from family_card_application f |
| | | where f.deleted = 0 and f.talent_id = ? and f.status <> '\u5df2\u89e3\u7ed1' |
| | | order by f.apply_time desc, f.id desc |
| | | """, talentId).stream().map(DatabaseRows::normalize).map(this::dependentCardItem).toList(); |
| | | } |
| | | |
| | | @Transactional |
| | | public void createByTalentCardId(DependentCardApplyRequest request) { |
| | | Long talentId = talentIdByCardId(request.talentCardId()); |
| | | create(new FamilyCardApplicationRequest( |
| | | talentId, request.dependentName(), request.dependentIdCardNo(), |
| | | request.dependentMobile(), request.relation() |
| | | )); |
| | | } |
| | | |
| | | @Transactional |
| | | public DependentCardApplyItemResponse unbindByTalentCardId(Long id, DependentCardUnbindRequest request) { |
| | | Long talentId = talentIdByCardId(request.talentCardId()); |
| | | unbind(id, new FamilyCardUnbindRequest(talentId)); |
| | | List<Map<String, Object>> rows = jdbc.queryForList(""" |
| | | select f.id, f.family_name, f.family_mobile, f.relationship, f.status |
| | | from family_card_application f |
| | | where f.id = ? and f.talent_id = ? and f.deleted = 0 |
| | | limit 1 |
| | | """, id, talentId).stream().map(DatabaseRows::normalize).toList(); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "\u5bb6\u5c5e\u5361\u7533\u8bf7\u4e0d\u5b58\u5728"); |
| | | return dependentCardItem(rows.get(0)); |
| | | } |
| | | @Transactional |
| | | public Map<String, Object> create(FamilyCardApplicationRequest request) { |
| | | Map<String, Object> owner = ownerCard(request.talentId()); |
| | | if (!"卓越人才卡".equals(string(owner.get("cardLevel")))) throw new BusinessException(ErrorCode.CONFLICT, "家庭附属卡仅向卓越人才卡用户开放"); |
| | | if (number(owner.get("isFamilyCardEnabled")) == null || number(owner.get("isFamilyCardEnabled")) != 1 || number(owner.get("familyCardLimit")) == null || number(owner.get("familyCardLimit")) < 1) throw new BusinessException(ErrorCode.CONFLICT, "当前人才卡暂未开通家庭附属卡申请"); |
| | | if (!List.of("配偶", "父母", "子女", "其他").contains(request.relationship().trim())) throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的家属关系"); |
| | | if (!validIdCard(request.idCard().trim())) throw new BusinessException(ErrorCode.BAD_REQUEST, "身份证号格式不正确"); |
| | | Integer occupied = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('待审核', '已通过')", Integer.class, request.talentId()); |
| | | if (occupied != null && occupied >= number(owner.get("familyCardLimit"))) throw new BusinessException(ErrorCode.CONFLICT, "家庭附属卡申请数量已达上限"); |
| | | Integer submitted = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('\u5f85\u5ba1\u6838', '\u5df2\u901a\u8fc7', '\u5df2\u9a73\u56de')", Integer.class, request.talentId()); |
| | | if (submitted != null && submitted >= 3) throw new BusinessException(ErrorCode.CONFLICT, "\u540c\u4e00\u5f20\u4e3b\u5361\u6700\u591a\u63d0\u4ea43\u4e2a\u5bb6\u5c5e\u9644\u5c5e\u5361\u7533\u8bf7"); |
| | | Integer occupied = jdbc.queryForObject("select count(*) from family_card_application where deleted = 0 and talent_id = ? and status in ('\u5f85\u5ba1\u6838', '\u5df2\u901a\u8fc7')", Integer.class, request.talentId()); |
| | | if (occupied != null && occupied >= 2) throw new BusinessException(ErrorCode.CONFLICT, "\u8be5\u4e3b\u5361\u5bb6\u5c5e\u9644\u5c5e\u5361\u540d\u989d\u5df2\u6ee1\uff0c\u6700\u591a\u7ed1\u5b9a2\u4e2a"); |
| | | Integer duplicate = jdbc.queryForObject(""" |
| | | select count(*) from family_card_application |
| | | where deleted = 0 and talent_id = ? and id_card_no = ? and status in ('待审核', '已通过') |
| | |
| | | return Map.of("applicationIds", applicationIds, "unbound", true); |
| | | } |
| | | |
| | | private Long talentIdByCardId(Long talentCardId) { |
| | | if (talentCardId == null || talentCardId <= 0) throw new BusinessException(ErrorCode.NOT_FOUND, "\u4e3b\u5361\u4e0d\u5b58\u5728\u6216\u5df2\u5931\u6548"); |
| | | List<Map<String, Object>> rows = jdbc.queryForList(""" |
| | | select talent_id from talent_card |
| | | where id = ? and deleted = 0 and card_type = 'PERSONAL' and status = 1 |
| | | limit 1 |
| | | """, talentCardId).stream().map(DatabaseRows::normalize).toList(); |
| | | if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "\u4e3b\u5361\u4e0d\u5b58\u5728\u6216\u5df2\u5931\u6548"); |
| | | return number(rows.get(0).get("talentId")); |
| | | } |
| | | |
| | | private DependentCardApplyItemResponse dependentCardItem(Map<String, Object> row) { |
| | | Integer status = switch (string(row.get("status"))) { |
| | | case "\u5f85\u5ba1\u6838" -> 0; |
| | | case "\u5df2\u901a\u8fc7" -> 1; |
| | | case "\u5df2\u9a73\u56de" -> 2; |
| | | case "\u5df2\u89e3\u7ed1" -> 3; |
| | | default -> -1; |
| | | }; |
| | | String statusText = switch (status) { |
| | | case 0 -> "\u5f85\u5ba1\u6838"; |
| | | case 1 -> "\u5df2\u901a\u8fc7"; |
| | | case 2 -> "\u5df2\u9a73\u56de"; |
| | | case 3 -> "\u5df2\u89e3\u7ed1"; |
| | | default -> "\u672a\u77e5"; |
| | | }; |
| | | return new DependentCardApplyItemResponse( |
| | | number(row.get("id")), string(row.get("familyName")), |
| | | maskMobile(string(row.get("familyMobile"))), status, statusText, |
| | | string(row.get("relationship")) |
| | | ); |
| | | } |
| | | private Map<String, Object> ownerCard(Long talentId) { |
| | | List<Map<String, Object>> rows = jdbc.queryForList(""" |
| | | select c.card_no, c.card_level, c.is_family_card_enabled, c.family_card_limit, u.name as talent_name, u.mobile as talent_mobile |
| New file |
| | |
| | | package com.talentcard.server.business.portal; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | |
| | | import tools.jackson.core.type.TypeReference; |
| | | import tools.jackson.databind.ObjectMapper; |
| | | |
| | | final class ProductPriceCalculator { |
| | | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| | | private static final TypeReference<List<Map<String, Object>>> RIGHTS_TYPE = new TypeReference<>() {}; |
| | | |
| | | private ProductPriceCalculator() { |
| | | } |
| | | |
| | | static ProductPrice calculate(Object originalPrice, Object salePrice, Object rightsConfig, Object cardLevel) { |
| | | return calculate(originalPrice, salePrice, rightsConfig, cardLevel, 0); |
| | | } |
| | | |
| | | static ProductPrice calculate(Object originalPrice, Object salePrice, Object rightsConfig, Object cardLevel, int usedFreeCount) { |
| | | BigDecimal fallbackSale = money(salePrice); |
| | | BigDecimal original = money(originalPrice); |
| | | if (original.signum() <= 0) { |
| | | original = fallbackSale; |
| | | } |
| | | BigDecimal calculatedSale = fallbackSale.signum() > 0 ? fallbackSale : original; |
| | | Map<String, Object> right = matchingRight(rightsConfig, cardLevel); |
| | | if (right != null && original.signum() > 0) { |
| | | boolean free = booleanValue(right.get("free")); |
| | | int limitCount = intValue(right.get("limitCount")); |
| | | boolean freeAvailable = free && (limitCount <= 0 || usedFreeCount < limitCount); |
| | | if (freeAvailable) { |
| | | calculatedSale = BigDecimal.ZERO; |
| | | } else { |
| | | BigDecimal discount = discountValue(right.get(free ? "exceedDiscount" : "discount")); |
| | | if (discount.signum() > 0) { |
| | | calculatedSale = original.multiply(discount) |
| | | .divide(BigDecimal.TEN, 2, RoundingMode.HALF_UP); |
| | | } |
| | | } |
| | | } |
| | | return new ProductPrice(original, calculatedSale.setScale(2, RoundingMode.HALF_UP), right != null); |
| | | } |
| | | |
| | | static ProductRight productRight(Object rightsConfig, Object cardLevel) { |
| | | Map<String, Object> right = matchingRight(rightsConfig, cardLevel); |
| | | if (right == null) return null; |
| | | return new ProductRight( |
| | | booleanValue(right.get("free")), |
| | | intValue(right.get("limitCount")), |
| | | text(right.get("resetCycle"))); |
| | | } |
| | | |
| | | private static Map<String, Object> matchingRight(Object rightsConfig, Object cardLevel) { |
| | | String normalizedCardLevel = normalizeCardLevel(cardLevel); |
| | | if (normalizedCardLevel.isBlank() || rightsConfig == null) return null; |
| | | for (Map<String, Object> right : parseRights(rightsConfig)) { |
| | | if (!booleanValue(right.get("enabled"))) continue; |
| | | if (normalizedCardLevel.equals(normalizeCardLevel(right.get("level")))) return right; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private static List<Map<String, Object>> parseRights(Object value) { |
| | | if (value instanceof List<?> list) { |
| | | return list.stream() |
| | | .filter(Map.class::isInstance) |
| | | .map(item -> (Map<String, Object>) item) |
| | | .toList(); |
| | | } |
| | | String text = String.valueOf(value).trim(); |
| | | if (text.isBlank()) return List.of(); |
| | | try { |
| | | return OBJECT_MAPPER.readValue(text, RIGHTS_TYPE); |
| | | } catch (Exception ignored) { |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | private static String normalizeCardLevel(Object value) { |
| | | if (value == null) return ""; |
| | | String raw = String.valueOf(value).trim(); |
| | | String upper = raw.toUpperCase(Locale.ROOT); |
| | | if (upper.contains("EXCELLENT") || raw.contains("\u5353\u8d8a")) return "EXCELLENT"; |
| | | if (upper.contains("OUTSTANDING") || raw.contains("\u6770\u51fa")) return "OUTSTANDING"; |
| | | if (upper.contains("ELITE") || raw.contains("\u83c1\u82f1") || raw.contains("\u7cbe\u82f1")) return "ELITE"; |
| | | return upper; |
| | | } |
| | | |
| | | private static boolean booleanValue(Object value) { |
| | | if (value instanceof Boolean bool) return bool; |
| | | if (value instanceof Number number) return number.intValue() != 0; |
| | | String text = value == null ? "" : String.valueOf(value).trim(); |
| | | return "true".equalsIgnoreCase(text) || "1".equals(text) || "yes".equalsIgnoreCase(text); |
| | | } |
| | | |
| | | private static int intValue(Object value) { |
| | | if (value instanceof Number number) return number.intValue(); |
| | | try { |
| | | String text = value == null ? "" : String.valueOf(value).trim(); |
| | | if (text.isBlank()) return 0; |
| | | return new BigDecimal(text).intValue(); |
| | | } catch (Exception ignored) { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | private static String text(Object value) { |
| | | return value == null ? "" : String.valueOf(value).trim(); |
| | | } |
| | | |
| | | private static BigDecimal discountValue(Object value) { |
| | | BigDecimal discount = money(value); |
| | | if (discount.compareTo(BigDecimal.ZERO) <= 0 || discount.compareTo(BigDecimal.TEN) > 0) { |
| | | return BigDecimal.ZERO; |
| | | } |
| | | return discount; |
| | | } |
| | | |
| | | private static BigDecimal money(Object value) { |
| | | if (value == null) return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); |
| | | try { |
| | | if (value instanceof BigDecimal decimal) return decimal.setScale(2, RoundingMode.HALF_UP); |
| | | return new BigDecimal(String.valueOf(value).trim()).setScale(2, RoundingMode.HALF_UP); |
| | | } catch (Exception ignored) { |
| | | return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); |
| | | } |
| | | } |
| | | |
| | | record ProductPrice(BigDecimal originalPrice, BigDecimal salePrice, boolean personalized) { |
| | | } |
| | | |
| | | record ProductRight(boolean free, int limitCount, String resetCycle) { |
| | | } |
| | | } |
| | |
| | | String level = value(card.get("cardLevel"), "人才卡"); |
| | | result.put("cardLevel", level); |
| | | result.put("familyCardLimit", ((Number) card.getOrDefault("familyCardLimit", 0)).intValue()); |
| | | if (!"卓越人才卡".equals(level)) { |
| | | if (!isExcellenceCardLevel(level)) { |
| | | result.put("unavailableReason", "家庭附属卡仅向卓越人才卡用户开放"); |
| | | return result; |
| | | } |
| | |
| | | result.put("unavailableReason", cardStatusText(cardStatus(card)) + ",暂不能使用家庭附属卡服务"); |
| | | return result; |
| | | } |
| | | if (((Number) card.getOrDefault("isFamilyCardEnabled", 0)).intValue() != 1) { |
| | | result.put("unavailableReason", "当前人才卡暂未开通家庭附属卡服务"); |
| | | return result; |
| | | } |
| | | if (((Number) card.getOrDefault("familyCardLimit", 0)).intValue() < 1) { |
| | | result.put("unavailableReason", "当前人才卡暂未配置家庭附属卡名额"); |
| | | return result; |
| | | } |
| | | // if (((Number) card.getOrDefault("isFamilyCardEnabled", 0)).intValue() != 1) { |
| | | // result.put("unavailableReason", "当前人才卡暂未开通家庭附属卡服务"); |
| | | // return result; |
| | | // } |
| | | // if (((Number) card.getOrDefault("familyCardLimit", 0)).intValue() < 1) { |
| | | // result.put("unavailableReason", "当前人才卡暂未配置家庭附属卡名额"); |
| | | // return result; |
| | | // } |
| | | result.put("enabled", true); |
| | | result.put("unavailableReason", ""); |
| | | return result; |
| | |
| | | return java.util.Optional.of(result); |
| | | } |
| | | |
| | | private boolean isExcellenceCardLevel(String level) { |
| | | if (level == null) return false; |
| | | String normalized = level.trim().toUpperCase(Locale.ROOT); |
| | | return "EXCELLENCE".equals(normalized) || level.contains("卓越") || level.contains("鍗撹秺"); |
| | | } |
| | | |
| | | private String cardStatus(Map<String, Object> card) { |
| | | if (((Number) card.getOrDefault("talentStatus", 1)).intValue() != 1) return "FROZEN"; |
| | | if (((Number) card.getOrDefault("cardStatus", 0)).intValue() == 0) return "FROZEN"; |
| | |
| | | if (!ELITE_CARD_LEVEL.equals(card.getCardLevel())) { |
| | | throw new BusinessException(ErrorCode.INTERNAL_ERROR, "仅卓越人才卡可申请家属附属卡"); |
| | | } |
| | | if (!Integer.valueOf(1).equals(card.getIsFamilyCardEnabled())) { |
| | | throw new BusinessException(ErrorCode.INTERNAL_ERROR, "该主卡未开通家属附属卡申请权限"); |
| | | } |
| | | Integer configuredLimit = card.getFamilyCardLimit(); |
| | | if (configuredLimit == null || configuredLimit <= 0) { |
| | | throw new BusinessException(ErrorCode.INTERNAL_ERROR, "该主卡未配置家属附属卡名额"); |
| | | } |
| | | return Math.min(configuredLimit, MAX_FAMILY_CARD_COUNT); |
| | | // if (!Integer.valueOf(1).equals(card.getIsFamilyCardEnabled())) { |
| | | // throw new BusinessException(ErrorCode.INTERNAL_ERROR, "该主卡未开通家属附属卡申请权限"); |
| | | // } |
| | | // Integer configuredLimit = card.getFamilyCardLimit(); |
| | | // if (configuredLimit == null || configuredLimit <= 0) { |
| | | // throw new BusinessException(ErrorCode.INTERNAL_ERROR, "该主卡未配置家属附属卡名额"); |
| | | // } |
| | | return Math.min(2, MAX_FAMILY_CARD_COUNT); |
| | | } |
| | | |
| | | private void recordStatus( |
| | |
| | | repositories: |
| | | enabled: false |
| | | flyway: |
| | | enabled: true |
| | | enabled: false |
| | | locations: classpath:db/migration |
| | | baseline-on-migrate: true |
| | | jpa: |
| New file |
| | |
| | | alter table merchant_onboarding_application add column subject_id_no_encrypted varchar(512) default null after subject_id_no_masked; |
| | | alter table merchant_onboarding_application add column subject_cert_validity_type varchar(8) default null after subject_id_no_encrypted; |
| | | alter table merchant_onboarding_application add column subject_cert_begin_date char(8) default null after subject_cert_validity_type; |
| New file |
| | |
| | | alter table merchant_coupon |
| | | add column talent_level_quantities varchar(512) default null comment 'talent card level quantities JSON'; |