shinnosuke
2026-08-13 ea7e8d87259a991c521190d674244cb067c7198d
fix(family-card): 修复管理列表与申请审核流程
2个文件已添加
8个文件已修改
333 ■■■■■ 已修改文件
talent-card-admin/src/router/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/types/admin.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/FamilyCardView.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/DependentCardApplyController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyService.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/response/DependentCardApplyAdminResponseTests.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/service/DependentCardApplyServiceTests.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts
@@ -48,6 +48,7 @@
          path: 'family-card-applications',
          name: 'family-card-applications',
          component: familyCardView,
          props: { mode: 'application' },
          meta: { title: '家属卡申请' },
        },
        {
@@ -72,6 +73,7 @@
          path: 'family-cards',
          name: 'family-cards',
          component: familyCardView,
          props: { mode: 'management' },
          meta: { title: '家属卡管理' },
        },
        {
talent-card-admin/src/style.css
@@ -3936,6 +3936,19 @@
  border-bottom: 1px solid #e5e7eb;
}
.family-card-management-page .family-card-filter-form {
  grid-template-columns: minmax(250px, 1.05fr) minmax(230px, 0.9fr) minmax(320px, 1.25fr) 74px;
  gap: 12px 28px;
}
.family-card-filter-talent {
  grid-column: 1;
}
.family-card-filter-date {
  grid-column: 3;
}
.family-card-filter-form .el-form-item {
  margin-bottom: 0;
}
@@ -3961,6 +3974,10 @@
  padding: 18px 0 16px;
}
.family-card-management-page .family-card-table-tools {
  padding: 14px 0 12px;
}
.family-card-table {
  color: #111827;
  font-size: 12px;
@@ -3979,6 +3996,14 @@
.family-card-table .el-table__cell {
  padding: 7px 0;
}
.family-card-management-page .family-card-table .el-table__header th {
  height: 40px;
}
.family-card-management-page .family-card-table .el-table__cell {
  padding: 6px 0;
}
.talent-register-range.el-date-editor {
@@ -4673,12 +4698,25 @@
    grid-template-columns: repeat(2, minmax(220px, 1fr));
  }
  .family-card-management-page .family-card-filter-form {
    grid-template-columns: minmax(0, 1fr);
  }
  .family-card-filter-talent,
  .family-card-filter-date {
    grid-column: auto;
  }
  .family-card-filter-actions {
    grid-row: auto;
    grid-column: auto;
    display: flex;
  }
  .family-card-page .panel {
    min-width: 0;
  }
  .product-basic-grid,
  .purchase-notice-grid {
    grid-template-columns: 1fr;
talent-card-admin/src/types/admin.ts
@@ -592,7 +592,7 @@
  statusText?: string
  auditTime?: string
  auditorName?: string
  auditBy?: string
  auditBy?: string | number
  auditRemark?: string
  proofFiles?: string | string[]
  familyTalentId?: number | string
talent-card-admin/src/views/FamilyCardView.vue
@@ -7,8 +7,10 @@
import type { FamilyCardApplication } from '../types/admin'
import { downloadExcel } from '../utils/excel'
const props = defineProps<{ mode?: 'application' | 'management' }>()
const route = useRoute()
const router = useRouter()
const isManagement = computed(() => props.mode === 'management')
const emptyText = computed(() => '暂无家属卡申请数据')
const filters = reactive({
@@ -34,6 +36,8 @@
  { label: '已驳回', value: 2 },
  { label: '已取消', value: 3 },
]
const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客']
function statusText(row: FamilyCardApplication) {
  return row.statusText || '-'
@@ -65,7 +69,7 @@
    const result = await getFamilyCardApplications({
      page: pagination.page,
      size: pagination.size,
      familyKeyword: filters.familyKeyword || undefined,
      familyKeyword: isManagement.value ? undefined : filters.familyKeyword || undefined,
      talentKeyword: filters.talentKeyword || undefined,
      cardLevel: filters.cardLevel || undefined,
      status: filters.status,
@@ -101,7 +105,7 @@
  const result = await getFamilyCardApplications({
    page: 1,
    size: 1000,
    familyKeyword: filters.familyKeyword || undefined,
    familyKeyword: isManagement.value ? undefined : filters.familyKeyword || undefined,
    talentKeyword: filters.talentKeyword || undefined,
    cardLevel: filters.cardLevel || undefined,
    status: filters.status,
@@ -109,8 +113,18 @@
    applyEnd: filters.applyTime?.[1],
  })
  downloadExcel(
    '家属卡申请列表',
    [
    isManagement.value ? '家属卡管理列表' : '家属卡申请列表',
    isManagement.value ? [
      { title: '申请编号', value: row => row.applicationNo || row.applyNo },
      { title: '持卡人姓名', value: row => row.talentName },
      { title: '持卡人手机号', value: row => row.talentMobile },
      { title: '卡片等级', value: row => row.cardLevel },
      { title: '家属姓名', value: row => familyName(row) },
      { title: '家属手机号', value: row => familyMobile(row) },
      { title: '家属关系', value: row => relationship(row) },
      { title: '申请时间', value: row => registerTime(row) },
      { title: '状态', value: row => statusText(row) },
    ] : [
      { title: '申请编号', value: row => row.applicationNo || row.applyNo },
      { title: '持卡人姓名', value: row => row.talentName },
      { title: '持卡人手机号', value: row => row.talentMobile },
@@ -131,7 +145,7 @@
}
function viewDetail(row: FamilyCardApplication) {
  router.push('/family-card-applications/' + row.id)
  router.push((isManagement.value ? '/family-cards/' : '/family-card-applications/') + row.id)
}
watch(() => route.path, () => resetFilters())
@@ -139,26 +153,26 @@
</script>
<template>
  <div class="page-stack prototype-page family-card-page">
  <div
    class="page-stack prototype-page family-card-page"
    :class="{ 'family-card-management-page': isManagement }"
  >
    <section class="panel">
      <div class="panel-header">
        <h3>查询表格</h3>
      </div>
      <el-form class="family-card-filter-form" :model="filters" label-width="84px">
        <el-form-item label="家属信息">
        <el-form-item v-if="!isManagement" label="家属信息">
          <el-input v-model="filters.familyKeyword" placeholder="请输入家属姓名、手机号或身份证号" clearable />
        </el-form-item>
        <el-form-item label="持卡人">
          <el-input v-model="filters.talentKeyword" placeholder="请输入持卡人姓名、手机号或卡号" clearable />
        <el-form-item label="持卡人" :class="{ 'family-card-filter-talent': isManagement }">
          <el-input v-model="filters.talentKeyword" placeholder="持卡人姓名、手机号、卡号" clearable />
        </el-form-item>
        <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 v-for="level in cardLevels" :key="level" :label="level" :value="level" />
          </el-select>
        </el-form-item>
        <el-form-item label="状态">
@@ -167,7 +181,7 @@
            <el-option v-for="item in statusOptions" :key="item.label" :label="item.label" :value="item.value" />
          </el-select>
        </el-form-item>
        <el-form-item label="申请时间">
        <el-form-item label="申请时间" :class="{ 'family-card-filter-date': isManagement }">
          <el-date-picker
            v-model="filters.applyTime"
            type="daterange"
@@ -184,10 +198,39 @@
      </el-form>
      <div class="family-card-table-tools">
        <el-button :icon="Download" @click="download">导出</el-button>
        <el-button :icon="Download" @click="download">{{ isManagement ? '下载' : '导出' }}</el-button>
      </div>
      <el-table v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
      <el-table v-if="isManagement" v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
        <el-table-column label="申请编号" min-width="112">
          <template #default="{ row }">{{ row.applicationNo || row.applyNo || row.id }}</template>
        </el-table-column>
        <el-table-column prop="talentName" label="持卡人名称" min-width="110" />
        <el-table-column prop="talentMobile" label="持卡人手机号" min-width="130" />
        <el-table-column prop="cardLevel" label="卡片等级" min-width="106" />
        <el-table-column label="家属名称" min-width="106">
          <template #default="{ row }">{{ familyName(row) }}</template>
        </el-table-column>
        <el-table-column label="家属手机号" min-width="130">
          <template #default="{ row }">{{ familyMobile(row) }}</template>
        </el-table-column>
        <el-table-column label="家属关系" min-width="102">
          <template #default="{ row }">{{ relationship(row) }}</template>
        </el-table-column>
        <el-table-column label="申请时间" min-width="150">
          <template #default="{ row }">{{ registerTime(row) }}</template>
        </el-table-column>
        <el-table-column label="状态" min-width="90">
          <template #default="{ row }">{{ statusText(row) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="72" fixed="right">
          <template #default="{ row }">
            <el-button link type="primary" @click="viewDetail(row)">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-table v-else v-loading="loading" :data="rows" class="family-card-table" :empty-text="emptyText">
        <el-table-column prop="id" label="编号" width="86" align="center" />
        <el-table-column prop="applicationNo" label="申请编号" min-width="160" />
        <el-table-column label="家属姓名" min-width="110">
@@ -226,7 +269,7 @@
          v-model:current-page="pagination.page"
          v-model:page-size="pagination.size"
          background
          layout="total, sizes, prev, pager, next"
          :layout="isManagement ? 'total, prev, pager, next' : 'total, sizes, prev, pager, next'"
          :page-sizes="[10, 20, 50]"
          :pager-count="5"
          :total="pagination.total"
talent-card-server/src/main/java/com/talentcard/server/business/portal/DependentCardApplyController.java
@@ -74,7 +74,7 @@
    @Operation(
            summary = "修改重新提交被驳回的家属附属卡申请",
            description = "仅已驳回的申请可以修改家属姓名、证件号、手机号、关系后重新提交,提交后状态回到待审核"
            description = "仅已驳回的申请可以重新提交;原申请保留不变,系统新增一条待审核申请记录"
    )
    @PostMapping("/{id}/resubmit")
    public ApiResponse<DependentCardApplyItemResponse> resubmit(
talent-card-server/src/main/java/com/talentcard/server/response/DependentCardApplyAdminResponse.java
@@ -88,7 +88,7 @@
                statusText(apply.getStatus()),
                apply.getApplyTime(),
                apply.getAuditBy(),
                apply.getAuditBy() == null ? null : String.valueOf(apply.getAuditBy()),
                string(extra.get("auditorName")),
                apply.getAuditTime(),
                apply.getAuditRemark(),
                apply.getCreatedAt(),
@@ -132,4 +132,4 @@
        if (value == null) return null;
        return LocalDateTime.parse(String.valueOf(value).replace(' ', 'T'));
    }
}
}
talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyAdminService.java
@@ -174,6 +174,7 @@
                       u.gender as gender,
                       u.contact_address as contactAddress,
                       u.work_address as workAddress,
                       ao.display_name as auditorName,
                       c.valid_start as validStart,
                       c.valid_end as validEnd,
                       c.issue_time as issueTime,
@@ -185,6 +186,7 @@
                from dependent_card_apply a
                left join talent_user u on u.id = a.talent_id and u.deleted = 0
                left join talent_card c on c.id = a.talent_card_id and c.deleted = 0
                left join admin_operator ao on ao.id = a.audit_by
                where a.id = ? and a.deleted = 0
                limit 1
                """, apply.getId());
@@ -238,4 +240,4 @@
        }
        return value.trim();
    }
}
}
talent-card-server/src/main/java/com/talentcard/server/service/DependentCardApplyService.java
@@ -1,6 +1,5 @@
package com.talentcard.server.service;
import java.time.LocalDateTime;
import java.util.List;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -157,33 +156,31 @@
        if (!Integer.valueOf(DependentCardApply.STATUS_REJECTED).equals(apply.getStatus())) {
            throw new BusinessException(ErrorCode.CONFLICT, "仅已驳回的申请可以修改重新提交");
        }
        int updated = mapper.update(
                null,
                Wrappers.<DependentCardApply>lambdaUpdate()
                        .eq(DependentCardApply::getId, applyId)
                        .eq(DependentCardApply::getStatus, DependentCardApply.STATUS_REJECTED)
                        .set(DependentCardApply::getDependentName, request.dependentName().trim())
                        .set(DependentCardApply::getDependentIdCardNo, request.dependentIdCardNo().trim().toUpperCase())
                        .set(DependentCardApply::getDependentMobile, request.dependentMobile().trim())
                        .set(DependentCardApply::getRelation, request.relation().trim())
                        .set(DependentCardApply::getStatus, DependentCardApply.STATUS_PENDING_REVIEW)
                        .set(DependentCardApply::getAuditBy, null)
                        .set(DependentCardApply::getAuditTime, null)
                        .set(DependentCardApply::getAuditRemark, null)
                        .set(DependentCardApply::getUpdatedAt, LocalDateTime.now())
        DependentCardApply resubmitted = new DependentCardApply(
                BusinessIdGenerator.next("DCA"),
                apply.getTalentId(),
                apply.getTalentCardId(),
                apply.getTalentName(),
                apply.getTalentMobile(),
                apply.getTalentCardNo(),
                apply.getCardLevel(),
                request.dependentName().trim(),
                request.dependentMobile().trim(),
                request.dependentIdCardNo().trim().toUpperCase(),
                request.relation().trim()
        );
        if (updated != 1) {
            throw new BusinessException(ErrorCode.CONFLICT, "申请状态已变化,请刷新后重试");
        if (mapper.insert(resubmitted) != 1) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "家属附属卡申请重新提交失败");
        }
        recordStatus(
                apply,
                DependentCardApply.STATUS_REJECTED,
                resubmitted,
                null,
                DependentCardApply.STATUS_PENDING_REVIEW,
                "TALENT",
                apply.getTalentId(),
                "用户修改后重新提交"
                resubmitted.getTalentId(),
                "用户基于已驳回申请重新提交,原申请ID:" + applyId
        );
        return DependentCardApplyItemResponse.from(mapper.selectById(applyId));
        return DependentCardApplyItemResponse.from(resubmitted);
    }
    private DependentCardApply ownerCard(Long talentCardId) {
        requireTalentCardId(talentCardId);
@@ -249,4 +246,4 @@
    private String text(Object value) {
        return value == null ? null : String.valueOf(value);
    }
}
}
talent-card-server/src/test/java/com/talentcard/server/response/DependentCardApplyAdminResponseTests.java
New file
@@ -0,0 +1,47 @@
package com.talentcard.server.response;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Map;
import com.talentcard.server.entity.DependentCardApply;
import org.junit.jupiter.api.Test;
class DependentCardApplyAdminResponseTests {
    @Test
    void returnsAuditorDisplayNameInsteadOfOperatorId() {
        DependentCardApply apply = new DependentCardApply(
                "DCA-001",
                1L,
                2L,
                "持卡人",
                "13900000000",
                "TC-001",
                "卓越人才卡",
                "家属",
                "13800000000",
                "510105199001011234",
                "配偶"
        );
        setAuditBy(apply, 86L);
        DependentCardApplyAdminResponse response = DependentCardApplyAdminResponse.from(
                apply,
                Map.of("auditorName", "审核员张三")
        );
        assertThat(response.auditBy()).isEqualTo(86L);
        assertThat(response.auditorName()).isEqualTo("审核员张三");
    }
    private void setAuditBy(DependentCardApply apply, Long auditBy) {
        try {
            var field = DependentCardApply.class.getDeclaredField("auditBy");
            field.setAccessible(true);
            field.set(apply, auditBy);
        } catch (ReflectiveOperationException exception) {
            throw new AssertionError(exception);
        }
    }
}
talent-card-server/src/test/java/com/talentcard/server/service/DependentCardApplyServiceTests.java
New file
@@ -0,0 +1,114 @@
package com.talentcard.server.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.talentcard.server.entity.DependentCardApply;
import com.talentcard.server.entity.DependentCardApplyStatusLog;
import com.talentcard.server.mapper.DependentCardApplyMapper;
import com.talentcard.server.mapper.DependentCardApplyStatusLogMapper;
import com.talentcard.server.request.DependentCardApplyRequest;
import com.talentcard.server.response.DependentCardApplyItemResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class DependentCardApplyServiceTests {
    @Mock
    private DependentCardApplyMapper mapper;
    @Mock
    private DependentCardApplyStatusLogMapper statusLogMapper;
    private DependentCardApplyService service;
    @BeforeEach
    void setUp() {
        service = new DependentCardApplyService(mapper, statusLogMapper);
    }
    @Test
    void resubmitCreatesANewApplicationAndKeepsRejectedApplicationUnchanged() {
        DependentCardApply rejected = new DependentCardApply(
                "DCA-OLD",
                12L,
                34L,
                "持卡人",
                "13900000000",
                "TC-001",
                "卓越人才卡",
                "旧家属",
                "13800000000",
                "510105199001011234",
                "配偶"
        );
        setStatus(rejected, DependentCardApply.STATUS_REJECTED);
        when(mapper.selectByIdForUpdate(99L)).thenReturn(rejected);
        when(mapper.insert(any(DependentCardApply.class))).thenAnswer(invocation -> {
            setId(invocation.getArgument(0), 100L);
            return 1;
        });
        when(statusLogMapper.insert(any(DependentCardApplyStatusLog.class))).thenReturn(1);
        DependentCardApplyItemResponse result = service.resubmit(
                99L,
                new DependentCardApplyRequest(
                        34L,
                        " 新家属 ",
                        "51010519900101123x",
                        "13700000000",
                        " 子女 "
                )
        );
        ArgumentCaptor<DependentCardApply> applicationCaptor = ArgumentCaptor.forClass(DependentCardApply.class);
        verify(mapper).insert(applicationCaptor.capture());
        DependentCardApply created = applicationCaptor.getValue();
        assertThat(created.getId()).isEqualTo(100L);
        assertThat(created.getApplyNo()).startsWith("DCA").isNotEqualTo(rejected.getApplyNo());
        assertThat(created.getTalentId()).isEqualTo(rejected.getTalentId());
        assertThat(created.getTalentCardId()).isEqualTo(rejected.getTalentCardId());
        assertThat(created.getDependentName()).isEqualTo("新家属");
        assertThat(created.getDependentIdCardNo()).isEqualTo("51010519900101123X");
        assertThat(created.getDependentMobile()).isEqualTo("13700000000");
        assertThat(created.getRelation()).isEqualTo("子女");
        assertThat(created.getStatus()).isEqualTo(DependentCardApply.STATUS_PENDING_REVIEW);
        assertThat(result.id()).isEqualTo(100L);
        assertThat(result.status()).isEqualTo(DependentCardApply.STATUS_PENDING_REVIEW);
        assertThat(rejected.getStatus()).isEqualTo(DependentCardApply.STATUS_REJECTED);
        verify(mapper, never()).update(any(), any(Wrapper.class));
        ArgumentCaptor<DependentCardApplyStatusLog> logCaptor = ArgumentCaptor.forClass(DependentCardApplyStatusLog.class);
        verify(statusLogMapper).insert(logCaptor.capture());
        assertThat(logCaptor.getValue().getApplyId()).isEqualTo(100L);
        assertThat(logCaptor.getValue().getApplyNo()).isEqualTo(created.getApplyNo());
        assertThat(logCaptor.getValue().getFromStatus()).isNull();
        assertThat(logCaptor.getValue().getToStatus()).isEqualTo(DependentCardApply.STATUS_PENDING_REVIEW);
    }
    private void setId(DependentCardApply apply, Long id) {
        setField(apply, "id", id);
    }
    private void setStatus(DependentCardApply apply, Integer status) {
        setField(apply, "status", status);
    }
    private void setField(DependentCardApply apply, String name, Object value) {
        try {
            var field = DependentCardApply.class.getDeclaredField(name);
            field.setAccessible(true);
            field.set(apply, value);
        } catch (ReflectiveOperationException exception) {
            throw new AssertionError(exception);
        }
    }
}