fix(family-card): 修复管理列表与申请审核流程
| | |
| | | path: 'family-card-applications', |
| | | name: 'family-card-applications', |
| | | component: familyCardView, |
| | | props: { mode: 'application' }, |
| | | meta: { title: '家属卡申请' }, |
| | | }, |
| | | { |
| | |
| | | path: 'family-cards', |
| | | name: 'family-cards', |
| | | component: familyCardView, |
| | | props: { mode: 'management' }, |
| | | meta: { title: '家属卡管理' }, |
| | | }, |
| | | { |
| | |
| | | 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; |
| | | } |
| | |
| | | padding: 18px 0 16px; |
| | | } |
| | | |
| | | .family-card-management-page .family-card-table-tools { |
| | | padding: 14px 0 12px; |
| | | } |
| | | |
| | | .family-card-table { |
| | | color: #111827; |
| | | font-size: 12px; |
| | |
| | | |
| | | .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 { |
| | |
| | | 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; |
| | |
| | | statusText?: string |
| | | auditTime?: string |
| | | auditorName?: string |
| | | auditBy?: string |
| | | auditBy?: string | number |
| | | auditRemark?: string |
| | | proofFiles?: string | string[] |
| | | familyTalentId?: number | string |
| | |
| | | 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({ |
| | |
| | | { label: '已驳回', value: 2 }, |
| | | { label: '已取消', value: 3 }, |
| | | ] |
| | | |
| | | const cardLevels = ['卓越人才卡', '杰出人才卡', '菁英人才卡', '普通游客'] |
| | | |
| | | function statusText(row: FamilyCardApplication) { |
| | | return row.statusText || '-' |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | 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 }, |
| | |
| | | } |
| | | |
| | | 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()) |
| | |
| | | </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="状态"> |
| | |
| | | <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" |
| | |
| | | </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"> |
| | |
| | | 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" |
| | |
| | | |
| | | @Operation( |
| | | summary = "修改重新提交被驳回的家属附属卡申请", |
| | | description = "仅已驳回的申请可以修改家属姓名、证件号、手机号、关系后重新提交,提交后状态回到待审核" |
| | | description = "仅已驳回的申请可以重新提交;原申请保留不变,系统新增一条待审核申请记录" |
| | | ) |
| | | @PostMapping("/{id}/resubmit") |
| | | public ApiResponse<DependentCardApplyItemResponse> resubmit( |
| | |
| | | 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(), |
| | |
| | | if (value == null) return null; |
| | | return LocalDateTime.parse(String.valueOf(value).replace(' ', 'T')); |
| | | } |
| | | } |
| | | } |
| | |
| | | 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, |
| | |
| | | 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()); |
| | |
| | | } |
| | | return value.trim(); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.talentcard.server.service; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | |
| | | 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); |
| | |
| | | private String text(Object value) { |
| | | return value == null ? null : String.valueOf(value); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |