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.
132 lines
6.5 KiB
132 lines
6.5 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 和已录用,拒绝/撤回优先禁止。
|
|
*
|
|
* <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;
|
|
private final com.mydreamjob.safety.BlockRelationRepository blockRelationRepository;
|
|
|
|
@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.findLockedById(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(VerifiedCompanyGuard::requireVerified)
|
|
.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;
|
|
}
|
|
|
|
// 与撤回/状态推进共用当前读;不能仅根据匹配查询的 CLOSED 展示值判断沟通权限。
|
|
ApplicationMatchRecord record = recordRepository.findByApplicationId(applicationId).orElse(null);
|
|
if (!com.mydreamjob.application.ApplicationCommunicationPolicy.allows(app, record)) {
|
|
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 语义一致)
|
|
if (blockRelationRepository.existsByBlockerUserIdAndBlockedUserId(userId, otherUserId)
|
|
|| blockRelationRepository.existsByBlockerUserIdAndBlockedUserId(otherUserId, userId)) {
|
|
throw new BusinessException(ErrorCode.FORBIDDEN, "message cannot be sent");
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|