shinnosuke
2026-08-25 aabd3fb155475168427f1c14966beb873bff6763
fix: complete appointment management data and filters
8个文件已修改
102 ■■■■■ 已修改文件
talent-card-admin/src/api/admin.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/style.css 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/types/admin.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/MerchantCreateView.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/admin/ServiceAppointmentAdminController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/ServiceApplicationOperationLogMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentApplicationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/api/admin.ts
@@ -332,14 +332,24 @@
  return http.get<PageResult<VerificationRecord>, PageResult<VerificationRecord>>('/api/orders/verifications', { params })
}
export function getServiceApplications(params: ServiceApplicationQuery = {}) {
function serviceApplicationParams(params: ServiceApplicationQuery = {}) {
  const requestParams: Record<string, unknown> = { ...params }
  if (params.talentKeyword && !requestParams.keyword) {
    requestParams.keyword = params.talentKeyword
  }
  if (!requestParams.serviceKeyword) {
    requestParams.serviceKeyword = params.merchantName || params.hospitalName || params.parkingLot
  }
  delete requestParams.talentKeyword
  delete requestParams.merchantName
  delete requestParams.hospitalName
  delete requestParams.parkingLot
  return requestParams
}
export function getServiceApplications(params: ServiceApplicationQuery = {}) {
  return http.get<PageResult<ServiceApplication>, PageResult<ServiceApplication>>('/api/admin/service-appointment-applications', {
    params: requestParams,
    params: serviceApplicationParams(params),
  })
}
@@ -364,7 +374,7 @@
export function exportServiceApplications(params: ServiceApplicationQuery = {}) {
  return http.get<Blob, Blob>('/api/admin/service-appointment-applications/export', {
    params,
    params: serviceApplicationParams(params),
    responseType: 'blob',
  })
}
talent-card-admin/src/style.css
@@ -1400,7 +1400,11 @@
}
.hotel-voucher-row {
  grid-template-columns: 76px 42px 104px 24px 64px 104px 96px 42px;
  grid-template-columns: 76px 42px 104px 24px 72px 104px 96px 42px;
}
.hotel-reset-label {
  white-space: nowrap;
}
.hotel-consume-row {
talent-card-admin/src/types/admin.ts
@@ -576,6 +576,7 @@
  size?: number
  applicationType?: ServiceApplicationType
  talentKeyword?: string
  serviceKeyword?: string
  merchantName?: string
  hospitalName?: string
  parkingLot?: string
talent-card-admin/src/views/MerchantCreateView.vue
@@ -2737,7 +2737,7 @@
                <el-switch v-model="item.freeVoucherEnabled" />
                <el-input v-model="item.freeVoucherTimes" placeholder="请输入" class="hotel-times-input" />
                <span>次</span>
                <span class="hotel-margin-left">重置周期</span>
                <span class="hotel-reset-label">重置周期</span>
                <el-select v-model="item.freeVoucherCycle" class="hotel-cycle-select">
                  <el-option label="每天" value="每天" />
                  <el-option label="每周" value="每周" />
talent-card-server/src/main/java/com/talentcard/server/business/admin/ServiceAppointmentAdminController.java
@@ -47,13 +47,15 @@
            @Parameter(description = "联系电话") @RequestParam(required = false) String applicantMobile,
            @Parameter(description = "服务机构关键词") @RequestParam(required = false) String serviceKeyword,
            @Parameter(description = "人才卡等级") @RequestParam(required = false) String cardLevel,
            @Parameter(description = "是否使用补贴:是、否") @RequestParam(required = false) String subsidy,
            @Parameter(description = "提交类型:新增、修改") @RequestParam(required = false) String submissionType,
            @Parameter(description = "处理状态") @RequestParam(required = false) String status,
            @Parameter(description = "提交开始日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedStart,
            @Parameter(description = "提交结束日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedEnd,
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int size
    ) {
        return ApiResponse.success(service.page(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, status, submittedStart, submittedEnd, page, size));
        return ApiResponse.success(service.page(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, subsidy, submissionType, status, submittedStart, submittedEnd, page, size));
    }
    @Operation(summary = "预约申请详情", description = "查看预约申请完整信息,包含服务快照、申请人、配偶或子女信息")
@@ -86,12 +88,14 @@
            @Parameter(description = "联系电话") @RequestParam(required = false) String applicantMobile,
            @Parameter(description = "服务机构关键词") @RequestParam(required = false) String serviceKeyword,
            @Parameter(description = "人才卡等级") @RequestParam(required = false) String cardLevel,
            @Parameter(description = "是否使用补贴:是、否") @RequestParam(required = false) String subsidy,
            @Parameter(description = "提交类型:新增、修改") @RequestParam(required = false) String submissionType,
            @Parameter(description = "处理状态") @RequestParam(required = false) String status,
            @Parameter(description = "提交开始日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedStart,
            @Parameter(description = "提交结束日期,格式yyyy-MM-dd") @RequestParam(required = false) String submittedEnd,
            HttpServletResponse response
    ) throws IOException {
        byte[] data = service.exportExcel(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, status, submittedStart, submittedEnd);
        byte[] data = service.exportExcel(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, subsidy, submissionType, status, submittedStart, submittedEnd);
        String filename = URLEncoder.encode("预约申请列表.xlsx", StandardCharsets.UTF_8).replace("+", "%20");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet").toString());
@@ -100,4 +104,4 @@
        response.getOutputStream().write(data);
        response.flushBuffer();
    }
}
}
talent-card-server/src/main/java/com/talentcard/server/mapper/ServiceApplicationOperationLogMapper.java
@@ -3,7 +3,11 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.talentcard.server.entity.ServiceApplicationOperationLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ServiceApplicationOperationLogMapper extends BaseMapper<ServiceApplicationOperationLog> {
}
    @Select("select display_name from admin_operator where id = #{operatorId} and deleted = 0 limit 1")
    String selectOperatorName(@Param("operatorId") Long operatorId);
}
talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentAdminService.java
@@ -59,13 +59,13 @@
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> page(String applicationType, String keyword, String applicantName,
                                                String applicantMobile, String serviceKeyword,
                                                String cardLevel, String status, String submittedStart,
                                                String cardLevel, String subsidy, String submissionType, String status, String submittedStart,
                                                String submittedEnd, int page, int size) {
        int safePage = Math.max(page, 1);
        int safeSize = Math.min(Math.max(size, 1), 100);
        QueryWrapper<ServiceApplication> countWrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, status, submittedStart, submittedEnd);
        QueryWrapper<ServiceApplication> countWrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, subsidy, submissionType, status, submittedStart, submittedEnd);
        Long total = applicationMapper.selectCount(countWrapper);
        QueryWrapper<ServiceApplication> pageWrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, status, submittedStart, submittedEnd)
        QueryWrapper<ServiceApplication> pageWrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, subsidy, submissionType, status, submittedStart, submittedEnd)
                .orderByDesc("submitted_at", "id")
                .last("limit " + safeSize + " offset " + ((safePage - 1) * safeSize));
        List<Map<String, Object>> records = applicationMapper.selectList(pageWrapper).stream()
@@ -101,7 +101,7 @@
        if (REJECTED_STATUSES.contains(nextStatus) && remark.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "拒绝或驳回时必须填写处理备注");
        }
        String handlerName = first(request.handlerName(), operatorId == null ? "" : "管理员" + operatorId, "管理员");
        String handlerName = first(operatorName(operatorId), request.handlerName(), "管理员");
        LocalDateTime handledAt = PENDING_STATUSES.contains(nextStatus) ? null : LocalDateTime.now();
        UpdateWrapper<ServiceApplication> update = new UpdateWrapper<ServiceApplication>()
                .set("status", nextStatus)
@@ -135,8 +135,8 @@
    @Transactional(readOnly = true)
    public byte[] exportExcel(String applicationType, String keyword, String applicantName,
                              String applicantMobile, String serviceKeyword,
                              String cardLevel, String status, String submittedStart, String submittedEnd) {
        QueryWrapper<ServiceApplication> wrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, status, submittedStart, submittedEnd)
                              String cardLevel, String subsidy, String submissionType, String status, String submittedStart, String submittedEnd) {
        QueryWrapper<ServiceApplication> wrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, subsidy, submissionType, status, submittedStart, submittedEnd)
                .orderByDesc("submitted_at", "id");
        List<ServiceApplication> applications = applicationMapper.selectList(wrapper);
        String[] headers = {
@@ -175,7 +175,7 @@
    private QueryWrapper<ServiceApplication> filter(String applicationType, String keyword, String applicantName,
                                                    String applicantMobile, String serviceKeyword,
                                                    String cardLevel, String status, String submittedStart, String submittedEnd) {
                                                    String cardLevel, String subsidy, String submissionType, String status, String submittedStart, String submittedEnd) {
        QueryWrapper<ServiceApplication> wrapper = new QueryWrapper<ServiceApplication>().eq("deleted", 0);
        String type = normalizeFilterType(applicationType);
        if (!type.isBlank()) {
@@ -218,6 +218,17 @@
        if (!clean(cardLevel).isBlank()) {
            wrapper.eq("talent_level", clean(cardLevel));
        }
        String subsidyValue = clean(subsidy);
        if ("是".equals(subsidyValue)) {
            wrapper.eq("subsidy", "是");
        } else if ("否".equals(subsidyValue)) {
            wrapper.and(query -> query.eq("subsidy", "否").or().isNull("subsidy").or().eq("subsidy", ""));
        } else if (!subsidyValue.isBlank()) {
            wrapper.eq("subsidy", subsidyValue);
        }
        if (!clean(submissionType).isBlank()) {
            wrapper.eq("submission_type", clean(submissionType));
        }
        if (!clean(status).isBlank()) {
            wrapper.eq("status", clean(status));
        }
@@ -258,6 +269,7 @@
        result.put("appointmentTime", first(application.getExpectedTime(), "以专员通知为准"));
        result.put("applicantName", participantName(application));
        result.put("applicantMobile", sensitiveDataCrypto.decrypt(first(application.getApplicantMobile(), application.getSpouseMobile(), application.getTalentMobile())));
        result.put("guestCount", guestCount(application));
        if (ServiceAppointmentApplicationService.TYPE_SPOUSE.equals(application.getApplicationType())) {
            result.put("spouseName", first(application.getSpouseName(), application.getApplicantName()));
            result.put("spouseMobile", sensitiveDataCrypto.decrypt(first(application.getSpouseMobile(), application.getApplicantMobile())));
@@ -270,6 +282,7 @@
    private Map<String, Object> detailView(ServiceApplication application) {
        Map<String, Object> result = listView(application);
        result.put("handlerName", handlerName(application));
        result.put("talentId", application.getTalentId());
        result.put("cardId", application.getCardId());
        result.put("merchantId", application.getMerchantId());
@@ -280,7 +293,7 @@
        result.put("applicantIdCardNo", sensitiveDataCrypto.decrypt(application.getApplicantIdCardNo()));
        result.put("checkInDate", application.getCheckInDate());
        result.put("checkOutDate", application.getCheckOutDate());
        result.put("guestCount", application.getGuestCount());
        result.put("guestCount", guestCount(application));
        result.put("subsidy", application.getSubsidy());
        result.put("remark", application.getRejectReason());
        result.put("estimatedProcessingTime", application.getEstimatedProcessingTime());
@@ -444,6 +457,36 @@
        return first(application.getApplicantName(), application.getSpouseName(), application.getChildName(), application.getTalentName());
    }
    private Integer guestCount(ServiceApplication application) {
        if (application.getGuestCount() != null) {
            return application.getGuestCount();
        }
        return ServiceAppointmentApplicationService.TYPE_HOTEL.equals(application.getApplicationType()) ? 1 : null;
    }
    private String handlerName(ServiceApplication application) {
        if (application.getHandledAt() == null) {
            return first(application.getHandlerName());
        }
        ServiceApplicationOperationLog log = operationLogMapper.selectOne(
                new QueryWrapper<ServiceApplicationOperationLog>()
                        .eq("application_id", application.getId())
                        .eq("operation_type", "STATUS_UPDATE")
                        .eq("after_status", application.getStatus())
                        .orderByDesc("created_at", "id")
                        .last("limit 1")
        );
        return first(
                log == null ? "" : operatorName(log.getOperatorId()),
                log == null ? "" : log.getOperatorName(),
                application.getHandlerName()
        );
    }
    private String operatorName(Long operatorId) {
        return operatorId == null ? "" : first(operationLogMapper.selectOperatorName(operatorId));
    }
    private List<String> proofFileIds(String value) {
        if (value == null || value.isBlank()) {
            return List.of();
talent-card-server/src/main/java/com/talentcard/server/service/ServiceAppointmentApplicationService.java
@@ -157,7 +157,7 @@
        applyInstitution(application, merchant, request.merchantName(), request.contactMobile(), false);
        application.setApplicantName(first(clean(request.applicantName()), application.getTalentName()));
        application.setApplicantMobile(first(clean(request.applicantMobile()), application.getTalentMobile()));
        application.setGuestCount(request.guestCount());
        application.setGuestCount(request.guestCount() == null ? 1 : request.guestCount());
        application.setSubsidy(blankToNull(clean(request.subsidy())));
        applyHotelProduct(application, request.productId(), request.productName());
        LocalDate checkIn = date(request.checkInDate(), "入住日期格式不正确", false);