InquiryAiReplyService.java
package com.hwhub.batch.application.service;
import com.hwhub.batch.domain.ai.AiClient;
import com.hwhub.batch.domain.enums.InquiryStatus;
import com.hwhub.batch.domain.enums.NotificationLinkType;
import com.hwhub.batch.domain.enums.NotificationType;
import com.hwhub.batch.domain.enums.ProgramType;
import com.hwhub.batch.domain.enums.SenderType;
import com.hwhub.batch.domain.model.inquiry.InquiryMessageModel;
import com.hwhub.batch.domain.model.inquiry.InquiryModel;
import com.hwhub.batch.domain.model.notification.NotificationLink;
import com.hwhub.batch.domain.model.notification.NotificationMessage;
import com.hwhub.batch.domain.model.notification.NotificationModel;
import com.hwhub.batch.domain.repository.InquiryRepository;
import com.hwhub.batch.domain.repository.NotificationRepository;
import com.hwhub.batch.infrastructure.s3.KnowledgeLoader;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class InquiryAiReplyService {
private final InquiryRepository inquiryRepository;
private final NotificationRepository notificationRepository;
private final AiClient aiClient;
private final KnowledgeLoader knowledgeLoader;
@Value("${hwhub.ai.inquiry-reply.batch-size:100}")
private int batchSize;
private static final long SYSTEM_USER_ID = 1L;
/**
* OPEN な問い合わせに AI 返信する。 1件でも API 失敗したら後続は停止。 それまでの成功件数・失敗件数をログ出力。
*
* @return 処理結果サマリー
*/
@Transactional
public ProcessResult processAll() {
List<InquiryModel> targets = inquiryRepository.findOpenList(batchSize);
if (targets.isEmpty()) {
log.info("No open inquiries to process.");
return new ProcessResult(0, 0, batchSize);
}
log.info("Starting AI reply process. target={}", targets.size());
// ナレッジは全件共通で1回だけ読み込む
String knowledge = knowledgeLoader.load();
int successCount = 0;
int failCount = 0;
for (InquiryModel inquiry : targets) {
try {
String answer = aiClient.ask(inquiry, knowledge);
int nextSeq = inquiry.getMessages().size() + 1;
InquiryMessageModel message =
InquiryMessageModel.newMessage(
inquiry.getInquiryId(), nextSeq, SenderType.AI_SUPPORT, answer);
inquiryRepository.insertMessage(message, SYSTEM_USER_ID, ProgramType.BTC_INQ_AI.getCode());
inquiryRepository.updateStatus(
inquiry.getInquiryId().value(),
InquiryStatus.AI_ANSWERED.getCode(),
SYSTEM_USER_ID,
ProgramType.BTC_INQ_AI.getCode());
publishReplyNotification(inquiry);
successCount++;
log.info(
"AI reply succeeded. inquiryId={} seq={}", inquiry.getInquiryId().value(), nextSeq);
} catch (Exception e) {
failCount = targets.size() - successCount;
log.error(
"AI reply failed. Stopping subsequent processing. inquiryId={} success={} fail={} (including remaining)",
inquiry.getInquiryId().value(),
successCount,
failCount,
e);
break;
}
}
ProcessResult result = new ProcessResult(successCount, failCount, batchSize);
log.info(
"InquiryAiReplyJob finished. success={} fail={} batchSize={}",
result.successCount(),
result.failCount(),
result.batchSize());
return result;
}
public record ProcessResult(int successCount, int failCount, int batchSize) {}
private void publishReplyNotification(InquiryModel inquiry) {
Map<String, Object> params = new HashMap<>();
params.put("inquiryId", inquiry.getInquiryId().value());
params.put("title", inquiry.getTitle());
NotificationMessage message =
new NotificationMessage(
"notifications.messages.inquiryReplied.title",
"notifications.messages.inquiryReplied.body",
params);
NotificationLink link =
new NotificationLink(NotificationLinkType.INQUIRY_DETAIL, inquiry.getInquiryId().value());
NotificationModel model =
NotificationModel.newUnread(
null,
NotificationType.YOUR_INQUIRY_HAS_BEEN_REPLIED,
null,
inquiry.getUserId(),
message,
link,
LocalDateTime.now());
notificationRepository.insert(model, SYSTEM_USER_ID, ProgramType.BTC_INQ_AI.getCode());
}
}