轻松聘AI开发
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.
 
 
 

129 lines
6.1 KiB

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 创建/获取直聊会话。
*
* <p>幂等: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<String, Object> 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<ConversationMember> 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<String, Object> toView(Conversation c) {
Map<String, Object> 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;
}
}