feat(admin): 接入高德地图并完善管理功能
| | |
| | | |
| | | # 如果部署到独立域名,可改为后端网关地址。 |
| | | # VITE_API_BASE_URL=https://api.example.com |
| | | |
| | | # 高德地图 Web 端(JS API)Key 与安全密钥。 |
| | | VITE_AMAP_KEY= |
| | | VITE_AMAP_SECURITY_CODE= |
| | |
| | | "name": "talent-card-admin", |
| | | "version": "0.0.0", |
| | | "dependencies": { |
| | | "@amap/amap-jsapi-loader": "^1.0.1", |
| | | "@element-plus/icons-vue": "^2.3.2", |
| | | "@types/leaflet": "^1.9.22", |
| | | "@wangeditor/editor": "^5.1.23", |
| | | "@wangeditor/editor-for-vue": "^5.1.12", |
| | | "axios": "^1.18.1", |
| | | "element-plus": "^2.14.3", |
| | | "leaflet": "^1.9.4", |
| | | "pinia": "^4.0.2", |
| | | "vue": "^3.5.39", |
| | | "vue-router": "^4.6.4", |
| | |
| | | "vite": "^8.1.1", |
| | | "vue-tsc": "^3.3.5" |
| | | } |
| | | }, |
| | | "node_modules/@amap/amap-jsapi-loader": { |
| | | "version": "1.0.1", |
| | | "resolved": "https://registry.npmjs.org/@amap/amap-jsapi-loader/-/amap-jsapi-loader-1.0.1.tgz", |
| | | "integrity": "sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==", |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/@babel/helper-string-parser": { |
| | | "version": "7.29.7", |
| | |
| | | "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz", |
| | | "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==", |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/@types/geojson": { |
| | | "version": "7946.0.16", |
| | | "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", |
| | | "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/@types/leaflet": { |
| | | "version": "1.9.22", |
| | | "resolved": "https://registry.npmmirror.com/@types/leaflet/-/leaflet-1.9.22.tgz", |
| | | "integrity": "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==", |
| | | "license": "MIT", |
| | | "dependencies": { |
| | | "@types/geojson": "*" |
| | | } |
| | | }, |
| | | "node_modules/@types/lodash": { |
| | | "version": "4.17.24", |
| | |
| | | "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", |
| | | "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", |
| | | "license": "MIT" |
| | | }, |
| | | "node_modules/leaflet": { |
| | | "version": "1.9.4", |
| | | "resolved": "https://registry.npmmirror.com/leaflet/-/leaflet-1.9.4.tgz", |
| | | "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", |
| | | "license": "BSD-2-Clause" |
| | | }, |
| | | "node_modules/lightningcss": { |
| | | "version": "1.32.0", |
| | |
| | | "preview": "vite preview" |
| | | }, |
| | | "dependencies": { |
| | | "@amap/amap-jsapi-loader": "^1.0.1", |
| | | "@element-plus/icons-vue": "^2.3.2", |
| | | "@types/leaflet": "^1.9.22", |
| | | "@wangeditor/editor": "^5.1.23", |
| | | "@wangeditor/editor-for-vue": "^5.1.12", |
| | | "axios": "^1.18.1", |
| | | "element-plus": "^2.14.3", |
| | | "leaflet": "^1.9.4", |
| | | "pinia": "^4.0.2", |
| | | "vue": "^3.5.39", |
| | | "vue-router": "^4.6.4", |
| | |
| | | ] |
| | | |
| | | const reservationMenus: MenuItem[] = [ |
| | | { path: '/appointment-management', label: '预约服务管理' }, |
| | | { path: '/medical-reservations', label: '体检预约管理' }, |
| | | { path: '/medical-applications', label: '就医申请管理' }, |
| | | { path: '/parking-plates', label: '停车车牌管理' }, |
| | | { path: '/car-wash-benefits', label: '洗车权益管理' }, |
| | | { path: '/fuel-services', label: '加油服务管理' }, |
| | | { path: '/spouse-employment', label: '配偶就业管理' }, |
| | | { path: '/hotel-reservations', label: '酒店预约管理' }, |
| | |
| | | |
| | | .merchant-school-address-panel { |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | .merchant-school-address-panel, |
| | | .merchant-parking-address-panel { |
| | | padding: 20px 28px 18px; |
| | | background: #ffffff; |
| | | } |
| | | |
| | | .merchant-school-address-title { |
| | | .merchant-school-address-title, |
| | | .merchant-parking-address-title { |
| | | margin: 0 0 16px; |
| | | color: #111827; |
| | | font-size: 14px; |
| | |
| | | text-align: left; |
| | | } |
| | | |
| | | .merchant-school-address-actions { |
| | | .merchant-school-address-actions, |
| | | .merchant-parking-address-actions { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-top: 16px; |
| | |
| | | grid-column: 1 / 3; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .merchant-rights-parking-radio .el-radio-group { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 24px; |
| | | } |
| | | |
| | | .merchant-rights-parking-radio .el-radio { |
| | | margin-right: 0; |
| | | } |
| | | |
| | | .merchant-rights-detail-row label { |
| | |
| | | } |
| | | |
| | | .merchant-coupon-edit-form .coupon-valid-time-picker { |
| | | width: 300px; |
| | | flex: 0 1 280px; |
| | | width: 280px; |
| | | max-width: 100%; |
| | | } |
| | | |
| | | .merchant-coupon-edit-form .el-input__wrapper, |
| | |
| | | border-radius: 0; |
| | | background: #ffffff; |
| | | box-shadow: 0 0 0 1px #d9dee8 inset; |
| | | } |
| | | |
| | | .merchant-coupon-edit-form .el-date-editor.el-input__wrapper { |
| | | width: 280px; |
| | | } |
| | | |
| | | .merchant-coupon-edit-form .el-date-editor .el-range-input, |
| | |
| | | .merchant-map-results button:hover { background: #ecf5ff; color: #409eff; } |
| | | .merchant-location-coordinates { display: flex; justify-content: space-between; color: #606266; font-size: 13px; } |
| | | .merchant-location-coordinates span { color: #909399; } |
| | | .merchant-map-marker { background: transparent; border: 0; } |
| | | .merchant-map-marker span { display: block; width: 18px; height: 18px; border: 4px solid #fff; border-radius: 50% 50% 50% 0; background: #409eff; box-shadow: 0 1px 5px rgba(0, 0, 0, .35); transform: rotate(-45deg); } |
| | | .merchant-map-marker span::after { display: block; width: 6px; height: 6px; margin: 6px; border-radius: 50%; background: #fff; content: ''; } |
| | | .product-audit-preview-image { display: block; max-width: 100%; max-height: 70vh; margin: 0 auto; object-fit: contain; } |
| | | |
| | | .talent-create-page .detail-block { |
| | |
| | | birthDate?: string |
| | | talentName?: string |
| | | talentIdCardNo?: string |
| | | talentGender?: string | number |
| | | talentMobile?: string |
| | | talentCardNo?: string |
| | | cardLevel?: string |
| | | registerTime?: string |
| | | applyTime?: string |
| | | status?: string | number |
| | | rejectReason?: string |
| | | contactAddress?: string |
| | | workAddress?: string |
| | | images?: string[] | string |
| | |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Picture } from '@element-plus/icons-vue' |
| | | import { auditChildCardApplication, getChildCardApplicationDetail } from '../api/admin' |
| | | import type { ChildCardApplication } from '../types/admin' |
| | | |
| | | const route = useRoute() |
| | |
| | | } |
| | | |
| | | function genderText(gender?: number | string) { |
| | | if (gender === 1 || gender === '1') return '男' |
| | | if (gender === 2 || gender === '2') return '女' |
| | | if (gender === 1 || gender === '1' || gender === '男') return '男' |
| | | if (gender === 2 || gender === '2' || gender === '女') return '女' |
| | | return '-' |
| | | } |
| | | |
| | |
| | | { label: '联系方式:', value: text(detail.value?.talentMobile) }, |
| | | { label: '联系地址:', value: text(detail.value?.contactAddress) }, |
| | | { label: '工作地址:', value: text(detail.value?.workAddress) }, |
| | | { label: '性别:', value: genderText(detail.value?.gender) }, // 这里的性别是持卡人的性别还是需要额外字段?暂定复用。如果API返回的gender是子女的,可能持卡人性别没有或者从其它字段取。根据图2,持卡人也有性别。假设接口有talentGender,这里先暂用 text 模拟,或者如果是详情接口里有,我们可以从 detail.value 里取。假设后端一并返回。 |
| | | { label: '性别:', value: genderText(detail.value?.talentGender) }, |
| | | { label: '人才等级:', value: text(detail.value?.cardLevel) }, |
| | | ]) |
| | | |
| | |
| | | } |
| | | }) |
| | | |
| | | const mockDetail: ChildCardApplication = { |
| | | id: 232, |
| | | childName: '李一', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentName: '李四', |
| | | talentIdCardNo: '510664491312111', |
| | | talentMobile: '13666666666', |
| | | talentCardNo: '486913', |
| | | cardLevel: '卓越人才卡', |
| | | contactAddress: 'XXXXXXXXXXXXXX', |
| | | workAddress: 'XXXXXXXXXXXXXX', |
| | | status: '待审核', |
| | | images: [ |
| | | 'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=birth%20certificate%20document%20photo%20white%20background&image_size=square', |
| | | 'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=id%20card%20front%20side%20document%20photo%20white%20background&image_size=square', |
| | | 'https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=id%20card%20back%20side%20document%20photo%20white%20background&image_size=square' |
| | | ] |
| | | } |
| | | |
| | | const isPending = computed(() => detail.value?.status === '待审核') |
| | | |
| | | async function loadDetail() { |
| | | loading.value = true |
| | | try { |
| | | // 模拟网络请求延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 300)) |
| | | detail.value = { ...mockDetail, id: route.params.id as string } |
| | | |
| | | // 真实的 API 调用逻辑,暂时注释掉 |
| | | // detail.value = await getChildCardApplicationDetail(String(route.params.id)) |
| | | detail.value = await getChildCardApplicationDetail(String(route.params.id)) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | |
| | | async function approveApplication() { |
| | | if (!detail.value) return |
| | | await ElMessageBox.confirm('确认通过该子女管理申请?', '审核通过', { type: 'warning' }) |
| | | // 模拟接口调用成功 |
| | | // await auditChildCardApplication(detail.value.id, '已审核') |
| | | await auditChildCardApplication(detail.value.id, '已通过') |
| | | ElMessage.success('审核已通过') |
| | | router.back() |
| | | } |
| | | |
| | | async function rejectApplication() { |
| | | if (!detail.value) return |
| | | await ElMessageBox.prompt('请输入不通过原因', '审核不通过', { |
| | | inputValue: '资料不完整', |
| | | const { value } = await ElMessageBox.prompt('请输入不通过原因', '审核不通过', { |
| | | inputValue: '', |
| | | inputValidator: input => input.trim() ? true : '请输入不通过原因', |
| | | }) |
| | | // 模拟接口调用成功 |
| | | // await auditChildCardApplication(detail.value.id, '审核不通过', value) |
| | | await auditChildCardApplication(detail.value.id, '审核不通过', value) |
| | | ElMessage.success('操作成功') |
| | | router.back() |
| | | } |
| | |
| | | |
| | | const rows = ref<ChildCardApplication[]>([]) |
| | | |
| | | const mockData: ChildCardApplication[] = [ |
| | | { |
| | | id: 232, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 254, |
| | | childName: '李四', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '待审核', |
| | | }, |
| | | { |
| | | id: 46, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 577, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '486913', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 233, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '846511', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | { |
| | | id: 255, |
| | | childName: '张三', |
| | | gender: 1, |
| | | birthDate: '2026-07-06', |
| | | talentMobile: '138***4568', |
| | | talentCardNo: '846511', |
| | | cardLevel: '钻石卡', |
| | | registerTime: '2021-02-28 10:30', |
| | | status: '已审核', |
| | | }, |
| | | ] |
| | | |
| | | const statusOptions = computed(() => [ |
| | | { label: '已审核', value: '已审核' }, |
| | | { label: '待审核', value: '待审核' }, |
| | | { label: '已通过', value: '已通过' }, |
| | | { label: '审核不通过', value: '审核不通过' }, |
| | | ]) |
| | | |
| | | function statusText(row: ChildCardApplication) { |
| | |
| | | } |
| | | |
| | | function registerTime(row: ChildCardApplication) { |
| | | return row.registerTime || row.applyTime || '' |
| | | return String(row.registerTime || row.applyTime || '').replace('T', ' ') |
| | | } |
| | | |
| | | function genderText(gender?: number | string) { |
| | | if (gender === 1 || gender === '1') return '男' |
| | | if (gender === 2 || gender === '2') return '女' |
| | | if (gender === 1 || gender === '1' || gender === '男') return '男' |
| | | if (gender === 2 || gender === '2' || gender === '女') return '女' |
| | | return '-' |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | | // 模拟网络请求延迟 |
| | | await new Promise(resolve => setTimeout(resolve, 300)) |
| | | |
| | | // 使用 mock 数据 |
| | | let filteredData = [...mockData] |
| | | |
| | | if (filters.childKeyword) { |
| | | filteredData = filteredData.filter(item => item.childName?.includes(filters.childKeyword)) |
| | | } |
| | | if (filters.talentKeyword) { |
| | | filteredData = filteredData.filter(item => |
| | | item.talentName?.includes(filters.talentKeyword) || |
| | | item.talentMobile?.includes(filters.talentKeyword) || |
| | | item.talentCardNo?.includes(filters.talentKeyword) |
| | | ) |
| | | } |
| | | if (filters.cardLevel) { |
| | | filteredData = filteredData.filter(item => item.cardLevel === filters.cardLevel) |
| | | } |
| | | if (filters.status) { |
| | | filteredData = filteredData.filter(item => item.status === filters.status) |
| | | } |
| | | |
| | | const startIdx = (pagination.page - 1) * pagination.size |
| | | const endIdx = startIdx + pagination.size |
| | | |
| | | rows.value = filteredData.slice(startIdx, endIdx) |
| | | pagination.total = filteredData.length |
| | | |
| | | // 下面是真实的 API 调用逻辑,暂时注释掉 |
| | | /* |
| | | const query = { |
| | | page: pagination.page, |
| | | size: pagination.size, |
| | |
| | | const result = await getChildCardApplications(query) |
| | | rows.value = result.records |
| | | pagination.total = result.total |
| | | */ |
| | | } catch { |
| | | rows.value = [] |
| | | pagination.total = 0 |
| | |
| | | <el-form-item label="卡片等级"> |
| | | <el-select v-model="filters.cardLevel" placeholder="全部" clearable> |
| | | <el-option label="全部" value="" /> |
| | | <el-option label="钻石卡" value="钻石卡" /> |
| | | <el-option label="卓越人才卡" value="卓越人才卡" /> |
| | | <el-option label="精英人才卡" value="精英人才卡" /> |
| | | <el-option label="青年人才卡" value="青年人才卡" /> |
| | | <el-option label="杰出人才卡" value="杰出人才卡" /> |
| | | <el-option label="菁英人才卡" value="菁英人才卡" /> |
| | | <el-option label="普通游客" value="普通游客" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="状态"> |
| | |
| | | search() |
| | | } |
| | | |
| | | function formatDateTime(value?: string) { |
| | | return value ? value.replace('T', ' ') : '' |
| | | } |
| | | |
| | | async function download() { |
| | | const result = await getServiceApplications({ page: 1, size: 1000, applicationType: 'MEDICAL_RESERVATION', talentKeyword: filters.talentKeyword || undefined, hospitalName: filters.hospitalName || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] }) |
| | | downloadExcel('体检预约', [{ title: '预约单号', value: row => row.applicationNo }, { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel }, { title: '医院', value: row => row.hospitalName }, { title: '套餐', value: row => row.packageName }, { title: '预约时间', value: row => row.expectedTime }, { title: '提交时间', value: row => row.submittedAt }, { title: '状态', value: row => row.status }], result.records) |
| | | downloadExcel('体检预约', [{ title: '预约单号', value: row => row.applicationNo }, { title: '持卡人', value: row => row.talentName }, { title: '手机号', value: row => row.talentMobile }, { title: '人才等级', value: row => row.talentLevel }, { title: '医院', value: row => row.hospitalName }, { title: '套餐', value: row => row.packageName }, { title: '预约时间', value: row => row.expectedTime }, { title: '提交时间', value: row => formatDateTime(row.submittedAt) }, { title: '状态', value: row => row.status }], result.records) |
| | | ElMessage.success(`已导出 ${result.records.length} 条预约`) |
| | | } |
| | | |
| | |
| | | <el-table-column prop="applicantName" label="体检人姓名" min-width="96" align="center" /> |
| | | <el-table-column prop="applicantMobile" label="手机号" min-width="112" align="center" /> |
| | | <el-table-column prop="expectedTime" label="体检期望时间" min-width="118" align="center" /> |
| | | <el-table-column prop="submittedAt" label="提交时间" min-width="126" align="center" /> |
| | | <el-table-column label="提交时间" min-width="126" align="center"> |
| | | <template #default="{ row }">{{ formatDateTime(row.submittedAt) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" label="状态" min-width="82" align="center" /> |
| | | <el-table-column label="操作" width="86" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import L, { type Map as LeafletMap, type Marker } from 'leaflet' |
| | | import 'leaflet/dist/leaflet.css' |
| | | import { load as loadAMapApi } from '@amap/amap-jsapi-loader' |
| | | import { Minus, Picture, Plus, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import type { FormInstance, FormRules } from 'element-plus' |
| | |
| | | const locationSearching = ref(false) |
| | | const locationSearchResults = ref<Array<{ name: string; longitude: number; latitude: number }>>([]) |
| | | const mapElement = ref<HTMLElement>() |
| | | let locationMap: LeafletMap | undefined |
| | | let locationMarker: Marker | undefined |
| | | const mapMarkerIcon = L.divIcon({ className: 'merchant-map-marker', html: '<span></span>', iconSize: [26, 26], iconAnchor: [13, 26] }) |
| | | let locationMap: any |
| | | let locationMarker: any |
| | | let amapApi: any |
| | | |
| | | async function loadAMap() { |
| | | if (amapApi) return amapApi |
| | | const key = import.meta.env.VITE_AMAP_KEY?.trim() |
| | | const securityJsCode = import.meta.env.VITE_AMAP_SECURITY_CODE?.trim() |
| | | if (!key || !securityJsCode) throw new Error('AMap credentials are not configured') |
| | | ;(window as any)._AMapSecurityConfig = { securityJsCode } |
| | | amapApi = await loadAMapApi({ |
| | | key, |
| | | version: '2.0', |
| | | plugins: ['AMap.PlaceSearch', 'AMap.Scale', 'AMap.ToolBar'], |
| | | }) |
| | | return amapApi |
| | | } |
| | | |
| | | interface ParkingAddress { |
| | | name: string |
| | |
| | | } |
| | | |
| | | function selectedCoordinates() { |
| | | const longitude = Number(pendingLongitude.value) |
| | | const latitude = Number(pendingLatitude.value) |
| | | const longitudeText = pendingLongitude.value.trim() |
| | | const latitudeText = pendingLatitude.value.trim() |
| | | if (!longitudeText || !latitudeText) return undefined |
| | | const longitude = Number(longitudeText) |
| | | const latitude = Number(latitudeText) |
| | | return Number.isFinite(longitude) && Number.isFinite(latitude) ? { longitude, latitude } : undefined |
| | | } |
| | | |
| | |
| | | pendingLongitude.value = longitude.toFixed(6) |
| | | pendingLatitude.value = latitude.toFixed(6) |
| | | if (!locationMap) return |
| | | if (!locationMarker) locationMarker = L.marker([latitude, longitude], { icon: mapMarkerIcon }).addTo(locationMap) |
| | | else locationMarker.setLatLng([latitude, longitude]) |
| | | if (center) locationMap.setView([latitude, longitude], Math.max(locationMap.getZoom(), 16)) |
| | | const position = [longitude, latitude] |
| | | if (!locationMarker) { |
| | | locationMarker = new amapApi.Marker({ position, anchor: 'bottom-center' }) |
| | | locationMap.add(locationMarker) |
| | | } else { |
| | | locationMarker.setPosition(position) |
| | | } |
| | | if (center) locationMap.setZoomAndCenter(Math.max(locationMap.getZoom(), 16), position) |
| | | } |
| | | |
| | | async function initializeLocationMap() { |
| | | await nextTick() |
| | | if (!mapElement.value || locationMap) return |
| | | const initial = selectedCoordinates() || { longitude: 101.718637, latitude: 26.582347 } |
| | | locationMap = L.map(mapElement.value, { zoomControl: true }).setView([initial.latitude, initial.longitude], selectedCoordinates() ? 16 : 13) |
| | | L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(locationMap) |
| | | locationMap.on('click', (event) => setMapMarker(event.latlng.lat, event.latlng.lng, false)) |
| | | if (selectedCoordinates()) setMapMarker(initial.latitude, initial.longitude, false) |
| | | window.setTimeout(() => locationMap?.invalidateSize(), 120) |
| | | try { |
| | | const AMap = await loadAMap() |
| | | const selected = selectedCoordinates() |
| | | const initial = selected || { longitude: 101.718637, latitude: 26.582347 } |
| | | locationMap = new AMap.Map(mapElement.value, { |
| | | viewMode: '2D', |
| | | zoom: selected ? 16 : 13, |
| | | center: [initial.longitude, initial.latitude], |
| | | resizeEnable: true, |
| | | }) |
| | | locationMap.addControl(new AMap.Scale()) |
| | | locationMap.addControl(new AMap.ToolBar({ position: { right: '10px', top: '10px' } })) |
| | | locationMap.on('click', (event: any) => setMapMarker(event.lnglat.getLat(), event.lnglat.getLng(), false)) |
| | | if (selected) setMapMarker(initial.latitude, initial.longitude, false) |
| | | window.setTimeout(() => locationMap?.resize(), 120) |
| | | } catch (error) { |
| | | console.error('Failed to initialize AMap', error) |
| | | ElMessage.error('高德地图加载失败,请检查地图配置或网络后重试') |
| | | } |
| | | } |
| | | |
| | | async function searchMapLocation() { |
| | |
| | | if (!keyword) { ElMessage.warning('请输入地址或地点关键词'); return } |
| | | locationSearching.value = true |
| | | try { |
| | | const response = await fetch(`https://nominatim.openstreetmap.org/search?format=jsonv2&limit=6&accept-language=zh-CN&q=${encodeURIComponent(keyword)}`) |
| | | if (!response.ok) throw new Error('search failed') |
| | | const records = await response.json() as Array<{ display_name: string; lon: string; lat: string }> |
| | | locationSearchResults.value = records.map((item) => ({ name: item.display_name, longitude: Number(item.lon), latitude: Number(item.lat) })).filter((item) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude)) |
| | | const AMap = await loadAMap() |
| | | const placeSearch = new AMap.PlaceSearch({ city: '攀枝花', citylimit: false, pageSize: 6, pageIndex: 1, extensions: 'base' }) |
| | | const records = await new Promise<any[]>((resolve, reject) => { |
| | | placeSearch.search(keyword, (status: string, result: any) => { |
| | | if (status === 'complete') resolve(result?.poiList?.pois || []) |
| | | else if (status === 'no_data') resolve([]) |
| | | else reject(new Error(result?.info || 'place search failed')) |
| | | }) |
| | | }) |
| | | locationSearchResults.value = records.map((item) => { |
| | | const longitude = Number(item.location?.getLng?.() ?? item.location?.lng) |
| | | const latitude = Number(item.location?.getLat?.() ?? item.location?.lat) |
| | | const address = [item.name, item.address].filter(Boolean).join(' - ') |
| | | return { name: address, longitude, latitude } |
| | | }).filter((item) => Number.isFinite(item.longitude) && Number.isFinite(item.latitude)) |
| | | if (!locationSearchResults.value.length) ElMessage.info('未找到匹配地点,请调整关键词或直接点击地图标点') |
| | | } catch { |
| | | } catch (error) { |
| | | console.error('Failed to search AMap places', error) |
| | | ElMessage.error('地点搜索失败,请检查网络后重试') |
| | | } finally { locationSearching.value = false } |
| | | } |
| | |
| | | } |
| | | |
| | | function closeLocationMap() { |
| | | locationMap?.remove() |
| | | locationMap?.destroy() |
| | | locationMap = undefined |
| | | locationMarker = undefined |
| | | locationSearchResults.value = [] |
| | |
| | | async function saveProduct() { |
| | | const valid = await productFormRef.value?.validate().catch(() => false) |
| | | if (!valid) return |
| | | if (!isHealthCheckMerchant.value && !isHotelMerchant.value) { |
| | | if (productRights.some((item) => item.free && !validFreeLimitCount(item.limitCount))) { |
| | | ElMessage.warning('免费次数请输入 1 至 999 的整数,999 表示不限次数') |
| | | return |
| | | } |
| | | if (productRights.some((item) => item.free && !validateProductExceedDiscount(item))) { |
| | | return |
| | | } |
| | | } else { |
| | | if (isHealthCheckMerchant.value) { |
| | | if (!productValidPeriodRange.value || productValidPeriodRange.value.length !== 2) { |
| | | ElMessage.warning('请选择可预约时间') |
| | | return |
| | |
| | | ElMessage.warning('请完善检查项目信息(名称、意义、适用性别均不能为空)') |
| | | return |
| | | } |
| | | } |
| | | } else if (!isHotelMerchant.value) { |
| | | if (productRights.some((item) => item.free && !validFreeLimitCount(item.limitCount))) { |
| | | ElMessage.warning('免费次数请输入 1 至 999 的整数,999 表示不限次数') |
| | | return |
| | | } |
| | | if (productRights.some((item) => item.free && !validateProductExceedDiscount(item))) { |
| | | return |
| | | } |
| | | } |
| | | productSaving.value = true |
| | |
| | | availableTime: rangeText(productAvailableTimeRange.value), |
| | | }, |
| | | productImages: [...productImages.value], |
| | | rightsConfig: productRights.map((item) => ({ ...item })), |
| | | rightsConfig: isHealthCheckMerchant.value || isHotelMerchant.value |
| | | ? [] |
| | | : productRights.map((item) => ({ ...item })), |
| | | } |
| | | if (editingProductId.value) { |
| | | await updateProduct(editingProductId.value, payload) |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-if="isParkingMerchant || isChildcareMerchant" class="merchant-rights-detail-row consume-desc-row"> |
| | | <div v-if="isChildcareMerchant" class="merchant-rights-detail-row consume-desc-row"> |
| | | <label>到店消费使用描述</label> |
| | | <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" /> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | |
| | | <div v-if="isParkingMerchant" class="merchant-rights-parking-address-panel" style="padding: 0 100px;"> |
| | | <div class="parking-address-header">停车场地址</div> |
| | | <div class="parking-address-table-wrapper"> |
| | | <el-table :data="parkingAddresses" border> |
| | | <el-table-column label="名称"> |
| | | <template #default="{ row }"><el-input v-model="row.name" placeholder="请输入" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="地址"> |
| | | <template #default="{ row }"><el-input v-model="row.address" placeholder="请输入" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="定位标点" width="120" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" size="small" @click="markParkingLocation(row)">{{ row.latitude ? '已标点' : '未标点' }}</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button link type="danger" @click="removeParkingAddress($index)">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div style="text-align: center; margin-top: 16px;"> |
| | | <el-button plain @click="addParkingAddress">新增</el-button> |
| | | </div> |
| | | <div v-if="isParkingMerchant" class="merchant-rights-detail-row consume-desc-row"> |
| | | <label>到店消费使用描述</label> |
| | | <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" /> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | </div> |
| | | </section> |
| | | |
| | | <section v-if="isParkingMerchant" class="merchant-parking-address-panel"> |
| | | <h3 class="merchant-parking-address-title">停车场地址</h3> |
| | | <el-table :data="parkingAddresses" border> |
| | | <el-table-column label="名称"> |
| | | <template #default="{ row }"><el-input v-model="row.name" placeholder="请输入" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="地址"> |
| | | <template #default="{ row }"><el-input v-model="row.address" placeholder="请输入" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="定位标点" width="120" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" size="small" @click="markParkingLocation(row)">{{ row.latitude ? '已标点' : '未标点' }}</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button link type="danger" @click="removeParkingAddress($index)">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="merchant-parking-address-actions"> |
| | | <el-button plain @click="addParkingAddress">新增</el-button> |
| | | </div> |
| | | </section> |
| | | |
| | | <section v-if="isChildcareMerchant" class="merchant-school-address-panel"> |
| | | <h3 class="merchant-school-address-title">学校地址</h3> |
| | | <el-table :data="schoolAddresses" border> |
| | |
| | | @Transactional |
| | | public Map<String, Object> createProduct(ProductCreateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateProductConfiguration(request.merchantId(), request.salePrice(), request.productImages()); |
| | | validateProductRights(request.merchantId(), request.rightsConfig()); |
| | | String productNo = BusinessIdGenerator.next("P"); |
| | | BigDecimal subsidyAmount = valueOrZero(request.subsidyAmount()); |
| | |
| | | @Transactional |
| | | public Map<String, Object> updateProduct(Long id, ProductUpdateRequest request) { |
| | | ensureMerchantExists(request.merchantId()); |
| | | validateProductConfiguration(request.merchantId(), request.salePrice(), request.productImages()); |
| | | validateProductRights(request.merchantId(), request.rightsConfig()); |
| | | BigDecimal subsidyAmount = valueOrZero(request.subsidyAmount()); |
| | | int updated = jdbcTemplate.update(""" |
| | |
| | | if (rightsConfig == null) { |
| | | return; |
| | | } |
| | | if (isHotelMerchant(merchantId)) { |
| | | if (isHotelMerchant(merchantId) || isHealthCheckMerchant(merchantId)) { |
| | | return; |
| | | } |
| | | for (Map<String, Object> right : rightsConfig) { |
| | |
| | | } |
| | | } |
| | | |
| | | private void validateProductConfiguration(Long merchantId, BigDecimal salePrice, List<String> productImages) { |
| | | if (isHealthCheckMerchant(merchantId)) { |
| | | return; |
| | | } |
| | | if (salePrice == null) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "商品金额不能为空"); |
| | | } |
| | | if (productImages == null || productImages.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "请至少添加一张商品图片"); |
| | | } |
| | | } |
| | | |
| | | private boolean isHotelMerchant(Long merchantId) { |
| | | String type = merchantType(merchantId); |
| | | return "hotel".equalsIgnoreCase(type) || "酒店服务".equals(type); |
| | | } |
| | | |
| | | private boolean isHealthCheckMerchant(Long merchantId) { |
| | | String type = merchantType(merchantId); |
| | | return "healthCheck".equalsIgnoreCase(type) || "免费体检".equals(type); |
| | | } |
| | | |
| | | private String merchantType(Long merchantId) { |
| | | List<String> types = jdbcTemplate.queryForList( |
| | | "select merchant_type from merchant where deleted = 0 and id = ? limit 1", |
| | | String.class, |
| | | merchantId); |
| | | if (types.isEmpty()) { |
| | | return false; |
| | | return ""; |
| | | } |
| | | String type = types.get(0) == null ? "" : types.get(0).trim(); |
| | | return "hotel".equalsIgnoreCase(type) || "酒店服务".equals(type); |
| | | return types.get(0) == null ? "" : types.get(0).trim(); |
| | | } |
| | | |
| | | private void validateCouponConfiguration(String validStart, String validEnd, String productScope, List<Long> productIds) { |
| New file |
| | |
| | | package com.talentcard.server.business.admin; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.util.Map; |
| | | |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.common.ApiResponse; |
| | | import org.springframework.format.annotation.DateTimeFormat; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PatchMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/admin/cards/child-applications") |
| | | public class ChildInformationAdminController { |
| | | |
| | | private final ChildInformationAdminService service; |
| | | |
| | | public ChildInformationAdminController(ChildInformationAdminService service) { |
| | | this.service = service; |
| | | } |
| | | |
| | | @GetMapping |
| | | public ApiResponse<PageResult<Map<String, Object>>> page( |
| | | @RequestParam(required = false) String childKeyword, |
| | | @RequestParam(required = false) String talentKeyword, |
| | | @RequestParam(required = false) String cardLevel, |
| | | @RequestParam(required = false) String status, |
| | | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate applyStart, |
| | | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate applyEnd, |
| | | @RequestParam(defaultValue = "1") int page, |
| | | @RequestParam(defaultValue = "10") int size |
| | | ) { |
| | | return ApiResponse.success(service.page( |
| | | childKeyword, |
| | | talentKeyword, |
| | | cardLevel, |
| | | status, |
| | | applyStart, |
| | | applyEnd, |
| | | page, |
| | | size |
| | | )); |
| | | } |
| | | |
| | | @GetMapping("/{id}") |
| | | public ApiResponse<Map<String, Object>> detail(@PathVariable Long id) { |
| | | return ApiResponse.success(service.detail(id)); |
| | | } |
| | | |
| | | @PatchMapping("/{id}/audit") |
| | | public ApiResponse<Map<String, Object>> audit( |
| | | @PathVariable Long id, |
| | | @RequestBody Map<String, String> body |
| | | ) { |
| | | return ApiResponse.success(service.audit(id, body.get("status"), body.get("auditRemark"))); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.talentcard.server.business.admin; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import com.talentcard.server.business.DatabaseRows; |
| | | import com.talentcard.server.business.PageResult; |
| | | import com.talentcard.server.common.BusinessException; |
| | | import com.talentcard.server.common.ErrorCode; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | @Service |
| | | public class ChildInformationAdminService { |
| | | |
| | | private static final List<String> AUDIT_STATUSES = List.of("待审核", "已通过", "审核不通过"); |
| | | |
| | | private final JdbcTemplate jdbc; |
| | | |
| | | public ChildInformationAdminService(JdbcTemplate jdbc) { |
| | | this.jdbc = jdbc; |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public PageResult<Map<String, Object>> page( |
| | | String childKeyword, |
| | | String talentKeyword, |
| | | String cardLevel, |
| | | String status, |
| | | LocalDate applyStart, |
| | | LocalDate applyEnd, |
| | | int page, |
| | | int size |
| | | ) { |
| | | validateFilter(status, applyStart, applyEnd); |
| | | int safePage = Math.max(page, 1); |
| | | int safeSize = Math.min(Math.max(size, 1), 100); |
| | | List<Object> parameters = new ArrayList<>(); |
| | | String where = where(childKeyword, talentKeyword, cardLevel, status, applyStart, applyEnd, parameters); |
| | | Long total = jdbc.queryForObject("select count(*) " + baseFrom() + where, Long.class, parameters.toArray()); |
| | | if (total == null || total == 0) { |
| | | return new PageResult<>(List.of(), safePage, safeSize, 0); |
| | | } |
| | | |
| | | List<Object> pageParameters = new ArrayList<>(parameters); |
| | | pageParameters.add(safeSize); |
| | | pageParameters.add((safePage - 1) * safeSize); |
| | | List<Map<String, Object>> records = DatabaseRows.normalize(jdbc.queryForList( |
| | | selectFields() + baseFrom() + where + " order by ci.submitted_at desc, ci.id desc limit ? offset ?", |
| | | pageParameters.toArray() |
| | | )).stream().map(this::view).toList(); |
| | | return new PageResult<>(records, safePage, safeSize, total); |
| | | } |
| | | |
| | | @Transactional(readOnly = true) |
| | | public Map<String, Object> detail(Long id) { |
| | | List<Map<String, Object>> rows = DatabaseRows.normalize(jdbc.queryForList( |
| | | selectFields() + baseFrom() + " where ci.id = ? and ci.deleted = 0", |
| | | id |
| | | )); |
| | | if (rows.isEmpty()) { |
| | | throw new BusinessException(ErrorCode.NOT_FOUND, "子女信息不存在"); |
| | | } |
| | | return view(rows.get(0)); |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> audit(Long id, String status, String auditRemark) { |
| | | String targetStatus = clean(status); |
| | | if (!List.of("已通过", "审核不通过").contains(targetStatus)) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "审核状态只能为已通过或审核不通过"); |
| | | } |
| | | String remark = clean(auditRemark); |
| | | if ("审核不通过".equals(targetStatus) && remark.isBlank()) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "审核不通过时必须填写原因"); |
| | | } |
| | | int updated = jdbc.update(""" |
| | | update child_information |
| | | set audit_status = ?, reject_reason = ?, updated_at = now() |
| | | where id = ? and deleted = 0 and audit_status = '待审核' |
| | | """, targetStatus, "审核不通过".equals(targetStatus) ? remark : null, id); |
| | | if (updated != 1) { |
| | | Integer exists = jdbc.queryForObject( |
| | | "select count(*) from child_information where id = ? and deleted = 0", |
| | | Integer.class, |
| | | id |
| | | ); |
| | | if (exists == null || exists == 0) { |
| | | throw new BusinessException(ErrorCode.NOT_FOUND, "子女信息不存在"); |
| | | } |
| | | throw new BusinessException(ErrorCode.CONFLICT, "该子女信息已审核,请刷新后重试"); |
| | | } |
| | | return detail(id); |
| | | } |
| | | |
| | | private String selectFields() { |
| | | return """ |
| | | select ci.id, |
| | | ci.child_name, |
| | | ci.gender, |
| | | ci.birth_date, |
| | | ci.proof_file_ids, |
| | | ci.audit_status as status, |
| | | ci.reject_reason, |
| | | ci.submitted_at as apply_time, |
| | | ci.submitted_at as register_time, |
| | | u.name as talent_name, |
| | | u.id_card_no as talent_id_card_no, |
| | | u.gender as talent_gender, |
| | | u.mobile as talent_mobile, |
| | | u.contact_address, |
| | | u.work_address, |
| | | coalesce(tc.card_no, u.talent_card_no) as talent_card_no, |
| | | coalesce(tc.card_level, u.talent_level) as card_level |
| | | """; |
| | | } |
| | | |
| | | private String baseFrom() { |
| | | return """ |
| | | from child_information ci |
| | | join talent_user u on u.id = ci.talent_id and u.deleted = 0 |
| | | left join talent_card tc on tc.id = ( |
| | | select max(card.id) |
| | | from talent_card card |
| | | where card.talent_id = ci.talent_id |
| | | and card.card_type = 'PERSONAL' |
| | | and card.deleted = 0 |
| | | ) |
| | | """; |
| | | } |
| | | |
| | | private String where( |
| | | String childKeyword, |
| | | String talentKeyword, |
| | | String cardLevel, |
| | | String status, |
| | | LocalDate applyStart, |
| | | LocalDate applyEnd, |
| | | List<Object> parameters |
| | | ) { |
| | | StringBuilder sql = new StringBuilder(" where ci.deleted = 0"); |
| | | String child = clean(childKeyword); |
| | | if (!child.isBlank()) { |
| | | sql.append(" and ci.child_name like ?"); |
| | | parameters.add("%" + child + "%"); |
| | | } |
| | | String talent = clean(talentKeyword); |
| | | if (!talent.isBlank()) { |
| | | sql.append(" and (u.name like ? or u.mobile like ? or u.talent_card_no like ? or tc.card_no like ?)"); |
| | | String like = "%" + talent + "%"; |
| | | parameters.addAll(List.of(like, like, like, like)); |
| | | } |
| | | String level = clean(cardLevel); |
| | | if (!level.isBlank()) { |
| | | sql.append(" and coalesce(tc.card_level, u.talent_level) = ?"); |
| | | parameters.add(level); |
| | | } |
| | | String auditStatus = clean(status); |
| | | if (!auditStatus.isBlank()) { |
| | | sql.append(" and ci.audit_status = ?"); |
| | | parameters.add(auditStatus); |
| | | } |
| | | if (applyStart != null) { |
| | | sql.append(" and ci.submitted_at >= ?"); |
| | | parameters.add(applyStart.atStartOfDay()); |
| | | } |
| | | if (applyEnd != null) { |
| | | sql.append(" and ci.submitted_at < ?"); |
| | | parameters.add(applyEnd.plusDays(1).atStartOfDay()); |
| | | } |
| | | return sql.toString(); |
| | | } |
| | | |
| | | private Map<String, Object> view(Map<String, Object> row) { |
| | | Map<String, Object> result = new LinkedHashMap<>(row); |
| | | String proofFileIds = clean(row.get("proofFileIds")); |
| | | result.remove("proofFileIds"); |
| | | result.put("images", proofFileIds.isBlank() |
| | | ? List.of() |
| | | : Arrays.stream(proofFileIds.split(",")) |
| | | .map(String::trim) |
| | | .filter(value -> !value.isBlank()) |
| | | .toList()); |
| | | return result; |
| | | } |
| | | |
| | | private void validateFilter(String status, LocalDate applyStart, LocalDate applyEnd) { |
| | | String value = clean(status); |
| | | if (!value.isBlank() && !AUDIT_STATUSES.contains(value)) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "子女信息状态不正确"); |
| | | } |
| | | if (applyStart != null && applyEnd != null && applyStart.isAfter(applyEnd)) { |
| | | throw new BusinessException(ErrorCode.BAD_REQUEST, "申请开始时间不能晚于结束时间"); |
| | | } |
| | | } |
| | | |
| | | private String clean(Object value) { |
| | | return value == null ? "" : String.valueOf(value).trim(); |
| | | } |
| | | } |
| | |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.Digits; |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotEmpty; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | |
| | | String subtitle, |
| | | @NotBlank String coverUrl, |
| | | BigDecimal originalPrice, |
| | | @NotNull @DecimalMin(value = "0.01") @Digits(integer = 10, fraction = 2) BigDecimal salePrice, |
| | | @DecimalMin(value = "0.01") @Digits(integer = 10, fraction = 2) BigDecimal salePrice, |
| | | BigDecimal subsidyAmount, |
| | | Integer stock, |
| | | @NotBlank String detailText, |
| | |
| | | Map<String, Object> purchaseNotice, |
| | | String validStartTime, |
| | | String validEndTime, |
| | | @NotEmpty List<@NotBlank String> productImages, |
| | | List<@NotBlank String> productImages, |
| | | List<Map<String, Object>> rightsConfig |
| | | ) { |
| | | } |
| | |
| | | import jakarta.validation.constraints.DecimalMin; |
| | | import jakarta.validation.constraints.Digits; |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotEmpty; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.Size; |
| | | |
| | |
| | | String subtitle, |
| | | @NotBlank String coverUrl, |
| | | BigDecimal originalPrice, |
| | | @NotNull @DecimalMin(value = "0.01") @Digits(integer = 10, fraction = 2) BigDecimal salePrice, |
| | | @DecimalMin(value = "0.01") @Digits(integer = 10, fraction = 2) BigDecimal salePrice, |
| | | BigDecimal subsidyAmount, |
| | | Integer stock, |
| | | @NotBlank String detailText, |
| | |
| | | Map<String, Object> purchaseNotice, |
| | | String validStartTime, |
| | | String validEndTime, |
| | | @NotEmpty List<@NotBlank String> productImages, |
| | | List<@NotBlank String> productImages, |
| | | List<Map<String, Object>> rightsConfig, |
| | | Byte status, |
| | | Integer sortNo |