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

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());
}
}