shinnosuke
2026-08-24 0bf56e89b11367825dd52c32ce0daf4f4afcc167
fix: align parking plate management and paid benefits
5个文件已修改
274 ■■■■■ 已修改文件
rencaika/pages/merchant/merchant.uvue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ParkingPlateDetailView.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/ParkingPlateView.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rencaika/pages/merchant/merchant.uvue
@@ -44,7 +44,7 @@
                </view>
                <view v-if="!merchantError" class="tabs">
                    <view class="tab-item" :class="currentTab == 'primary' ? 'active' : ''" @tap="changeTab('primary')"><text>{{isMedicalMerchant ? '预约' : (showUsageInstructions ? '权益内容' : '消费购买')}}</text><view class="tab-line"></view></view>
                    <view class="tab-item" :class="currentTab == 'primary' ? 'active' : ''" @tap="changeTab('primary')"><text>{{isMedicalMerchant ? '预约' : (isParkingMerchant && parkingBenefitText == '付费' ? '使用说明' : (showUsageInstructions ? '权益内容' : '消费购买'))}}</text><view class="tab-line"></view></view>
                    <view class="tab-item" :class="currentTab == 'rights' ? 'active' : ''" @tap="changeTab('rights')"><text>{{isParkingMerchant ? '停车场地址' : (isTransportMerchant ? '适用门店' : '权益详情')}}</text><view class="tab-line"></view></view>
                </view>
@@ -243,7 +243,7 @@
    const listLoaded = ref(false)
    const listError = ref(false)
    // 餐饮商家没有上架商品时,改为展示当前人才卡等级可用的到店消费说明。
    const showUsageInstructions = computed(() => !isMedicalMerchant.value && (!groupBuyingEnabled.value || (merchantType.value == 'food' && listLoaded.value && visiblePackages.value.length == 0)))
    const showUsageInstructions = computed(() => !isMedicalMerchant.value && ((isParkingMerchant.value && parkingBenefitText.value == '付费') || !groupBuyingEnabled.value || (merchantType.value == 'food' && listLoaded.value && visiblePackages.value.length == 0)))
    const failOnce = ref(false)
    const forceEmpty = ref(false)
    let parkingDetailLoaded = false
@@ -547,7 +547,7 @@
                ? parkingRightDetail
                : (data != null && data.rightsText != null && data.rightsText != '' ? data.rightsText as string : '请咨询商户')
            merchantRightsText.value = data != null && data.rightsText != null ? data.rightsText as string : '请咨询商户'
            parkingActionMode.value = data != null && data.actionMode == 'talentCode' ? 'talentCode' : 'plate'
            parkingActionMode.value = data != null && (data.actionMode == 'talentCode' || data.parkingType == 'paid') ? 'talentCode' : 'plate'
            parkingPlateMode.value = data != null && data.plateFormMode == 'withParking' ? 'withParking' : 'simple'
            parkingBenefitId.value = data != null && data.benefit != null && data.benefit.benefitId != null ? '' + data.benefit.benefitId : ''
            groupBuyingEnabled.value = false
talent-card-admin/src/views/MerchantCreateView.vue
@@ -817,7 +817,7 @@
          discount: '',
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: false,
          consumeEnabled: true,
          parkingType: 'paid',
        },
        {
@@ -829,7 +829,7 @@
          discount: '',
          governmentSubsidy: false,
          consumeDesc: '',
          consumeEnabled: false,
          consumeEnabled: true,
          parkingType: 'paid',
        },
      ]
@@ -1521,7 +1521,9 @@
function serializeMerchantRightsConfig() {
  return rightsConfigs.map((item, index) => {
    const payload = { ...item }
    if (!supportsStoreConsumption.value) {
    const supportsItemStoreConsumption = supportsStoreConsumption.value
      || (isParkingMerchant.value && item.parkingType === 'paid')
    if (!supportsItemStoreConsumption) {
      payload.storeConsume = false
      payload.discount = ''
      payload.governmentSubsidy = false
@@ -2685,7 +2687,7 @@
              </div>
          </div>
          <template v-if="supportsStoreConsumption && item.consumeEnabled">
          <template v-if="(supportsStoreConsumption && item.consumeEnabled) || (isParkingMerchant && item.parkingType === 'paid')">
            <template v-if="isHotelMerchant">
              <div class="merchant-consume-row hotel-voucher-row">
                <strong>免费券</strong>
@@ -2789,7 +2791,7 @@
            </div>
          </template>
          <div v-if="isParkingMerchant" class="merchant-rights-detail-row consume-desc-row">
          <div v-if="isParkingMerchant && item.parkingType === 'paid'" class="merchant-rights-detail-row consume-desc-row">
            <label>到店消费使用描述</label>
            <el-input v-model="item.consumeDesc" type="textarea" :rows="4" resize="none" placeholder="请输入" />
          </div>
talent-card-admin/src/views/ParkingPlateDetailView.vue
@@ -1,42 +1,173 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getParkingPlateOperationLogs, getParkingPlateSubmissionDetail, updateParkingPlateSubmissionStatus } from '../api/admin'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getParkingPlateSubmissionDetail, updateParkingPlateSubmissionStatus } from '../api/admin'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
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) }
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')],
function text(value: unknown) {
  return value === undefined || value === null || value === '' ? '-' : String(value)
}
function pick(group: Record<string, any> | undefined, ...keys: string[]) {
  if (!group) return ''
  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', ' ')
}
function statusName(value: unknown, fallback = '') {
  const raw = String(value ?? '').trim().toUpperCase().replace('-', '_')
  const label = fallback.trim()
  if (label === '已拒绝') return '已驳回'
  if (label) return label
  if (raw === 'ACTIVE' || raw === 'PROCESSED' || raw === 'PASSED' || raw === 'APPROVED') return '已处理'
  if (raw === 'REJECTED' || raw === 'REFUSED' || raw === 'DENIED') return '已驳回'
  if (raw === 'CANCELLED' || raw === 'CANCELED') return '已取消'
  return '待处理'
}
const talentInfo = computed(() => detail.value.talentInfo || {})
const submissionInfo = computed(() => detail.value.submissionInfo || {})
const statusInfo = computed(() => detail.value.statusInfo || {})
const cardholderItems = computed(() => [
  { label: '持卡人姓名:', value: text(pick(talentInfo.value, 'talentName') || pick(detail.value, 'talentName')) },
  { label: '手机号:', value: text(pick(talentInfo.value, 'talentMobile') || pick(detail.value, 'talentMobile')) },
  { label: '人才等级:', value: text(pick(talentInfo.value, 'cardLevel') || pick(detail.value, 'cardLevel', 'talentLevel')) },
])
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 } }
const submissionItems = computed(() => [
  { label: '车牌号:', value: text(pick(submissionInfo.value, 'plateNumber', 'plateNo') || pick(detail.value, 'plateNumber', 'plateNo')) },
  { label: '停车场:', value: text(pick(submissionInfo.value, 'parkingLotName', 'parkingLot', 'serviceName') || pick(detail.value, 'parkingLotName', 'parkingLot', 'serviceName')) },
  { label: '提交时间:', value: formatDate(pick(submissionInfo.value, 'submittedAt', 'createdAt') || pick(detail.value, 'submittedAt', 'createdAt')) },
])
const currentStatus = computed(() => statusName(
  pick(statusInfo.value, 'status') || pick(detail.value, 'status'),
  String(pick(statusInfo.value, 'statusName') || pick(detail.value, 'statusName') || ''),
))
const statusItems = computed(() => [
  { label: '状态:', value: currentStatus.value },
  { label: '处理时间:', value: formatDate(pick(statusInfo.value, 'handledAt', 'processedAt') || pick(detail.value, 'handledAt', 'processedAt')) },
  { label: '处理人:', value: text(pick(statusInfo.value, 'handledByName', 'handlerName', 'processedBy') || pick(detail.value, 'handledByName', 'handlerName', 'processedBy')) },
])
const canProcess = computed(() => !['已处理', '已驳回', '已取消'].includes(currentStatus.value))
async function loadData() {
  loading.value = true
  try {
    detail.value = await getParkingPlateSubmissionDetail(String(route.params.id)) || {}
  } finally {
    loading.value = false
  }
}
async function markHandled() {
  const id = detail.value.id || detail.value.registrationId
  if (!id || processing.value || !canProcess.value) return
  try {
    await ElMessageBox.confirm('确认将该停车车牌申请标记为已处理吗?', '确认处理', {
      type: 'warning',
      confirmButtonText: '确认处理',
      cancelButtonText: '取消',
    })
  } catch {
    return
  }
  processing.value = true
  try {
    detail.value = await updateParkingPlateSubmissionStatus(id, '已处理')
    ElMessage.success('已处理该车牌申请')
  } finally {
    processing.value = false
  }
}
onMounted(loadData)
</script>
<template>
  <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>
  <div v-loading="loading" class="parking-plate-detail-page">
    <section class="parking-plate-detail-panel">
      <div class="reservation-detail-section">
        <h3>持卡人信息</h3>
        <div class="reservation-detail-grid parking-plate-cardholder-grid">
          <div v-for="item in cardholderItems" :key="item.label" class="reservation-detail-item">
            <span>{{ item.label }}</span><strong>{{ item.value }}</strong>
          </div>
        </div>
      </div>
      <div class="reservation-detail-section">
        <h3>提交信息</h3>
        <div class="reservation-detail-grid parking-plate-submit-grid">
          <div v-for="item in submissionItems" :key="item.label" class="reservation-detail-item">
            <span>{{ item.label }}</span><strong>{{ item.value }}</strong>
          </div>
        </div>
      </div>
      <div class="reservation-detail-section">
        <h3>状态信息</h3>
        <div class="reservation-detail-grid parking-plate-status-grid">
          <div v-for="item in statusItems" :key="item.label" class="reservation-detail-item">
            <span>{{ item.label }}</span><strong>{{ item.value }}</strong>
          </div>
        </div>
      </div>
    </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>
    <el-button v-if="canProcess" type="primary" class="reservation-detail-action" :loading="processing" @click="markHandled">已处理</el-button>
  </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)); } }
.parking-plate-detail-page {
  display: grid;
  gap: 20px;
}
.parking-plate-detail-panel {
  min-height: 0;
  padding: 20px 18px 26px;
  background: #ffffff;
}
.parking-plate-detail-panel .reservation-detail-section {
  margin-bottom: 24px;
}
.parking-plate-detail-panel .reservation-detail-section:last-child {
  margin-bottom: 0;
}
.parking-plate-detail-panel .reservation-detail-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  min-height: 74px;
  padding: 22px 16px;
  background: #f1f3f5;
}
.parking-plate-detail-panel .parking-plate-submit-grid {
  min-height: 92px;
  align-content: start;
}
@media (max-width: 900px) {
  .parking-plate-detail-panel .reservation-detail-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
</style>
talent-card-admin/src/views/ParkingPlateView.vue
@@ -14,12 +14,17 @@
const statusOptions = ['待处理', '已处理', '已驳回', '已提交商家', '已录入']
const submissionTypes = ['新增', '修改']
function text(value: unknown) { return value === undefined || value === null || value === '' ? '-' : String(value) }
function formatDate(value: unknown) { return text(value).replace('T', ' ') }
function statusText(value: unknown) {
  const label = text(value)
  return label === '已拒绝' ? '已驳回' : label
}
function queryParams(size = pagination.size) { return { page: pagination.page, size, keyword: filters.keyword || undefined, serviceKeyword: filters.serviceKeyword || undefined, plateNumber: filters.plateNumber || undefined, cardLevel: filters.cardLevel || undefined, status: filters.status || undefined, submissionType: filters.submissionType || undefined, submittedStart: filters.submittedTime?.[0], submittedEnd: filters.submittedTime?.[1] } }
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() }
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 || []) }
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 => formatDate(row.submittedAt || row.createdAt) }, { title: '状态', value: row => statusText(row.statusName || row.status) }], result.records || []) }
onMounted(loadData)
</script>
<template>
@@ -43,8 +48,8 @@
        <el-table-column label="车牌号" min-width="120"><template #default="{ row }">{{ text(row.plateNumber || row.plateNo) }}</template></el-table-column>
        <el-table-column label="停车场" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ text(row.parkingLotName || row.parkingLot || row.serviceName) }}</template></el-table-column>
        <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="提交时间" min-width="170"><template #default="{ row }">{{ formatDate(row.submittedAt || row.createdAt) }}</template></el-table-column>
        <el-table-column label="状态" min-width="110"><template #default="{ row }">{{ statusText(row.statusName || row.status) }}</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>
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java
@@ -30,11 +30,13 @@
        Map<String, Object> benefit = parkingBenefit(merchantId);
        String cardLevel = talentCardId == null ? "" : talentCardLevel(talentCardId);
        List<Map<String, Object>> rights = parseList(merchant.get("merchantRightsConfig"));
        Map<String, Object> right = matchingRight(rights, cardLevel);
        boolean hasCardRightsConfig = !rights.isEmpty() && !cardLevel.isBlank();
        Map<String, Object> legacyRight = matchingRight(rights, cardLevel);
        Map<String, Object> configuredRight = parkingCardLevelRight(merchantId, cardLevel);
        Map<String, Object> right = mergeRight(legacyRight, configuredRight);
        boolean hasCardRightsConfig = (!rights.isEmpty() || configuredRight != null) && !cardLevel.isBlank();
        boolean available = number(merchant.get("status")) == 1
                && (!hasCardRightsConfig || right != null)
                && (right == null || booleanValue(right.get("enabled")));
                && (right == null || (booleanValue(right.get("enabled")) && rightStatusEnabled(right)));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("merchantId", merchant.get("id"));
@@ -50,10 +52,11 @@
        result.put("serviceLabel", text(merchant.get("serviceLabel"), "停车服务"));
        result.put("benefitText", benefitText(merchant, right, benefit));
        result.put("applicableCards", applicableCards(right, benefit));
        result.put("usageInstructions", text(merchant.get("usageInstructions"), "请咨询商户"));
        result.put("usageInstructions", usageInstructions(merchant, right));
        result.put("rightsText", rightsText(merchant, right));
        String parkingType = text(right == null ? null : right.get("parkingType"), "paid");
        result.put("actionMode", "free".equalsIgnoreCase(parkingType) ? "talentCode" : "plate");
        // 付费停车权益按到店消费规则使用,前端展示使用说明并出示人才码。
        result.put("actionMode", "paid".equalsIgnoreCase(parkingType) || "free".equalsIgnoreCase(parkingType) ? "talentCode" : "plate");
        result.put("plateFormMode", text(benefit.get("plateFormMode"), "simple"));
        result.put("benefit", benefitView(benefit));
        result.put("applicableStores", parkingStores(merchant.get("usageInstructions")));
@@ -151,6 +154,61 @@
        return rows.isEmpty() ? new LinkedHashMap<>() : rows.get(0);
    }
    private Map<String, Object> parkingCardLevelRight(Long merchantId, String cardLevel) {
        if (cardLevel == null || cardLevel.isBlank()) return null;
        List<Map<String, Object>> rows = query("""
                select card_level as level, enabled, label as tag, benefit_detail as detail,
                       charge_type as chargeType, selection_mode as selectionMode,
                       store_consume_enabled as storeConsumeEnabled, discount_text as discountText,
                       government_subsidy as governmentSubsidy, consume_usage_text as consumeDesc,
                       status as rightStatus
                from transport_parking_card_level_right
                where merchant_id = ? and card_level = ? and deleted = 0
                limit 1
                """, merchantId, cardLevel);
        if (rows.isEmpty()) return null;
        Map<String, Object> right = rows.get(0);
        String chargeType = text(right.get("chargeType"), "FREE");
        right.put("parkingType", "PAID".equalsIgnoreCase(chargeType) ? "paid" : "free");
        String selectionMode = text(right.get("selectionMode"), "ALL");
        right.put("parkingScope", "ONE".equalsIgnoreCase(selectionMode) ? "selected" : "all");
        // 付费权益的到店消费使用描述就是详情页的使用说明。
        if ("paid".equals(right.get("parkingType")) && text(right.get("consumeDesc"), "").isBlank()) {
            right.put("consumeDesc", text(right.get("detail"), ""));
        }
        return right;
    }
    private Map<String, Object> mergeRight(Map<String, Object> legacyRight, Map<String, Object> configuredRight) {
        if (legacyRight == null && configuredRight == null) return null;
        Map<String, Object> result = new LinkedHashMap<>();
        if (legacyRight != null) result.putAll(legacyRight);
        if (configuredRight != null) {
            for (Map.Entry<String, Object> entry : configuredRight.entrySet()) {
                Object value = entry.getValue();
                if (value != null && (!(value instanceof String text) || !text.isBlank())) result.put(entry.getKey(), value);
            }
        }
        String parkingType = text(result.get("parkingType"), "free");
        if ("paid".equalsIgnoreCase(parkingType) && !text(result.get("consumeDesc"), "").isBlank()) {
            result.put("detail", text(result.get("consumeDesc"), ""));
        }
        return result;
    }
    private boolean rightStatusEnabled(Map<String, Object> right) {
        Object status = right.get("rightStatus");
        return status == null || number(status) != 0;
    }
    private String usageInstructions(Map<String, Object> merchant, Map<String, Object> right) {
        if (right != null && "paid".equalsIgnoreCase(text(right.get("parkingType"), ""))) {
            String consumeDesc = text(right.get("consumeDesc"), text(right.get("detail"), ""));
            if (!consumeDesc.isBlank()) return consumeDesc;
        }
        return text(merchant.get("usageInstructions"), "请咨询商户");
    }
    private List<Map<String, Object>> parkingStores(Object usageInstructions) {
        List<Map<String, Object>> stores = parseList(usageInstructions);
        int index = 0;