You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
12 KiB
262 lines
12 KiB
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<CompanyVerification> 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<Map<String, Object>> getHistory(Long verificationId) {
|
|
return historyRepository.findByVerificationIdOrderByCreatedAtAsc(verificationId)
|
|
.stream().map(this::toHistoryView).collect(Collectors.toList());
|
|
}
|
|
|
|
// ── 视图 ────────────────────────────────────────────────────
|
|
|
|
public Map<String, Object> toView(CompanyVerification v) {
|
|
Map<String, Object> 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<String, Object> toHistoryView(CompanyVerificationHistory h) {
|
|
Map<String, Object> 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());
|
|
}
|
|
}
|