Эх сурвалжийг харах

chore: auto bump version and changelog [skip ci]

iwt 2 сар өмнө
parent
commit
4c20837597

+ 86 - 55
cfc-backend/src/main/java/com/etotem/cfc/service/impl/SurveyServiceImpl.java

@@ -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);
     }
     }
 
 

+ 43 - 0
cfc-frontend/pages/health-main/index.vue

@@ -47,6 +47,17 @@
           <text class="self-test-arrow">›</text>
           <text class="self-test-arrow">›</text>
         </view>
         </view>
       </view>
       </view>
+      <!-- 本期待填调研(按周期计算,服务端返回 pending 列表) -->
+      <view v-for="item in pendingSurveys" :key="surveyKey(item)" class="self-test-card" @click="goSurveyFill(item)">
+        <view class="self-test-head">
+          <text class="self-test-icon">📋</text>
+          <view class="self-test-info">
+            <text class="self-test-title">{{ item.title }}</text>
+            <text class="self-test-sub">{{ cycleLabel(item.cycleType) }} · {{ item.periodStart }} 至 {{ item.periodEnd }}</text>
+          </view>
+          <text class="survey-pending-tag">待填写</text>
+        </view>
+      </view>
       
       
         <RadarChart :dimensions="demoBodyDims" :scores="demoBodyScores" fillColor="#FF8C42" childName="示例" width="560" height="460" />
         <RadarChart :dimensions="demoBodyDims" :scores="demoBodyScores" fillColor="#FF8C42" childName="示例" width="560" height="460" />
         <view class="demo-chart-title-row">
         <view class="demo-chart-title-row">
@@ -263,6 +274,7 @@ export default {
       lastPlanId: null,
       lastPlanId: null,
       healthArticles: [],
       healthArticles: [],
       hasAiQHistory: false,
       hasAiQHistory: false,
+      pendingSurveys: [],
       tabs: [
       tabs: [
         { code: 'body', label: '健康', icon: '🟠', color: '#FF8C42' },
         { code: 'body', label: '健康', icon: '🟠', color: '#FF8C42' },
         { code: 'cognitive', label: '认知', icon: '🧠', color: '#6366F1' },
         { code: 'cognitive', label: '认知', icon: '🧠', color: '#6366F1' },
@@ -446,6 +458,9 @@ export default {
       }
       }
       this.loadHealthData()
       this.loadHealthData()
       this.loadPlanList()
       this.loadPlanList()
+      this.loadPendingSurveys()
+    } else {
+      this.pendingSurveys = []
     }
     }
   },
   },
   methods: {
   methods: {
@@ -505,6 +520,33 @@ export default {
       return status === 'resolved' ? 'resolved' : 'pending'
       return status === 'resolved' ? 'resolved' : 'pending'
     },
     },
     goSelfTest: function() { uni.navigateTo({ url: '/pages/health/survey-questionnaire?dim=all' }) },
     goSelfTest: function() { uni.navigateTo({ url: '/pages/health/survey-questionnaire?dim=all' }) },
+    // 拉取本期待填调研(服务端按 cycle_type 计算当前周期)
+    loadPendingSurveys: function() {
+      var self = this
+      var api = require('../../utils/api.js')
+      api.getSurveyStatus({}).then(function(res) {
+        var list = (res && res.code === 200 && res.data) ? res.data : []
+        var pending = []
+        for (var i = 0; i < list.length; i++) {
+          if (list[i] && list[i].status === 'pending') pending.push(list[i])
+        }
+        self.pendingSurveys = pending
+      }).catch(function() { self.pendingSurveys = [] })
+    },
+    surveyKey: function(item) { return item && item.templateId ? item.templateId : Math.random() },
+    cycleLabel: function(cycleType) {
+      var map = { weekly: '每周调研', biweekly: '双周调研', monthly: '每月调研' }
+      return map[cycleType] || '定期调研'
+    },
+    goSurveyFill: function(item) {
+      if (!item || !item.templateId) return
+      var url = '/pages/health/survey-questionnaire?templateId=' + item.templateId +
+        '&templateTitle=' + encodeURIComponent(item.title || '定期调研') +
+        '&templateDimension=' + encodeURIComponent(item.dimension || '') +
+        '&periodStart=' + (item.periodStart || '') +
+        '&periodEnd=' + (item.periodEnd || '')
+      uni.navigateTo({ url: url })
+    },
     goPlanSummary: function() {
     goPlanSummary: function() {
       var url = '/pages/health/health-plan-summary'
       var url = '/pages/health/health-plan-summary'
       if (this.lastPlanId) url += '?planId=' + this.lastPlanId
       if (this.lastPlanId) url += '?planId=' + this.lastPlanId
@@ -741,4 +783,5 @@ export default {
 .wc-action .wc-name { color: #10B981; }
 .wc-action .wc-name { color: #10B981; }
 .wc-mind { border-color: rgba(255,107,157,0.3); background: rgba(255,107,157,0.04); border-left: 8rpx solid #FF6B9D; }
 .wc-mind { border-color: rgba(255,107,157,0.3); background: rgba(255,107,157,0.04); border-left: 8rpx solid #FF6B9D; }
 .wc-mind .wc-name { color: #FF6B9D; }
 .wc-mind .wc-name { color: #FF6B9D; }
+.survey-pending-tag { flex-shrink: 0; font-size: 20rpx; color: #fff; background: #F97316; padding: 6rpx 16rpx; border-radius: 999rpx; }
 </style>
 </style>

+ 2 - 1
cfc-frontend/pages/index-home/index.vue

@@ -53,6 +53,7 @@
         <view class="self-test-info">
         <view class="self-test-info">
           <text class="self-test-title">{{ selfTestTitle }}</text>
           <text class="self-test-title">{{ selfTestTitle }}</text>
           <text class="self-test-sub">{{ selfTestSub }}</text>
           <text class="self-test-sub">{{ selfTestSub }}</text>
+          <text v-if="pendingSurveys.length > 0" class="self-test-badge">有 {{ pendingSurveys.length }} 项调研待填</text>
         </view>
         </view>
         <text class="self-test-arrow">›</text>
         <text class="self-test-arrow">›</text>
       </view>
       </view>
@@ -362,7 +363,7 @@ import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import DailyTaskCard from '../../components/daily-task-card.vue'
 import DailyTaskCard from '../../components/daily-task-card.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getEnergyOverview, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getEnergyOverview, getFamilyMemberList, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 import config from '@/config.js'
 import { parseDate } from '../../utils/format.js'
 import { parseDate } from '../../utils/format.js'

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-520418f6ede892e8c01633e2c4c5847e13f6097c
+1e9da2cfb9d5095aa8c44fcf4233cf79d216b2d7

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "cfc-web",
   "name": "cfc-web",
-  "version": "1.0.1193",
+  "version": "1.0.1194",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "cfc-web",
       "name": "cfc-web",
-      "version": "1.0.1193",
+      "version": "1.0.1194",
       "dependencies": {
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "cfc-web",
   "name": "cfc-web",
-  "version": "1.0.1194",
+  "version": "1.0.1195",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {
     "dev": "vue-cli-service serve",
     "dev": "vue-cli-service serve",

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 
 ---
 ---
 
 
+## v1.0.1195 (2026-08-22)
+
+### 新功能
+- 完整圈子社交功能-创建/加入/动态Feed/点赞评论
+
+
 ## v1.0.1194 (2026-08-22)
 ## v1.0.1194 (2026-08-22)
 
 
 ### 文档
 ### 文档

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 # 更新日志
 
 
-> 当前版本: v1.0.1194
+> 当前版本: v1.0.1195
 
 
 ## 历史版本
 ## 历史版本
 
 
@@ -8,6 +8,12 @@
 
 
 ---
 ---
 
 
+## v1.0.1195 (2026-08-22)
+
+### 新功能
+- 完整圈子社交功能-创建/加入/动态Feed/点赞评论
+
+
 ## v1.0.1194 (2026-08-22)
 ## v1.0.1194 (2026-08-22)
 
 
 ### 文档
 ### 文档