package com.talentcard.server.service;
|
|
import java.io.ByteArrayOutputStream;
|
import java.io.IOException;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeParseException;
|
import java.util.ArrayList;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.talentcard.server.business.PageResult;
|
import com.talentcard.server.request.ServiceAppointmentStatusUpdateRequest;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import com.talentcard.server.common.SensitiveDataCrypto;
|
import com.talentcard.server.entity.ChildInformation;
|
import com.talentcard.server.entity.ServiceApplication;
|
import com.talentcard.server.entity.ServiceApplicationOperationLog;
|
import com.talentcard.server.mapper.ChildInformationMapper;
|
import com.talentcard.server.mapper.ServiceApplicationMapper;
|
import com.talentcard.server.mapper.ServiceApplicationOperationLogMapper;
|
import org.apache.poi.ss.usermodel.CellStyle;
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
import org.apache.poi.ss.usermodel.IndexedColors;
|
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
@Service
|
public class ServiceAppointmentAdminService {
|
private static final List<String> PENDING_STATUSES = List.of("待处理", "待审核");
|
private static final List<String> REJECTED_STATUSES = List.of("已驳回", "审核不通过", "已取消");
|
|
private final ServiceApplicationMapper applicationMapper;
|
private final ChildInformationMapper childInformationMapper;
|
private final ServiceApplicationOperationLogMapper operationLogMapper;
|
private final SensitiveDataCrypto sensitiveDataCrypto;
|
|
public ServiceAppointmentAdminService(
|
ServiceApplicationMapper applicationMapper,
|
ChildInformationMapper childInformationMapper,
|
ServiceApplicationOperationLogMapper operationLogMapper,
|
SensitiveDataCrypto sensitiveDataCrypto
|
) {
|
this.applicationMapper = applicationMapper;
|
this.childInformationMapper = childInformationMapper;
|
this.operationLogMapper = operationLogMapper;
|
this.sensitiveDataCrypto = sensitiveDataCrypto;
|
}
|
|
@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 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);
|
Long total = applicationMapper.selectCount(countWrapper);
|
QueryWrapper<ServiceApplication> pageWrapper = filter(applicationType, keyword, applicantName, applicantMobile, serviceKeyword, cardLevel, status, submittedStart, submittedEnd)
|
.orderByDesc("submitted_at", "id")
|
.last("limit " + safeSize + " offset " + ((safePage - 1) * safeSize));
|
List<Map<String, Object>> records = applicationMapper.selectList(pageWrapper).stream()
|
.map(this::listView)
|
.toList();
|
return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> detail(Long id) {
|
ServiceApplication application = application(id);
|
return detailView(application);
|
}
|
|
@Transactional(readOnly = true)
|
public List<Map<String, Object>> operationLogs(Long id) {
|
application(id);
|
return operationLogMapper.selectList(new QueryWrapper<ServiceApplicationOperationLog>()
|
.eq("application_id", id)
|
.orderByAsc("created_at", "id"))
|
.stream().map(this::operationLogView).toList();
|
}
|
|
|
@Transactional
|
public Map<String, Object> updateStatus(Long id, ServiceAppointmentStatusUpdateRequest request, Long operatorId) {
|
ServiceApplication current = applicationMapper.selectByIdForUpdate(id);
|
if (current == null) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "预约申请不存在");
|
}
|
String nextStatus = normalizeStatus(request.status(), current.getApplicationType());
|
String remark = first(request.remark(), request.rejectReason());
|
if (REJECTED_STATUSES.contains(nextStatus) && remark.isBlank()) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "拒绝或驳回时必须填写处理备注");
|
}
|
String handlerName = first(request.handlerName(), operatorId == null ? "" : "管理员" + operatorId, "管理员");
|
LocalDateTime handledAt = PENDING_STATUSES.contains(nextStatus) ? null : LocalDateTime.now();
|
UpdateWrapper<ServiceApplication> update = new UpdateWrapper<ServiceApplication>()
|
.set("status", nextStatus)
|
.set("handled_at", handledAt)
|
.set("handler_name", handledAt == null ? null : handlerName)
|
.set("reject_reason", remark.isBlank() ? null : remark)
|
.eq("id", id)
|
.eq("deleted", 0);
|
int updated = applicationMapper.update(null, update);
|
if (updated != 1) {
|
throw new BusinessException(ErrorCode.CONFLICT, "预约申请状态已变化,请刷新后重试");
|
}
|
ServiceApplicationOperationLog log = new ServiceApplicationOperationLog();
|
log.setApplicationId(current.getId());
|
log.setApplicationNo(current.getApplicationNo());
|
log.setOperationType("STATUS_UPDATE");
|
log.setBeforeStatus(current.getStatus());
|
log.setAfterStatus(nextStatus);
|
log.setOperatorType("ADMIN");
|
log.setOperatorId(operatorId);
|
log.setOperatorName(handlerName);
|
log.setRemark(remark.isBlank() ? null : remark);
|
log.setCreatedAt(LocalDateTime.now());
|
if (operationLogMapper.insert(log) != 1) {
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "预约申请操作日志保存失败");
|
}
|
syncChildAudit(current, nextStatus, remark);
|
return detail(id);
|
}
|
|
@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)
|
.orderByDesc("submitted_at", "id");
|
List<ServiceApplication> applications = applicationMapper.selectList(wrapper);
|
String[] headers = {
|
"序号", "申请编号", "预约类型", "状态", "持卡人", "持卡人手机号", "人才卡等级", "人才卡号",
|
"服务机构", "联系电话", "预约内容", "预约时间", "申请人/配偶/子女", "申请人手机号",
|
"证件号", "子女性别", "子女出生日期", "处理人", "处理时间", "处理备注", "提交时间"
|
};
|
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
Sheet sheet = workbook.createSheet("预约申请");
|
sheet.createFreezePane(0, 1);
|
CellStyle headerStyle = headerStyle(workbook);
|
Row headerRow = sheet.createRow(0);
|
for (int index = 0; index < headers.length; index++) {
|
headerRow.createCell(index).setCellValue(headers[index]);
|
headerRow.getCell(index).setCellStyle(headerStyle);
|
}
|
int rowIndex = 1;
|
for (ServiceApplication application : applications) {
|
Row row = sheet.createRow(rowIndex);
|
List<String> values = exportValues(rowIndex, application);
|
for (int index = 0; index < values.size(); index++) {
|
row.createCell(index).setCellValue(values.get(index));
|
}
|
rowIndex++;
|
}
|
int[] widths = { 8, 24, 18, 14, 16, 18, 16, 20, 28, 18, 24, 28, 20, 18, 24, 12, 16, 16, 22, 30, 22 };
|
for (int index = 0; index < widths.length; index++) {
|
sheet.setColumnWidth(index, widths[index] * 256);
|
}
|
workbook.write(output);
|
return output.toByteArray();
|
} catch (IOException exception) {
|
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "预约申请导出失败");
|
}
|
}
|
|
private QueryWrapper<ServiceApplication> filter(String applicationType, String keyword, String applicantName,
|
String applicantMobile, String serviceKeyword,
|
String cardLevel, String status, String submittedStart, String submittedEnd) {
|
QueryWrapper<ServiceApplication> wrapper = new QueryWrapper<ServiceApplication>().eq("deleted", 0);
|
String type = normalizeFilterType(applicationType);
|
if (!type.isBlank()) {
|
wrapper.eq("application_type", type);
|
}
|
String key = clean(keyword);
|
if (!key.isBlank()) {
|
String like = "%" + key + "%";
|
wrapper.and(query -> query.like("application_no", like)
|
.or().like("talent_name", like)
|
.or().like("talent_mobile", like)
|
.or().like("card_no", like)
|
.or().like("applicant_name", like)
|
.or().like("applicant_mobile", like)
|
.or().like("spouse_name", like)
|
.or().like("child_name", like));
|
}
|
String name = clean(applicantName);
|
if (!name.isBlank()) {
|
wrapper.and(query -> query.like("talent_name", name)
|
.or().like("applicant_name", name)
|
.or().like("spouse_name", name)
|
.or().like("child_name", name));
|
}
|
String mobile = clean(applicantMobile);
|
if (!mobile.isBlank()) {
|
wrapper.and(query -> query.like("talent_mobile", mobile)
|
.or().like("applicant_mobile", mobile)
|
.or().like("spouse_mobile", mobile)
|
.or().like("contact_mobile", mobile));
|
}
|
String service = clean(serviceKeyword);
|
if (!service.isBlank()) {
|
String like = "%" + service + "%";
|
wrapper.and(query -> query.like("merchant_name", like)
|
.or().like("hospital_name", like)
|
.or().like("package_name", like)
|
.or().like("product_name", like));
|
}
|
if (!clean(cardLevel).isBlank()) {
|
wrapper.eq("talent_level", clean(cardLevel));
|
}
|
if (!clean(status).isBlank()) {
|
wrapper.eq("status", clean(status));
|
}
|
LocalDate start = localDate(submittedStart, "提交开始日期格式不正确");
|
LocalDate end = localDate(submittedEnd, "提交结束日期格式不正确");
|
if (start != null) {
|
wrapper.ge("submitted_at", start.atStartOfDay());
|
}
|
if (end != null) {
|
wrapper.lt("submitted_at", end.plusDays(1).atStartOfDay());
|
}
|
return wrapper;
|
}
|
|
private Map<String, Object> listView(ServiceApplication application) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("id", application.getId());
|
result.put("applicationNo", application.getApplicationNo());
|
result.put("applicationType", application.getApplicationType());
|
result.put("applicationTypeName", typeName(application.getApplicationType()));
|
result.put("status", application.getStatus());
|
result.put("talentName", application.getTalentName());
|
result.put("talentMobile", application.getTalentMobile());
|
result.put("talentLevel", application.getTalentLevel());
|
result.put("cardNo", application.getCardNo());
|
result.put("serviceName", serviceName(application));
|
result.put("appointmentContent", appointmentContent(application));
|
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("submittedAt", application.getSubmittedAt());
|
result.put("handledAt", application.getHandledAt());
|
result.put("handlerName", application.getHandlerName());
|
return result;
|
}
|
|
private Map<String, Object> detailView(ServiceApplication application) {
|
Map<String, Object> result = listView(application);
|
result.put("talentId", application.getTalentId());
|
result.put("cardId", application.getCardId());
|
result.put("merchantId", application.getMerchantId());
|
result.put("contactMobile", application.getContactMobile());
|
result.put("packageName", application.getPackageName());
|
result.put("productName", application.getProductName());
|
result.put("applicantMobile", sensitiveDataCrypto.decrypt(application.getApplicantMobile()));
|
result.put("applicantIdCardNo", sensitiveDataCrypto.decrypt(application.getApplicantIdCardNo()));
|
result.put("checkInDate", application.getCheckInDate());
|
result.put("checkOutDate", application.getCheckOutDate());
|
result.put("guestCount", application.getGuestCount());
|
result.put("subsidy", application.getSubsidy());
|
result.put("remark", application.getRejectReason());
|
result.put("estimatedProcessingTime", application.getEstimatedProcessingTime());
|
result.put("institutionAddress", application.getInstitutionAddressSnapshot());
|
result.put("institutionLongitude", application.getInstitutionLongitudeSnapshot());
|
result.put("institutionLatitude", application.getInstitutionLatitudeSnapshot());
|
Map<String, Object> spouse = new LinkedHashMap<>();
|
spouse.put("name", first(application.getSpouseName()));
|
spouse.put("mobile", sensitiveDataCrypto.decrypt(application.getSpouseMobile()));
|
spouse.put("idCardNo", sensitiveDataCrypto.decrypt(application.getSpouseIdCardNo()));
|
spouse.put("politicalStatus", first(application.getSpousePoliticalStatus()));
|
spouse.put("education", first(application.getSpouseEducation()));
|
spouse.put("schoolMajor", first(application.getSpouseSchoolMajor()));
|
spouse.put("workUnit", first(application.getSpouseWorkUnit()));
|
spouse.put("inInstitution", first(application.getSpouseInInstitution()));
|
spouse.put("workExperience", first(application.getSpouseWorkExperience()));
|
result.put("spouse", spouse);
|
|
Map<String, Object> child = new LinkedHashMap<>();
|
child.put("id", application.getChildId());
|
child.put("name", first(application.getChildName()));
|
child.put("gender", first(application.getChildGender()));
|
child.put("birthDate", application.getChildBirthDate());
|
child.put("proofFileIds", proofFileIds(application.getProofFileIds()));
|
result.put("child", child);
|
return result;
|
}
|
|
private Map<String, Object> operationLogView(ServiceApplicationOperationLog log) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("id", log.getId());
|
result.put("applicationId", log.getApplicationId());
|
result.put("applicationNo", log.getApplicationNo());
|
result.put("operationType", log.getOperationType());
|
result.put("operationTypeText", "SUBMIT".equals(log.getOperationType()) ? "提交申请" : "状态更新");
|
result.put("beforeStatus", log.getBeforeStatus());
|
result.put("afterStatus", log.getAfterStatus());
|
result.put("operatorType", log.getOperatorType());
|
result.put("operatorId", log.getOperatorId());
|
result.put("operatorName", log.getOperatorName());
|
result.put("remark", log.getRemark());
|
result.put("createdAt", log.getCreatedAt());
|
return result;
|
}
|
|
|
private List<String> exportValues(int index, ServiceApplication application) {
|
return List.of(
|
String.valueOf(index),
|
first(application.getApplicationNo()),
|
typeName(application.getApplicationType()),
|
first(application.getStatus()),
|
first(application.getTalentName()),
|
first(application.getTalentMobile()),
|
first(application.getTalentLevel()),
|
first(application.getCardNo()),
|
serviceName(application),
|
first(application.getContactMobile()),
|
appointmentContent(application),
|
first(application.getExpectedTime()),
|
participantName(application),
|
sensitiveDataCrypto.decrypt(application.getApplicantMobile()),
|
sensitiveDataCrypto.decrypt(first(application.getApplicantIdCardNo(), application.getSpouseIdCardNo())),
|
first(application.getChildGender()),
|
application.getChildBirthDate() == null ? "" : application.getChildBirthDate().toString(),
|
first(application.getHandlerName()),
|
application.getHandledAt() == null ? "" : application.getHandledAt().toString(),
|
first(application.getRejectReason()),
|
application.getSubmittedAt() == null ? "" : application.getSubmittedAt().toString()
|
);
|
}
|
|
private void syncChildAudit(ServiceApplication application, String status, String remark) {
|
if (!ServiceAppointmentApplicationService.TYPE_CHILD.equals(application.getApplicationType()) || application.getChildId() == null) {
|
return;
|
}
|
String auditStatus = switch (status) {
|
case "审核通过", "已处理" -> "已通过";
|
case "审核不通过", "已驳回", "已取消" -> "审核不通过";
|
case "待审核", "待处理" -> "待审核";
|
default -> null;
|
};
|
if (auditStatus == null) {
|
return;
|
}
|
childInformationMapper.update(null, new UpdateWrapper<ChildInformation>()
|
.set("audit_status", auditStatus)
|
.set("reject_reason", "审核不通过".equals(auditStatus) ? remark : null)
|
.eq("id", application.getChildId())
|
.eq("talent_id", application.getTalentId())
|
.eq("deleted", 0));
|
}
|
|
private ServiceApplication application(Long id) {
|
if (id == null || id <= 0) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, "预约申请ID不能为空");
|
}
|
ServiceApplication application = applicationMapper.selectOne(new QueryWrapper<ServiceApplication>()
|
.eq("id", id)
|
.eq("deleted", 0)
|
.last("limit 1"));
|
if (application == null) {
|
throw new BusinessException(ErrorCode.NOT_FOUND, "预约申请不存在");
|
}
|
return application;
|
}
|
|
private String normalizeStatus(String raw, String applicationType) {
|
String status = clean(raw);
|
boolean child = ServiceAppointmentApplicationService.TYPE_CHILD.equals(applicationType);
|
return switch (status) {
|
case "待处理", "处理中", "已处理", "已驳回", "已取消", "待审核", "审核通过", "审核不通过" -> status;
|
case "通过", "已通过", "完成", "处理完成" -> child ? "审核通过" : "已处理";
|
case "拒绝", "驳回", "不通过", "审核拒绝" -> child ? "审核不通过" : "已驳回";
|
default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "处理状态不正确");
|
};
|
}
|
|
private String normalizeFilterType(String raw) {
|
String value = clean(raw).toUpperCase(Locale.ROOT).replace('-', '_');
|
if (value.isBlank()) {
|
return "";
|
}
|
return switch (value) {
|
case "MEDICAL", "MEDICAL_CHECKUP", "MEDICAL_RESERVATION", "PHYSICAL_EXAM", "HEALTH_CHECK" -> ServiceAppointmentApplicationService.TYPE_MEDICAL;
|
case "FAST_TRACK", "QUICK_CHANNEL", "GREEN_CHANNEL", "MEDICAL_APPLICATION", "HOSPITAL", "HOSPITAL_APPOINTMENT" -> ServiceAppointmentApplicationService.TYPE_FAST_TRACK;
|
case "HOTEL", "HOTEL_RESERVATION", "HOTEL_BOOKING" -> ServiceAppointmentApplicationService.TYPE_HOTEL;
|
case "SPOUSE", "SPOUSE_EMPLOYMENT", "EMPLOYMENT" -> ServiceAppointmentApplicationService.TYPE_SPOUSE;
|
case "CHILD", "CHILDREN", "CHILD_ENTRUSTMENT", "CHILDREN_ENTRUSTMENT", "STUDENT_CARE", "STUDENT_ENTRUSTMENT" -> ServiceAppointmentApplicationService.TYPE_CHILD;
|
default -> raw;
|
};
|
}
|
|
private String typeName(String type) {
|
return switch (type) {
|
case ServiceAppointmentApplicationService.TYPE_MEDICAL -> "体检预约";
|
case ServiceAppointmentApplicationService.TYPE_FAST_TRACK -> "医院快捷通道";
|
case ServiceAppointmentApplicationService.TYPE_HOTEL -> "酒店预约";
|
case ServiceAppointmentApplicationService.TYPE_SPOUSE -> "配偶推荐就业";
|
case ServiceAppointmentApplicationService.TYPE_CHILD -> "学生托管";
|
default -> "预约申请";
|
};
|
}
|
|
private String serviceName(ServiceApplication application) {
|
return first(application.getHospitalName(), application.getMerchantName(), application.getParkingLot(), "人才服务机构");
|
}
|
|
private String appointmentContent(ServiceApplication application) {
|
return first(application.getProductName(), application.getPackageName(), typeName(application.getApplicationType()));
|
}
|
|
private String participantName(ServiceApplication application) {
|
return first(application.getApplicantName(), application.getSpouseName(), application.getChildName(), application.getTalentName());
|
}
|
|
private List<String> proofFileIds(String value) {
|
if (value == null || value.isBlank()) {
|
return List.of();
|
}
|
List<String> result = new ArrayList<>();
|
for (String item : value.split(",")) {
|
String cleaned = clean(item);
|
if (!cleaned.isBlank()) {
|
result.add(cleaned);
|
}
|
}
|
return result;
|
}
|
|
private LocalDate localDate(String value, String message) {
|
String text = clean(value);
|
if (text.isBlank()) {
|
return null;
|
}
|
try {
|
return LocalDate.parse(text);
|
} catch (DateTimeParseException exception) {
|
throw new BusinessException(ErrorCode.BAD_REQUEST, message);
|
}
|
}
|
|
private CellStyle headerStyle(Workbook workbook) {
|
CellStyle style = workbook.createCellStyle();
|
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
|
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
return style;
|
}
|
|
private String first(String... values) {
|
for (String value : values) {
|
String text = clean(value);
|
if (!text.isBlank()) {
|
return text;
|
}
|
}
|
return "";
|
}
|
|
private String clean(String value) {
|
return value == null ? "" : value.trim();
|
}
|
}
|