package com.mydreamjob.bidirectionalmatch; import com.mydreamjob.application.Application; import com.mydreamjob.application.ApplicationRepository; 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.company.VerifiedCompanyGuard; import com.mydreamjob.job.Job; import com.mydreamjob.job.JobService; import com.mydreamjob.messaging.Conversation; import com.mydreamjob.messaging.ConversationMember; import com.mydreamjob.messaging.ConversationMemberRepository; import com.mydreamjob.messaging.ConversationRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 匹配后沟通解锁:仅 MATCHED 的双方可基于 application 创建/获取直聊会话。 * *

幂等:application_match_conversations 唯一约束保证同一投递只解锁一个会话, * 重复调用返回同一会话。不修改通用 POST /conversations 的会话结构。 */ @Slf4j @Service @RequiredArgsConstructor public class MatchConversationService { private final ApplicationRepository applicationRepository; private final JobService jobService; private final CompanyRepository companyRepository; private final ApplicationMatchRecordRepository recordRepository; private final ApplicationMatchConversationRepository conversationLinkRepository; private final ConversationRepository conversationRepository; private final ConversationMemberRepository memberRepository; @Transactional public Map createOrGetMatchConversation(Long userId, Long applicationId) { Company ownedCompany = companyRepository.findByOwnerUserId(userId).orElse(null); if (ownedCompany != null) VerifiedCompanyGuard.requireVerified(ownedCompany); Application app = applicationRepository.findById(applicationId) .orElseThrow(() -> new BusinessException(ErrorCode.APPLICATION_NOT_FOUND)); Job job = jobService.getJob(app.getJobId()); Long companyId = job.getCompanyId(); Long candidateId = app.getStudentUserId(); // 参与者推导(身份一律来自 JWT):求职者 or 岗位所属企业 Long otherUserId; if (candidateId.equals(userId)) { otherUserId = companyRepository.findById(companyId) .map(Company::getOwnerUserId) .orElseThrow(() -> new BusinessException(ErrorCode.COMPANY_NOT_FOUND)); } else { Long actorCompanyId = ownedCompany == null ? null : ownedCompany.getId(); if (actorCompanyId == null || !actorCompanyId.equals(companyId)) { throw new BusinessException(ErrorCode.FORBIDDEN); } otherUserId = candidateId; } // 必须已 MATCHED if ("WITHDRAWN".equals(app.getStatus()) || "REJECTED".equals(app.getStatus())) { throw new BusinessException(ErrorCode.APPLICATION_NOT_MATCHED); } ApplicationMatchRecord record = recordRepository.findByApplicationId(applicationId).orElse(null); if (record == null || !ApplicationMatchRecord.STATUS_MATCHED.equals(record.getStatus())) { throw new BusinessException(ErrorCode.APPLICATION_NOT_MATCHED); } // 幂等:已有关联 → 返回既有会话 ApplicationMatchConversation link = conversationLinkRepository.findByApplicationId(applicationId).orElse(null); if (link != null) { return toView(conversationRepository.findById(link.getConversationId()) .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST))); } // 创建/复用两人之间的 DIRECT 会话(与 MessagingService 语义一致) Conversation conversation = findOrCreateDirectConversation(userId, otherUserId); try { conversationLinkRepository.save(ApplicationMatchConversation.builder() .applicationId(applicationId) .conversationId(conversation.getId()) .build()); } catch (DataIntegrityViolationException e) { // 并发:另一请求已建关联 → 返回既有会话 ApplicationMatchConversation existing = conversationLinkRepository .findByApplicationId(applicationId).orElse(null); if (existing != null) { return toView(conversationRepository.findById(existing.getConversationId()) .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST))); } throw e; } return toView(conversation); } private Conversation findOrCreateDirectConversation(Long userId, Long otherUserId) { List my = memberRepository.findByUserId(userId); for (ConversationMember m : my) { if (memberRepository.existsByConversationIdAndUserId(m.getConversationId(), otherUserId)) { Conversation existing = conversationRepository.findById(m.getConversationId()).orElse(null); if (existing != null && "DIRECT".equals(existing.getType())) return existing; } } Conversation c = conversationRepository.save(Conversation.builder().createdBy(userId).type("DIRECT").build()); memberRepository.save(ConversationMember.builder().conversationId(c.getId()).userId(userId).build()); memberRepository.save(ConversationMember.builder().conversationId(c.getId()).userId(otherUserId).build()); return c; } private Map toView(Conversation c) { Map v = new HashMap<>(); v.put("id", c.getId()); v.put("title", c.getTitle()); v.put("type", c.getType()); v.put("createdBy", c.getCreatedBy()); v.put("createdAt", c.getCreatedAt()); v.put("updatedAt", c.getUpdatedAt()); return v; } }