|
@@ -1,23 +1,24 @@
|
|
|
package com.etotem.cfc.service.impl;
|
|
package com.etotem.cfc.service.impl;
|
|
|
|
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.etotem.cfc.entity.SurveyRecord;
|
|
import com.etotem.cfc.entity.SurveyRecord;
|
|
|
import com.etotem.cfc.entity.SurveyTemplate;
|
|
import com.etotem.cfc.entity.SurveyTemplate;
|
|
|
import com.etotem.cfc.entity.FamilyMember;
|
|
import com.etotem.cfc.entity.FamilyMember;
|
|
|
import com.etotem.cfc.mapper.SurveyRecordMapper;
|
|
import com.etotem.cfc.mapper.SurveyRecordMapper;
|
|
|
import com.etotem.cfc.mapper.SurveyTemplateMapper;
|
|
import com.etotem.cfc.mapper.SurveyTemplateMapper;
|
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
|
|
+import com.etotem.cfc.service.AiGateway;
|
|
|
import com.etotem.cfc.service.SurveyService;
|
|
import com.etotem.cfc.service.SurveyService;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
import org.slf4j.LoggerFactory;
|
|
|
-import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
-import org.springframework.http.HttpEntity;
|
|
|
|
|
-import org.springframework.http.HttpHeaders;
|
|
|
|
|
-import org.springframework.http.MediaType;
|
|
|
|
|
-import org.springframework.http.ResponseEntity;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
-import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
|
|
+import java.time.DayOfWeek;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.LocalTime;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
@@ -38,8 +39,8 @@ public class SurveyServiceImpl implements SurveyService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private FamilyMemberMapper familyMemberMapper;
|
|
private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
|
|
|
- @Value("${langgraph.base-url:}")
|
|
|
|
|
- private String langgraphBaseUrl;
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiGateway aiGateway;
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public SurveyTemplate saveTemplate(SurveyTemplate template, Long adminId) {
|
|
public SurveyTemplate saveTemplate(SurveyTemplate template, Long adminId) {
|
|
@@ -94,40 +95,55 @@ public class SurveyServiceImpl implements SurveyService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询调研待办状态(按周期计算)
|
|
|
|
|
+ * 每个启用模板返回当前周期的完成状态:
|
|
|
|
|
+ * - "done": 该成员在当前周期内已有提交记录
|
|
|
|
|
+ * - "pending": 当前周期内尚未提交
|
|
|
|
|
+ * 周期由服务端按模板 cycle_type 统一计算(见 computeCurrentPeriod)
|
|
|
|
|
+ */
|
|
|
@Override
|
|
@Override
|
|
|
public Object getSurveyStatus(Long userId, Long memberId) {
|
|
public Object getSurveyStatus(Long userId, Long memberId) {
|
|
|
memberId = resolveMemberId(userId, memberId);
|
|
memberId = resolveMemberId(userId, memberId);
|
|
|
|
|
+ List<Map<String, Object>> statusList = new ArrayList<>();
|
|
|
|
|
+ if (memberId == null) return statusList;
|
|
|
List<SurveyTemplate> activeTemplates = getActiveTemplates();
|
|
List<SurveyTemplate> activeTemplates = getActiveTemplates();
|
|
|
- List<SurveyRecord> allRecords = surveyRecordMapper.selectList(null);
|
|
|
|
|
|
|
+ if (activeTemplates == null || activeTemplates.isEmpty()) return statusList;
|
|
|
|
|
+ // 仅查询该成员的调研记录(按 memberId 过滤,避免全表扫描)
|
|
|
|
|
+ List<SurveyRecord> memberRecords = surveyRecordMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<SurveyRecord>().eq(SurveyRecord::getMemberId, memberId));
|
|
|
Map<Long, List<SurveyRecord>> recordsByTemplate = new HashMap<>();
|
|
Map<Long, List<SurveyRecord>> recordsByTemplate = new HashMap<>();
|
|
|
- for (SurveyRecord r : allRecords) {
|
|
|
|
|
- if (r.getMemberId() != null && r.getMemberId().equals(memberId)) {
|
|
|
|
|
- Long tid = r.getTemplateId();
|
|
|
|
|
- if (tid != null) {
|
|
|
|
|
- recordsByTemplate.computeIfAbsent(tid, k -> new ArrayList<>()).add(r);
|
|
|
|
|
|
|
+ if (memberRecords != null) {
|
|
|
|
|
+ for (SurveyRecord r : memberRecords) {
|
|
|
|
|
+ if (r.getTemplateId() != null) {
|
|
|
|
|
+ recordsByTemplate.computeIfAbsent(r.getTemplateId(), k -> new ArrayList<>()).add(r);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- List<Map<String, Object>> statusList = new ArrayList<>();
|
|
|
|
|
|
|
+ SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
for (SurveyTemplate template : activeTemplates) {
|
|
for (SurveyTemplate template : activeTemplates) {
|
|
|
|
|
+ Date[] period = computeCurrentPeriod(template.getCycleType());
|
|
|
Map<String, Object> item = new HashMap<>();
|
|
Map<String, Object> item = new HashMap<>();
|
|
|
item.put("templateId", template.getId());
|
|
item.put("templateId", template.getId());
|
|
|
item.put("title", template.getTitle());
|
|
item.put("title", template.getTitle());
|
|
|
item.put("dimension", template.getDimension());
|
|
item.put("dimension", template.getDimension());
|
|
|
item.put("cycleType", template.getCycleType());
|
|
item.put("cycleType", template.getCycleType());
|
|
|
|
|
+ item.put("periodStart", fmt.format(period[0]));
|
|
|
|
|
+ item.put("periodEnd", fmt.format(period[1]));
|
|
|
List<SurveyRecord> records = recordsByTemplate.get(template.getId());
|
|
List<SurveyRecord> records = recordsByTemplate.get(template.getId());
|
|
|
- if (records != null && records.size() > 0) {
|
|
|
|
|
- SurveyRecord latest = records.get(0);
|
|
|
|
|
|
|
+ SurveyRecord latest = null;
|
|
|
|
|
+ boolean submittedInCurrentPeriod = false;
|
|
|
|
|
+ if (records != null) {
|
|
|
for (SurveyRecord r : records) {
|
|
for (SurveyRecord r : records) {
|
|
|
- if (r.getSubmittedAt() != null && latest.getSubmittedAt() != null && r.getSubmittedAt().after(latest.getSubmittedAt())) {
|
|
|
|
|
- latest = r;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Date st = r.getSubmittedAt();
|
|
|
|
|
+ if (st == null) continue;
|
|
|
|
|
+ if (latest == null || st.after(latest.getSubmittedAt())) latest = r;
|
|
|
|
|
+ // 提交时间落在当前周期 [periodStart, periodEnd] 内即视为本期已完成
|
|
|
|
|
+ if (!st.before(period[0]) && !st.after(period[1])) submittedInCurrentPeriod = true;
|
|
|
}
|
|
}
|
|
|
- item.put("status", "done");
|
|
|
|
|
- item.put("lastSubmittedAt", latest.getSubmittedAt());
|
|
|
|
|
- } else {
|
|
|
|
|
- item.put("status", "pending");
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ item.put("status", submittedInCurrentPeriod ? "done" : "pending");
|
|
|
|
|
+ item.put("lastSubmittedAt", latest != null ? latest.getSubmittedAt() : null);
|
|
|
statusList.add(item);
|
|
statusList.add(item);
|
|
|
}
|
|
}
|
|
|
return statusList;
|
|
return statusList;
|
|
@@ -149,8 +165,10 @@ public class SurveyServiceImpl implements SurveyService {
|
|
|
record.setMemberId(memberId);
|
|
record.setMemberId(memberId);
|
|
|
record.setTemplateId(templateId);
|
|
record.setTemplateId(templateId);
|
|
|
record.setAnswersJson(answersJson);
|
|
record.setAnswersJson(answersJson);
|
|
|
- record.setPeriodStart(periodStart);
|
|
|
|
|
- record.setPeriodEnd(periodEnd);
|
|
|
|
|
|
|
+ // 周期由服务端按模板 cycle_type 统一计算并落库,忽略客户端传参(防止前端时钟偏差/篡改导致周期错乱)
|
|
|
|
|
+ Date[] period = computeCurrentPeriod(template.getCycleType());
|
|
|
|
|
+ record.setPeriodStart(period[0]);
|
|
|
|
|
+ record.setPeriodEnd(period[1]);
|
|
|
surveyRecordMapper.insert(record);
|
|
surveyRecordMapper.insert(record);
|
|
|
return surveyRecordMapper.selectById(record.getId());
|
|
return surveyRecordMapper.selectById(record.getId());
|
|
|
}
|
|
}
|
|
@@ -158,14 +176,9 @@ public class SurveyServiceImpl implements SurveyService {
|
|
|
@Override
|
|
@Override
|
|
|
public List<SurveyRecord> getSurveyHistory(Long userId, Long memberId) {
|
|
public List<SurveyRecord> getSurveyHistory(Long userId, Long memberId) {
|
|
|
memberId = resolveMemberId(userId, memberId);
|
|
memberId = resolveMemberId(userId, memberId);
|
|
|
- List<SurveyRecord> all = surveyRecordMapper.selectList(null);
|
|
|
|
|
- List<SurveyRecord> result = new ArrayList<>();
|
|
|
|
|
- for (SurveyRecord r : all) {
|
|
|
|
|
- if (r.getMemberId() != null && r.getMemberId().equals(memberId)) {
|
|
|
|
|
- result.add(r);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return result;
|
|
|
|
|
|
|
+ if (memberId == null) return new ArrayList<>();
|
|
|
|
|
+ return surveyRecordMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<SurveyRecord>().eq(SurveyRecord::getMemberId, memberId));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private Long resolveMemberId(Long userId, Long memberId) {
|
|
private Long resolveMemberId(Long userId, Long memberId) {
|
|
@@ -173,7 +186,7 @@ public class SurveyServiceImpl implements SurveyService {
|
|
|
if (userId == null) return null;
|
|
if (userId == null) return null;
|
|
|
try {
|
|
try {
|
|
|
FamilyMember self = familyMemberMapper.selectOne(
|
|
FamilyMember self = familyMemberMapper.selectOne(
|
|
|
- new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<FamilyMember>()
|
|
|
|
|
|
|
+ new LambdaQueryWrapper<FamilyMember>()
|
|
|
.eq(FamilyMember::getUserId, userId)
|
|
.eq(FamilyMember::getUserId, userId)
|
|
|
.last("LIMIT 1")
|
|
.last("LIMIT 1")
|
|
|
);
|
|
);
|
|
@@ -184,29 +197,47 @@ public class SurveyServiceImpl implements SurveyService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按周期类型计算当前调研周期的起止时间(服务端唯一时间源)
|
|
|
|
|
+ * - weekly(默认,含未知类型): 本周一 00:00:00 ~ 本周日 23:59:59
|
|
|
|
|
+ * - biweekly: 上周一 00:00:00 ~ 本周日 23:59:59(14天窗口,锚定自然周)
|
|
|
|
|
+ * - monthly: 本月1日 00:00:00 ~ 本月最后一天 23:59:59
|
|
|
|
|
+ */
|
|
|
|
|
+ private Date[] computeCurrentPeriod(String cycleType) {
|
|
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
|
|
+ String cycle = cycleType == null ? "" : cycleType.trim().toLowerCase();
|
|
|
|
|
+ LocalDate startDate;
|
|
|
|
|
+ LocalDate endDate;
|
|
|
|
|
+ if ("monthly".equals(cycle)) {
|
|
|
|
|
+ startDate = today.withDayOfMonth(1);
|
|
|
|
|
+ endDate = today.withDayOfMonth(today.lengthOfMonth());
|
|
|
|
|
+ } else if ("biweekly".equals(cycle)) {
|
|
|
|
|
+ startDate = today.with(DayOfWeek.MONDAY).minusWeeks(1);
|
|
|
|
|
+ endDate = today.with(DayOfWeek.SUNDAY);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ startDate = today.with(DayOfWeek.MONDAY);
|
|
|
|
|
+ endDate = today.with(DayOfWeek.SUNDAY);
|
|
|
|
|
+ }
|
|
|
|
|
+ Date start = Date.from(startDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
|
|
+ Date end = Date.from(endDate.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant());
|
|
|
|
|
+ return new Date[]{start, end};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * AI 生成调研题目 — 统一走 AiGateway(熔断+超时+Fallback),禁止直连 LangGraph
|
|
|
|
|
+ */
|
|
|
@Override
|
|
@Override
|
|
|
public String aiGenerateQuestions(String dimension, String topic) {
|
|
public String aiGenerateQuestions(String dimension, String topic) {
|
|
|
- if (langgraphBaseUrl == null || langgraphBaseUrl.isEmpty()) {
|
|
|
|
|
- return buildFallbackQuestions(dimension, topic);
|
|
|
|
|
- }
|
|
|
|
|
- try {
|
|
|
|
|
- String url = langgraphBaseUrl + "/api/v1/analysis/run";
|
|
|
|
|
- HttpHeaders headers = new HttpHeaders();
|
|
|
|
|
- headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
- Map<String, Object> body = new HashMap<>();
|
|
|
|
|
- body.put("dimension", dimension);
|
|
|
|
|
- body.put("topic", topic);
|
|
|
|
|
- body.put("task", "generate_survey_questions");
|
|
|
|
|
- body.put("prompt", "基于维度[" + dimension + "]和主题[" + topic + "],生成5道定期调研单选题。每道题包含text和options(字符串数组,2-5个选项)。直接返回JSON数组。");
|
|
|
|
|
- HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
|
|
|
|
|
- RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
- ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
|
|
|
|
|
- if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
|
|
- return response.getBody();
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.warn("LangGraph AI生成调研题目失败: {}", e.getMessage());
|
|
|
|
|
|
|
+ Map<String, Object> inputs = new HashMap<>();
|
|
|
|
|
+ inputs.put("dimension", dimension);
|
|
|
|
|
+ inputs.put("topic", topic);
|
|
|
|
|
+ inputs.put("task", "generate_survey_questions");
|
|
|
|
|
+ inputs.put("prompt", "基于维度[" + dimension + "]和主题[" + topic + "],生成5道定期调研单选题。每道题包含text和options(字符串数组,2-5个选项)。直接返回JSON数组。");
|
|
|
|
|
+ String result = aiGateway.generateHealthPlan(inputs);
|
|
|
|
|
+ if (result != null && !result.trim().isEmpty()) {
|
|
|
|
|
+ return result;
|
|
|
}
|
|
}
|
|
|
|
|
+ log.warn("AI生成调研题目失败或网关不可用,使用兜底题目: dimension={}, topic={}", dimension, topic);
|
|
|
return buildFallbackQuestions(dimension, topic);
|
|
return buildFallbackQuestions(dimension, topic);
|
|
|
}
|
|
}
|
|
|
|
|
|