fix: repair benefit submissions and parking review flow
| | |
| | | |
| | | <script setup lang="uts"> |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { submitBusiness } from '../../services/business.uts' |
| | | import { createIdempotencyKey } from '../../services/business.uts' |
| | | import { getTalentCardProfile, submitServiceAppointment } from '../../services/portal.uts' |
| | | import { getSession } from '../../services/session.uts' |
| | | |
| | | const navTop = ref(28) |
| | | const navHeight = ref(32) |
| | |
| | | return date.getFullYear() + '-' + (month < 10 ? '0' + month : '' + month) + '-' + (day < 10 ? '0' + day : '' + day) |
| | | } |
| | | const todayDate = getDateText(0) |
| | | // 姓名、手机号、身份证号由认证信息默认回填,可编辑修改,真实数据以后台返回为准。 |
| | | // 预约人信息可编辑,姓名在已登录人才卡资料可用时自动带入。 |
| | | const userName = ref('') |
| | | const userMobile = ref('') |
| | | const userIdCard = ref('') |
| | | const hospitalName = ref('攀枝花市中心医院') |
| | | const hospitalId = ref('') |
| | | const packageId = ref('') |
| | | const talentId = ref('') |
| | | const cardId = ref('') |
| | | // 预约套餐名称随入口参数带入,当前表单不展示,预留给预约提交接口。 |
| | | const packageName = ref('') |
| | | const expectDate = ref(getDateText(1)) |
| | |
| | | if (options != null) { |
| | | if (options['hospital'] != null) hospitalName.value = decodeURIComponent(options['hospital'] as string) |
| | | if (options['package'] != null) packageName.value = decodeURIComponent(options['package'] as string) |
| | | if (options['hospitalId'] != null) hospitalId.value = options['hospitalId'] as string |
| | | if (options['packageId'] != null) packageId.value = options['packageId'] as string |
| | | } |
| | | const session = getSession() |
| | | if (session != null && session.talentId != null) talentId.value = session.talentId as string |
| | | if (talentId.value != '') { |
| | | getTalentCardProfile(talentId.value, (data: any) => { |
| | | const card = data != null ? data.card : null |
| | | cardId.value = card != null && card.id != null ? '' + card.id : '' |
| | | if (userName.value == '' && card != null && card.name != null) userName.value = '' + card.name |
| | | }, () => {}) |
| | | } |
| | | }) |
| | | |
| | |
| | | uni.showToast({ title: '预约日期不能早于今天', icon: 'none' }) |
| | | return |
| | | } |
| | | if (!/^[1-9][0-9]*$/.test(hospitalId.value) || !/^[1-9][0-9]*$/.test(packageId.value)) { |
| | | uni.showToast({ title: '体检服务信息异常,请返回重新选择套餐', icon: 'none' }) |
| | | return |
| | | } |
| | | if (talentId.value == '' && cardId.value == '') { |
| | | uni.showToast({ title: '请先登录人才账号', icon: 'none' }) |
| | | return |
| | | } |
| | | isSubmitting.value = true |
| | | submitBusiness('medicalBooking', { hospitalName: hospitalName.value, packageName: packageName.value, userName: userName.value.trim(), userMobile: userMobile.value.trim(), userIdCard: userIdCard.value.trim(), expectDate: expectDate.value }, () => { |
| | | const payload: any = { |
| | | applicationType: 'MEDICAL_RESERVATION', |
| | | merchantId: Number(hospitalId.value), |
| | | packageId: Number(packageId.value), |
| | | packageName: packageName.value, |
| | | talentId: talentId.value != '' ? Number(talentId.value) : null, |
| | | cardId: cardId.value != '' ? Number(cardId.value) : null, |
| | | applicantName: userName.value.trim(), |
| | | applicantMobile: userMobile.value.trim(), |
| | | applicantIdCardNo: userIdCard.value.trim(), |
| | | hospitalName: hospitalName.value, |
| | | expectedDate: expectDate.value |
| | | } |
| | | submitServiceAppointment(payload, createIdempotencyKey('medical-booking'), () => { |
| | | uni.redirectTo({ url: '/pages/medical-booking-success/medical-booking-success?hospital=' + encodeURIComponent(hospitalName.value) }) |
| | | }, (error) => { |
| | | isSubmitting.value = false |
| | |
| | | productImages.value = Array.isArray(savedImages) |
| | | ? savedImages.filter((image): image is string => typeof image === 'string' && image.length > 0) |
| | | : [] |
| | | productForm.salePrice = row?.salePrice |
| | | productForm.originalPrice = row?.originalPrice |
| | | productForm.salePrice = isHealthCheckMerchant.value ? undefined : row?.salePrice |
| | | productForm.originalPrice = isHealthCheckMerchant.value ? undefined : row?.originalPrice |
| | | productForm.stock = row?.stock === -1 ? undefined : row?.stock |
| | | productForm.detailText = row?.detailText || '' |
| | | productForm.benefitDesc = row?.benefitDesc || '' |
| | |
| | | ...productForm, |
| | | merchantId: currentMerchantId(), |
| | | detailText: isHealthCheckMerchant.value ? JSON.stringify(healthCheckItems.value) : productForm.detailText, |
| | | salePrice: productForm.salePrice === undefined ? undefined : Number(productForm.salePrice), |
| | | originalPrice: productForm.originalPrice === undefined ? undefined : Number(productForm.originalPrice), |
| | | salePrice: isHealthCheckMerchant.value || productForm.salePrice === undefined ? undefined : Number(productForm.salePrice), |
| | | originalPrice: isHealthCheckMerchant.value || productForm.originalPrice === undefined ? undefined : Number(productForm.originalPrice), |
| | | stock: productForm.stock === undefined ? undefined : Number(productForm.stock), |
| | | sortNo: productForm.sortNo === undefined ? 0 : Number(productForm.sortNo), |
| | | purchaseNotice: { |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, onMounted, ref } from 'vue' |
| | | import { useRoute } from 'vue-router' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getParkingPlateSubmissionDetail, getParkingPlateOperationLogs, updateParkingPlateSubmissionStatus } from '../api/admin' |
| | | import { getParkingPlateOperationLogs, getParkingPlateSubmissionDetail, updateParkingPlateSubmissionStatus } from '../api/admin' |
| | | |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const loading = ref(false) |
| | | const detail = ref<Record<string, any>>() |
| | | const logs = ref<Record<string, any>[]>([]) |
| | | const processing = ref(false) |
| | | const detail = ref<Record<string, any>>({}) |
| | | const logs = ref<Record<string, any>[]>([]) |
| | | |
| | | function text(value: unknown) { return value === undefined || value === null || value === '' ? '-' : String(value) } |
| | | |
| | | const overviewFields = computed(() => [ |
| | | ['submissionNo', '????'], ['talentName', '???'], ['talentMobile', '???'], ['talentLevel', '????'], ['plateNumber', '???'], ['parkingLotName', '???'], ['submissionType', '????'], ['status', '??'], ['submittedAt', '????'], ['handledAt', '????'], ['handlerName', '???'], |
| | | function field(...keys: string[]) { const groups = [detail.value, detail.value.talentInfo || {}, detail.value.submissionInfo || {}, detail.value.statusInfo || {}]; for (const group of groups) { for (const key of keys) { if (group[key] !== undefined && group[key] !== null && group[key] !== '') return group[key] } } return '' } |
| | | function formatDate(value: unknown) { return text(value).replace('T', ' ') } |
| | | const overview = computed(() => [ |
| | | ['提交单号', field('submissionNo', 'applicationNo')], ['持卡人姓名', field('talentName')], ['手机号', field('talentMobile')], ['人才等级', field('talentLevel', 'cardLevel')], |
| | | ['车牌号', field('plateNumber', 'plateNo')], ['停车场', field('parkingLotName', 'parkingLot', 'serviceName')], ['提交类型', field('submissionType')], ['提交时间', formatDate(field('submittedAt', 'createdAt'))], |
| | | ['状态', field('status')], ['处理时间', formatDate(field('handledAt', 'processedAt'))], ['处理人', field('handlerName', 'processedBy')], |
| | | ]) |
| | | |
| | | async function loadData() { |
| | | loading.value = true |
| | | try { |
| | | const id = String(route.params.id) |
| | | const [info, logResult] = await Promise.all([getParkingPlateSubmissionDetail(id), getParkingPlateOperationLogs(id)]) |
| | | detail.value = info |
| | | logs.value = logResult || [] |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | async function mark(status: string) { |
| | | if (!detail.value?.id) return |
| | | processing.value = true |
| | | try { |
| | | detail.value = await updateParkingPlateSubmissionStatus(detail.value.id, status) |
| | | ElMessage.success('?????') |
| | | } finally { |
| | | processing.value = false |
| | | } |
| | | } |
| | | |
| | | async function loadData() { loading.value = true; try { const id = String(route.params.id); const [info, operationLogs] = await Promise.all([getParkingPlateSubmissionDetail(id), getParkingPlateOperationLogs(id)]); detail.value = info || {}; logs.value = operationLogs || [] } finally { loading.value = false } } |
| | | async function mark(status: string) { const id = detail.value.id || detail.value.registrationId; if (!id || processing.value) return; processing.value = true; try { detail.value = await updateParkingPlateSubmissionStatus(id, status); logs.value = await getParkingPlateOperationLogs(String(id)); ElMessage.success(status === '已驳回' ? '已驳回该车牌申请' : '已处理该车牌申请') } finally { processing.value = false } } |
| | | onMounted(loadData) |
| | | </script> |
| | | |
| | | <template> |
| | | <div v-loading="loading" class="page-stack"> |
| | | <div class="toolbar"><div><h2>??????</h2><p>??????????????</p></div><el-button @click="$router.back()">??</el-button></div> |
| | | <section class="panel"> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item v-for="[key, label] in overviewFields" :key="key" :label="label">{{ text(detail?.[key]) }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div v-loading="loading" class="page-stack parking-plate-detail"> |
| | | <div class="toolbar"><div><h2>停车车牌详情</h2><p>查看人才提交的车牌信息,并在详情页完成处理。</p></div><el-button @click="router.back()">返回</el-button></div> |
| | | <section class="panel detail-panel"> |
| | | <h3>持卡人信息</h3><div class="info-grid"><div v-for="[label, value] in overview.slice(0, 4)" :key="label" class="info-item"><span>{{ label }}</span><strong>{{ value }}</strong></div></div> |
| | | <h3>提交信息</h3><div class="info-grid"><div v-for="[label, value] in overview.slice(4, 8)" :key="label" class="info-item"><span>{{ label }}</span><strong>{{ value }}</strong></div></div> |
| | | <h3>状态信息</h3><div class="info-grid"><div v-for="[label, value] in overview.slice(8)" :key="label" class="info-item"><span>{{ label }}</span><strong>{{ value }}</strong></div></div> |
| | | <div class="detail-actions"><el-button type="primary" :loading="processing" :disabled="field('status') === '已处理'" @click="mark('已处理')">已处理</el-button><el-button type="danger" :loading="processing" :disabled="field('status') === '已驳回'" @click="mark('已驳回')">驳回</el-button></div> |
| | | </section> |
| | | <section class="panel"> |
| | | <div class="toolbar"><div><h3>????</h3></div><div><el-button type="primary" :loading="processing" @click="mark('???')">??</el-button><el-button type="danger" :loading="processing" @click="mark('???')">??</el-button></div></div> |
| | | <el-table :data="logs" border empty-text="????"> |
| | | <el-table-column prop="operationType" label="????" min-width="120" /> |
| | | <el-table-column prop="operatorName" label="???" min-width="120" /> |
| | | <el-table-column prop="beforeStatus" label="???" min-width="110" /> |
| | | <el-table-column prop="afterStatus" label="???" min-width="110" /> |
| | | <el-table-column prop="remark" label="??" min-width="180" show-overflow-tooltip /> |
| | | <el-table-column prop="createdAt" label="??" min-width="170" /> |
| | | </el-table> |
| | | </section> |
| | | <section class="panel"><div class="section-heading"><h3>操作记录</h3></div><el-table :data="logs" border empty-text="暂无操作记录"><el-table-column prop="operationType" label="操作类型" min-width="120" /><el-table-column prop="operatorName" label="操作人" min-width="120" /><el-table-column prop="beforeStatus" label="处理前状态" min-width="110" /><el-table-column prop="afterStatus" label="处理后状态" min-width="110" /><el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip /><el-table-column label="操作时间" min-width="170"><template #default="{ row }">{{ formatDate(row.createdAt) }}</template></el-table-column></el-table></section> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .detail-panel { padding: 24px 28px 28px; }.detail-panel h3 { margin: 0 0 14px; color: #1f2937; font-size: 16px; }.detail-panel h3:not(:first-child) { margin-top: 28px; }.info-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; overflow: hidden; border: 1px solid #e7ebf0; background: #e7ebf0; }.info-item { display: flex; min-height: 66px; padding: 14px 16px; box-sizing: border-box; background: #f8fafc; flex-direction: column; justify-content: center; gap: 7px; }.info-item span { color: #7b8794; font-size: 13px; }.info-item strong { color: #263445; font-size: 14px; font-weight: 500; word-break: break-all; }.detail-actions { display: flex; margin-top: 28px; gap: 12px; }.section-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }.section-heading h3 { margin: 0; color: #1f2937; font-size: 16px; }@media (max-width: 1000px) { .info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } |
| | | </style> |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { Download, Refresh, Search } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getParkingPlateSubmissions, updateParkingPlateSubmissionStatus } from '../api/admin' |
| | | import { getParkingPlateSubmissions } from '../api/admin' |
| | | import { downloadExcel } from '../utils/excel' |
| | | |
| | | const loading = ref(false) |
| | | const processingId = ref<number | string | null>(null) |
| | | const router = useRouter() |
| | | const rows = ref<Record<string, any>[]>([]) |
| | | const filters = reactive({ keyword: '', serviceKeyword: '', plateNumber: '', cardLevel: '', status: '', submissionType: '', submittedTime: [] as string[] }) |
| | | const pagination = reactive({ page: 1, size: 10, total: 0 }) |
| | |
| | | async function loadData() { loading.value = true; try { const result = await getParkingPlateSubmissions(queryParams()); rows.value = result.records || []; pagination.total = result.total || 0 } finally { loading.value = false } } |
| | | function search() { pagination.page = 1; loadData() } |
| | | function resetFilters() { Object.assign(filters, { keyword: '', serviceKeyword: '', plateNumber: '', cardLevel: '', status: '', submissionType: '', submittedTime: [] }); search() } |
| | | async function handle(row: Record<string, any>, status = '已处理') { processingId.value = row.id; try { await updateParkingPlateSubmissionStatus(row.id, status); ElMessage.success('状态已更新'); await loadData() } finally { processingId.value = null } } |
| | | function viewDetail(row: Record<string, any>) { router.push(`/parking-plates/${row.id}`) } |
| | | async function download() { const result = await getParkingPlateSubmissions({ ...queryParams(1000), page: 1 }); downloadExcel('停车车牌管理', [{ title: '提交单号', value: row => text(row.submissionNo || row.applicationNo) }, { title: '持卡人', value: row => text(row.talentName) }, { title: '手机号', value: row => text(row.talentMobile) }, { title: '车牌号', value: row => text(row.plateNumber || row.plateNo) }, { title: '停车场', value: row => text(row.parkingLotName || row.parkingLot || row.serviceName) }, { title: '提交类型', value: row => text(row.submissionType) }, { title: '提交时间', value: row => text(row.submittedAt || row.createdAt) }, { title: '状态', value: row => text(row.status) }], result.records || []) } |
| | | onMounted(loadData) |
| | | </script> |
| | |
| | | <el-table-column prop="submissionType" label="提交类型" min-width="100" /> |
| | | <el-table-column label="提交时间" min-width="170"><template #default="{ row }">{{ text(row.submittedAt || row.createdAt) }}</template></el-table-column> |
| | | <el-table-column prop="status" label="状态" min-width="110" /> |
| | | <el-table-column label="操作" width="170" fixed="right"><template #default="{ row }"><el-button link type="primary" :loading="processingId === row.id" @click="handle(row)">处理</el-button><el-button link type="danger" :loading="processingId === row.id" @click="handle(row, '已驳回')">驳回</el-button></template></el-table-column> |
| | | <el-table-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="viewDetail(row)">查看</el-button></template></el-table-column> |
| | | </el-table> |
| | | <div class="pagination-row"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, prev, pager, next" :total="pagination.total" @change="loadData" /></div> |
| | | </section> |
| | |
| | | request.sortNo() == null ? 0 : request.sortNo()); |
| | | Long id = jdbcTemplate.queryForObject("select last_insert_id()", Long.class); |
| | | ensureParkingBenefit(id, request.merchantType()); |
| | | ensureFuelBenefit(id, request.merchantType(), request.serviceLabel()); |
| | | Long storeId = createDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | if (hasMerchantAppAccess(request)) { |
| | |
| | | id); |
| | | ensureUpdated(updated); |
| | | ensureParkingBenefit(id, request.merchantType()); |
| | | ensureFuelBenefit(id, request.merchantType(), request.serviceLabel()); |
| | | Long storeId = findOrCreateDefaultStore(id, request.merchantName(), request.address()); |
| | | syncMerchantBusinessHours(id, storeId, request.businessHours()); |
| | | if (request.loginPassword() != null && !request.loginPassword().isBlank()) { |
| | |
| | | """, merchantId, merchantId); |
| | | } |
| | | |
| | | private void ensureFuelBenefit(Long merchantId, String merchantType, String serviceLabel) { |
| | | if (!"加油服务".equals(merchantType) && !"fuel".equalsIgnoreCase(merchantType)) return; |
| | | String discountText = serviceLabel == null || serviceLabel.isBlank() ? "请咨询商户" : serviceLabel.trim(); |
| | | jdbcTemplate.update(""" |
| | | insert into transport_fuel_benefit( |
| | | merchant_id, talent_levels, discount_text, oil_types, single_limit_text, annual_limit_text, |
| | | available_time, usage_instructions, rule_text, status, deleted |
| | | ) |
| | | select ?, '卓越人才卡、杰出人才卡、菁英人才卡', ?, '请咨询商户', '请咨询商户', '请咨询商户', |
| | | '请咨询商户', '提交手机号后,工作人员将根据加油权益与您联系。', |
| | | '具体适用油品、优惠额度、可用时段和活动规则以加油站说明为准。', 1, 0 |
| | | where not exists (select 1 from transport_fuel_benefit where merchant_id = ? and deleted = 0) |
| | | """, merchantId, discountText, merchantId); |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> updateMerchantStatus(Long id, Byte status) { |
| | | int updated = jdbcTemplate.update("update merchant set status = ? where deleted = 0 and id = ?", status, id); |
| | |
| | | m.longitude, |
| | | m.latitude |
| | | from merchant m |
| | | join premium_topic_merchant mapping on mapping.merchant_id = m.id and mapping.deleted = 0 |
| | | where m.id = #{merchantId} |
| | | and mapping.category_key = #{categoryKey} |
| | | and m.deleted = 0 |
| | | and m.status = 1 |
| | | and (exists (select 1 from premium_topic_merchant mapping |
| | | where mapping.merchant_id = m.id |
| | | and mapping.category_key = #{categoryKey} |
| | | and mapping.deleted = 0) |
| | | or (#{categoryKey} = 'medical' and m.merchant_type in ('medical', '免费体检', 'healthCheck'))) |
| | | limit 1 |
| | | """) |
| | | Map<String, Object> selectPremiumMerchantSnapshot(@Param("merchantId") Long merchantId, @Param("categoryKey") String categoryKey); |
| New file |
| | |
| | | insert into transport_fuel_benefit( |
| | | merchant_id, talent_levels, discount_text, oil_types, single_limit_text, annual_limit_text, |
| | | available_time, stock, valid_end, usage_instructions, rule_text, status, deleted |
| | | ) |
| | | select m.id, '卓越人才卡、杰出人才卡、菁英人才卡', |
| | | case when m.service_label is null or m.service_label = '' then '请咨询商户' else m.service_label end, |
| | | '请咨询商户', '请咨询商户', '请咨询商户', '请咨询商户', null, null, |
| | | '提交手机号后,工作人员将根据加油权益与您联系。', |
| | | '具体适用油品、优惠额度、可用时段和活动规则以加油站说明为准。', 1, 0 |
| | | from merchant m |
| | | where m.merchant_type in ('fuel', '加油服务') |
| | | and m.deleted = 0 |
| | | and not exists ( |
| | | select 1 from transport_fuel_benefit b |
| | | where b.merchant_id = m.id and b.deleted = 0 |
| | | ); |