package com.mydreamjob.verification; import com.mydreamjob.audit.AuditContext; import com.mydreamjob.audit.AuditService; import com.mydreamjob.auth.User; import com.mydreamjob.auth.UserRepository; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyRepository; import com.mydreamjob.notification.NotificationService; import com.mydreamjob.verification.dto.VerificationDecisionRequest; import com.mydreamjob.verification.dto.VerificationSubmitRequest; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import com.mydreamjob.security.UserRoleGuard; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Slf4j @Service @RequiredArgsConstructor public class CompanyVerificationService { private static final int MAX_PAGE_SIZE = 100; private final com.mydreamjob.security.UserRoleGuard userRoleGuard; private final CompanyVerificationRepository verificationRepository; private final CompanyVerificationHistoryRepository historyRepository; private final CompanyRepository companyRepository; private final UserRepository userRepository; private final NotificationService notificationService; private final AuditService auditService; @PersistenceContext private EntityManager entityManager; // ── 企业提交认证 ──────────────────────────────────────────── @Transactional public CompanyVerification submit(Long companyUserId, VerificationSubmitRequest req, AuditContext ctx) { Company company = requireCompany(companyUserId); // 检查是否已有审批中的认证 CompanyVerification existing = verificationRepository .findTopByCompanyIdOrderByIdDesc(company.getId()).orElse(null); if (existing != null && (existing.getStatus().equals("PENDING") || existing.getStatus().equals("RESUBMITTED"))) { throw new BusinessException(ErrorCode.VERIFICATION_ALREADY_PENDING); } if (existing != null && existing.getStatus().equals("APPROVED")) { throw new BusinessException(ErrorCode.VERIFICATION_ALREADY_APPROVED); } String targetStatus = (existing != null && existing.getStatus().equals("REJECTED")) ? VerificationStatus.RESUBMITTED.name() : VerificationStatus.PENDING.name(); CompanyVerification verification = CompanyVerification.builder() .companyId(company.getId()) .status(targetStatus) .submittedBy(companyUserId) .materialsJson(req.getMaterialsJson()) .submittedAt(LocalDateTime.now()) .build(); verification = verificationRepository.save(verification); recordHistory(verification.getId(), null, targetStatus, companyUserId, "企业提交认证材料"); auditService.log(companyUserId, "SUBMIT_VERIFICATION", "VERIFICATION", verification.getId(), "Company verification submitted", ctx); return verification; } // ── 企业查询自己的认证状态 ─────────────────────────────────── public CompanyVerification getMyVerification(Long companyUserId) { Company company = requireCompany(companyUserId); return verificationRepository.findTopByCompanyIdOrderByIdDesc(company.getId()) .orElseThrow(() -> new BusinessException(ErrorCode.VERIFICATION_NOT_FOUND)); } // ── 管理员:待审核列表 ──────────────────────────────────────── public Page listPending(String status, LocalDateTime from, LocalDateTime to, int page, int size) { if (size > MAX_PAGE_SIZE) size = MAX_PAGE_SIZE; PageRequest pr = PageRequest.of(page, size); boolean hasStatus = status != null && !status.isBlank(); boolean hasRange = from != null && to != null; if (hasStatus && hasRange) return verificationRepository.findByStatusAndSubmittedAtBetweenOrderByCreatedAtDesc( status, from, to, pr); if (hasStatus) return verificationRepository.findByStatusOrderByCreatedAtDesc(status, pr); if (hasRange) return verificationRepository.findBySubmittedAtBetweenOrderByCreatedAtDesc(from, to, pr); return verificationRepository.findAllByOrderByCreatedAtDesc(pr); } // ── 管理员:查看认证详情 ────────────────────────────────────── public CompanyVerification getDetail(Long verificationId) { return verificationRepository.findById(verificationId) .orElseThrow(() -> new BusinessException(ErrorCode.VERIFICATION_NOT_FOUND)); } // ── 管理员:审核决定 ────────────────────────────────────────── @Transactional public CompanyVerification decide(Long adminUserId, Long verificationId, VerificationDecisionRequest req, AuditContext ctx) { CompanyVerification verification = verificationRepository.findById(verificationId) .orElseThrow(() -> new BusinessException(ErrorCode.VERIFICATION_NOT_FOUND)); String decision = req.getDecision().toUpperCase(); VerificationStatus targetStatus; try { targetStatus = VerificationStatus.valueOf(decision); } catch (IllegalArgumentException e) { throw new BusinessException(ErrorCode.BAD_REQUEST, "invalid decision: " + decision); } if (targetStatus != VerificationStatus.APPROVED && targetStatus != VerificationStatus.REJECTED) { throw new BusinessException(ErrorCode.BAD_REQUEST, "decision must be APPROVED or REJECTED"); } VerificationStatus current = VerificationStatus.valueOf(verification.getStatus()); if (current == VerificationStatus.APPROVED) { throw new BusinessException(ErrorCode.VERIFICATION_ALREADY_APPROVED); } // 幂等:已相同状态无需重复 if (current == targetStatus) { return verification; } if (current != VerificationStatus.PENDING && current != VerificationStatus.RESUBMITTED) { throw new BusinessException(ErrorCode.INVALID_VERIFICATION_STATUS_TRANSITION); } if (targetStatus == VerificationStatus.REJECTED && (req.getReason() == null || req.getReason().isBlank())) { throw new BusinessException(ErrorCode.VERIFICATION_REJECT_REASON_REQUIRED); } String fromStatus = verification.getStatus(); try { verification.setStatus(targetStatus.name()); verification.setReviewerId(adminUserId); verification.setDecisionReason(req.getReason()); verification.setDecidedAt(LocalDateTime.now()); CompanyVerification saved = verificationRepository.saveAndFlush(verification); recordHistory(saved.getId(), fromStatus, targetStatus.name(), adminUserId, req.getReason()); // 更新企业认证状态 if (targetStatus == VerificationStatus.APPROVED) { Company company = companyRepository.findById(saved.getCompanyId()) .orElse(null); if (company != null && !Boolean.TRUE.equals(company.getVerified())) { company.setVerified(true); companyRepository.save(company); } } auditService.log(adminUserId, "DECIDE_VERIFICATION", "VERIFICATION", saved.getId(), "Verification " + targetStatus.name() + " for company " + saved.getCompanyId(), ctx); notificationService.publishEvent(saved.getSubmittedBy(), "VERIFICATION_" + targetStatus.name(), "企业认证审核结果", "你的企业认证申请已被" + (targetStatus == VerificationStatus.APPROVED ? "通过" : "拒绝") + (req.getReason() != null ? ":" + req.getReason() : ""), "VERIFICATION", saved.getId(), "verification-decision-" + saved.getId()); return saved; } catch (OptimisticLockingFailureException e) { entityManager.clear(); CompanyVerification fresh = verificationRepository.findById(verificationId) .orElseThrow(() -> new BusinessException(ErrorCode.VERIFICATION_NOT_FOUND)); if (fresh.getStatus().equals(targetStatus.name())) return fresh; throw new BusinessException(ErrorCode.INVALID_VERIFICATION_STATUS_TRANSITION, "verification was modified concurrently"); } } // ── 管理员:审核历史 ────────────────────────────────────────── public List> getHistory(Long verificationId) { return historyRepository.findByVerificationIdOrderByCreatedAtAsc(verificationId) .stream().map(this::toHistoryView).collect(Collectors.toList()); } // ── 视图 ──────────────────────────────────────────────────── public Map toView(CompanyVerification v) { Map view = new HashMap<>(); view.put("id", v.getId()); view.put("companyId", v.getCompanyId()); view.put("status", v.getStatus()); view.put("submittedBy", v.getSubmittedBy()); view.put("materialsJson", v.getMaterialsJson()); view.put("reviewerId", v.getReviewerId()); view.put("decisionReason", v.getDecisionReason()); view.put("submittedAt", v.getSubmittedAt()); view.put("decidedAt", v.getDecidedAt()); view.put("version", v.getVersion()); view.put("createdAt", v.getCreatedAt()); return view; } private Map toHistoryView(CompanyVerificationHistory h) { Map v = new HashMap<>(); v.put("id", h.getId()); v.put("fromStatus", h.getFromStatus()); v.put("toStatus", h.getToStatus()); v.put("actorUserId", h.getActorUserId()); v.put("reason", h.getReason()); v.put("createdAt", h.getCreatedAt()); return v; } // ── 私有辅助 ──────────────────────────────────────────────── private Company requireCompany(Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); if (!hasRole(user, User.Role.COMPANY)) { throw new BusinessException(ErrorCode.FORBIDDEN); } return companyRepository.findByOwnerUserId(userId) .orElseThrow(() -> new BusinessException(ErrorCode.COMPANY_NOT_FOUND)); } private boolean hasRole(User user, User.Role role) { return user.getRoles() != null && user.getRoles().stream().anyMatch(r -> r == role); } public void assertAdminRole(Long userId) { userRoleGuard.requireAdmin(userId); } private void recordHistory(Long verificationId, String fromStatus, String toStatus, Long actorUserId, String reason) { historyRepository.save(CompanyVerificationHistory.builder() .verificationId(verificationId) .fromStatus(fromStatus) .toStatus(toStatus) .actorUserId(actorUserId) .reason(reason) .build()); } }