|
@@ -0,0 +1,865 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.etotem.cfc.common.OrderNoGenerator;
|
|
|
|
|
+import com.etotem.cfc.dto.QuotaStatusDTO;
|
|
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 菌群健康调研项目(单次付费问卷)
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 业务流程:
|
|
|
|
|
+ * 1. 用户付费 19.9 元创建项目
|
|
|
|
|
+ * 2. 填写健康问卷(睡眠/饮食/体重/消化等菌群相关指标)
|
|
|
|
|
+ * 3. 生成健康建议 + 风险评分 + 一周行动任务
|
|
|
|
|
+ * 4. 用户获得「会员价购买菌群检测」资格凭证(不自动开通会员)
|
|
|
|
|
+ * 5. 用户在小程序/商城使用会员价购买菌群检测商品
|
|
|
|
|
+ * 6. 购买成功后 → 自动开通一年家庭会员
|
|
|
|
|
+ * 7. 用户在小程序打卡完成一周任务后,返还同等金额的 CF 值
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+public class GutHealthProjectService {
|
|
|
|
|
+
|
|
|
|
|
+ /** 默认项目价格(分) */
|
|
|
|
|
+ private static final int DEFAULT_PRICE = 1990;
|
|
|
|
|
+ /** 任务列表默认总数(一周 7 天) */
|
|
|
|
|
+ private static final int DEFAULT_TASK_TOTAL = 7;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private GutHealthProjectMapper projectMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private PaymentOrderMapper paymentOrderMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private FamilyMembershipMapper familyMembershipMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private PlatformPointsService platformPointsService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private SysConfigService sysConfigService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MemberUpgradeRecordMapper memberUpgradeRecordMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MembershipService membershipService;
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 问卷题库 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /** 问卷题目定义(菌群健康相关指标) */
|
|
|
|
|
+ public static List<Map<String, Object>> getQuestionnaireQuestions() {
|
|
|
|
|
+ List<Map<String, Object>> questions = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 睡眠质量
|
|
|
|
|
+ Map<String, Object> q1 = new LinkedHashMap<>();
|
|
|
|
|
+ q1.put("dimension", "sleep");
|
|
|
|
|
+ q1.put("label", "睡眠质量");
|
|
|
|
|
+ q1.put("question", "您最近两周的睡眠质量如何?");
|
|
|
|
|
+ q1.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "很好,入睡快且无夜间醒来", "score", 0),
|
|
|
|
|
+ mapOf("text", "尚可,偶尔入睡困难或早醒", "score", 1),
|
|
|
|
|
+ mapOf("text", "较差,经常入睡困难或夜间多次醒来", "score", 2),
|
|
|
|
|
+ mapOf("text", "很差,几乎每晚失眠或严重早醒", "score", 3)));
|
|
|
|
|
+ questions.add(q1);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 饮食结构
|
|
|
|
|
+ Map<String, Object> q2 = new LinkedHashMap<>();
|
|
|
|
|
+ q2.put("dimension", "diet");
|
|
|
|
|
+ q2.put("label", "饮食结构");
|
|
|
|
|
+ q2.put("question", "您的日常饮食结构如何?(荤素搭配、蔬菜水果摄入)");
|
|
|
|
|
+ q2.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "均衡饮食,蔬菜水果充足", "score", 0),
|
|
|
|
|
+ mapOf("text", "基本均衡,但蔬菜水果偏少", "score", 1),
|
|
|
|
|
+ mapOf("text", "偏荤或偏素,蔬菜水果摄入不足", "score", 2),
|
|
|
|
|
+ mapOf("text", "高油高糖高盐为主,极少蔬果", "score", 3)));
|
|
|
|
|
+ questions.add(q2);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 体重状况
|
|
|
|
|
+ Map<String, Object> q3 = new LinkedHashMap<>();
|
|
|
|
|
+ q3.put("dimension", "weight");
|
|
|
|
|
+ q3.put("label", "体重状况");
|
|
|
|
|
+ q3.put("question", "您的体重状况?(或身高体重比)");
|
|
|
|
|
+ q3.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "体重正常(BMI 18.5-23.9)", "score", 0),
|
|
|
|
|
+ mapOf("text", "偏瘦或轻微超重(BMI 24-26)", "score", 1),
|
|
|
|
|
+ mapOf("text", "超重(BMI 26-28)", "score", 2),
|
|
|
|
|
+ mapOf("text", "肥胖(BMI ≥28)或严重偏瘦", "score", 3)));
|
|
|
|
|
+ questions.add(q3);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 消化状况
|
|
|
|
|
+ Map<String, Object> q4 = new LinkedHashMap<>();
|
|
|
|
|
+ q4.put("dimension", "digestion");
|
|
|
|
|
+ q4.put("label", "消化状况");
|
|
|
|
|
+ q4.put("question", "您近期的消化功能如何?");
|
|
|
|
|
+ q4.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "消化良好,无腹胀腹泻便秘", "score", 0),
|
|
|
|
|
+ mapOf("text", "偶尔腹胀、排气或排便不规律", "score", 1),
|
|
|
|
|
+ mapOf("text", "经常腹胀、腹泻或便秘交替", "score", 2),
|
|
|
|
|
+ mapOf("text", "长期肠胃不适,严重影响生活", "score", 3)));
|
|
|
|
|
+ questions.add(q4);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 运动习惯
|
|
|
|
|
+ Map<String, Object> q5 = new LinkedHashMap<>();
|
|
|
|
|
+ q5.put("dimension", "exercise");
|
|
|
|
|
+ q5.put("label", "运动习惯");
|
|
|
|
|
+ q5.put("question", "您每周的运动频率如何?");
|
|
|
|
|
+ q5.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "每周3次以上,每次30分钟以上", "score", 0),
|
|
|
|
|
+ mapOf("text", "每周1-2次,强度适中", "score", 1),
|
|
|
|
|
+ mapOf("text", "偶尔运动,不规律", "score", 2),
|
|
|
|
|
+ mapOf("text", "几乎不运动", "score", 3)));
|
|
|
|
|
+ questions.add(q5);
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 压力水平
|
|
|
|
|
+ Map<String, Object> q6 = new LinkedHashMap<>();
|
|
|
|
|
+ q6.put("dimension", "stress");
|
|
|
|
|
+ q6.put("label", "压力水平");
|
|
|
|
|
+ q6.put("question", "您近期的精神压力状态如何?");
|
|
|
|
|
+ q6.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "轻松愉快,压力很小", "score", 0),
|
|
|
|
|
+ mapOf("text", "偶尔有压力,能自我调节", "score", 1),
|
|
|
|
|
+ mapOf("text", "经常感到压力,难以放松", "score", 2),
|
|
|
|
|
+ mapOf("text", "长期高压焦虑,影响睡眠和食欲", "score", 3)));
|
|
|
|
|
+ questions.add(q6);
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 膳食纤维
|
|
|
|
|
+ Map<String, Object> q7 = new LinkedHashMap<>();
|
|
|
|
|
+ q7.put("dimension", "fiber");
|
|
|
|
|
+ q7.put("label", "膳食纤维");
|
|
|
|
|
+ q7.put("question", "您每天的主食和膳食纤维摄入情况?");
|
|
|
|
|
+ q7.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "全谷物/粗粮占主食一半以上", "score", 0),
|
|
|
|
|
+ mapOf("text", "粗细搭配,有一定全谷物", "score", 1),
|
|
|
|
|
+ mapOf("text", "以精米白面为主,很少粗粮", "score", 2),
|
|
|
|
|
+ mapOf("text", "完全精制主食,不吃粗粮豆类", "score", 3)));
|
|
|
|
|
+ questions.add(q7);
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 抗生素与药物
|
|
|
|
|
+ Map<String, Object> q8 = new LinkedHashMap<>();
|
|
|
|
|
+ q8.put("dimension", "antibiotic");
|
|
|
|
|
+ q8.put("label", "抗生素使用");
|
|
|
|
|
+ q8.put("question", "近半年您是否使用过抗生素或长期服药?");
|
|
|
|
|
+ q8.put("options", Arrays.asList(
|
|
|
|
|
+ mapOf("text", "未使用过", "score", 0),
|
|
|
|
|
+ mapOf("text", "偶尔使用(1次以内)", "score", 1),
|
|
|
|
|
+ mapOf("text", "使用过2-3次", "score", 2),
|
|
|
|
|
+ mapOf("text", "多次使用或长期服药", "score", 3)));
|
|
|
|
|
+ questions.add(q8);
|
|
|
|
|
+
|
|
|
|
|
+ return questions;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Map<String, Object> mapOf(Object... kv) {
|
|
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
|
|
+ for (int i = 0; i + 1 < kv.length; i += 2) {
|
|
|
|
|
+ m.put(String.valueOf(kv[i]), kv[i + 1]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 业务方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取当前用户的项目状态
|
|
|
|
|
+ * @return Map: {hasProject, project, questions, price}
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> getProjectStatus(Long userId) {
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ GutHealthProject project = getLatestProject(userId);
|
|
|
|
|
+ result.put("hasProject", project != null);
|
|
|
|
|
+ result.put("project", project != null ? toProjectVO(project) : null);
|
|
|
|
|
+ result.put("questions", getQuestionnaireQuestions());
|
|
|
|
|
+ result.put("price", getProjectPrice());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建项目付费订单
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> createProjectOrder(Long userId) {
|
|
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("用户不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 已存在未完成项目,禁止重复购买
|
|
|
|
|
+ GutHealthProject existing = getLatestProject(userId);
|
|
|
|
|
+ if (existing != null && "pending".equals(existing.getStatus())) {
|
|
|
|
|
+ throw new RuntimeException("您已有进行中的菌群健康调研项目,请先完成问卷");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int price = getProjectPrice();
|
|
|
|
|
+
|
|
|
|
|
+ PaymentOrder order = new PaymentOrder();
|
|
|
|
|
+ order.setOrderNo(OrderNoGenerator.generate("GH"));
|
|
|
|
|
+ order.setFamilyId(user.getFamilyId());
|
|
|
|
|
+ order.setLevelCode("GUT_HEALTH");
|
|
|
|
|
+ order.setPaymentType("one_time");
|
|
|
|
|
+ order.setPeriod("once");
|
|
|
|
|
+ order.setAmount(price);
|
|
|
|
|
+ order.setStatus("pending");
|
|
|
|
|
+ order.setPeriodNo(1);
|
|
|
|
|
+ order.setCreatedAt(new Date());
|
|
|
|
|
+ order.setUpdatedAt(new Date());
|
|
|
|
|
+ paymentOrderMapper.insert(order);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("orderNo", order.getOrderNo());
|
|
|
|
|
+ result.put("amount", order.getAmount());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理支付成功回调:创建项目记录
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public boolean processPaymentCallback(String orderNo, String transactionId, String payMethod) {
|
|
|
|
|
+ PaymentOrder order = paymentOrderMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<PaymentOrder>()
|
|
|
|
|
+ .eq(PaymentOrder::getOrderNo, orderNo));
|
|
|
|
|
+ if (order == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("paid".equals(order.getStatus())) {
|
|
|
|
|
+ return true; // 幂等
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ order.setStatus("paid");
|
|
|
|
|
+ order.setTransactionId(transactionId);
|
|
|
|
|
+ order.setPayMethod(payMethod);
|
|
|
|
|
+ order.setPaidAt(new Date());
|
|
|
|
|
+ order.setUpdatedAt(new Date());
|
|
|
|
|
+ paymentOrderMapper.updateById(order);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建项目记录
|
|
|
|
|
+ GutHealthProject project = new GutHealthProject();
|
|
|
|
|
+ Long userId = resolveUserIdByOrder(order);
|
|
|
|
|
+ project.setUserId(userId);
|
|
|
|
|
+ project.setFamilyId(order.getFamilyId());
|
|
|
|
|
+ project.setStatus("pending");
|
|
|
|
|
+ project.setMicrobeTestRecommended(0);
|
|
|
|
|
+ project.setMemberDiscountQualified(0);
|
|
|
|
|
+ project.setFamilyMembershipQualified(0);
|
|
|
|
|
+ project.setFamilyMembershipActivated(0);
|
|
|
|
|
+ project.setCfRefundAmount(order.getAmount());
|
|
|
|
|
+ project.setCfRefunded(0);
|
|
|
|
|
+ project.setCompletedTasksCount(0);
|
|
|
|
|
+ project.setPaidAt(new Date());
|
|
|
|
|
+ project.setCreatedAt(new Date());
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.insert(project);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("菌群健康项目创建成功: orderNo={}, projectId={}, userId={}, amount={}",
|
|
|
|
|
+ orderNo, project.getId(), project.getUserId(), order.getAmount());
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long resolveUserIdByOrder(PaymentOrder order) {
|
|
|
|
|
+ // 从 family_memberships 或用户表回查
|
|
|
|
|
+ if (order.getFamilyId() == null) return null;
|
|
|
|
|
+ List<User> users = userMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<User>().eq(User::getFamilyId, order.getFamilyId()));
|
|
|
|
|
+ if (users != null && !users.isEmpty()) {
|
|
|
|
|
+ return users.get(0).getId();
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交问卷答案,生成健康建议 + 行动任务 + 奖励资格
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Map<String, Object> submitQuestionnaire(Long userId, Long projectId, List<Map<String, Object>> answers) {
|
|
|
|
|
+ GutHealthProject project = projectMapper.selectById(projectId);
|
|
|
|
|
+ if (project == null || !userId.equals(project.getUserId())) {
|
|
|
|
|
+ throw new RuntimeException("项目不存在或无权操作");
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("completed".equals(project.getStatus())) {
|
|
|
|
|
+ throw new RuntimeException("该项目已完成,无需重复提交");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存答案
|
|
|
|
|
+ ObjectMapper mapper = new ObjectMapper();
|
|
|
|
|
+ try {
|
|
|
|
|
+ project.setQuestions(mapper.writeValueAsString(answers));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ project.setQuestions(answers.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算风险评分
|
|
|
|
|
+ Map<String, Integer> riskScores = computeRiskScores(answers);
|
|
|
|
|
+ project.setRiskScores(safeJson(riskScores));
|
|
|
|
|
+
|
|
|
|
|
+ // 生成健康建议
|
|
|
|
|
+ List<Map<String, Object>> recommendations = generateRecommendations(riskScores, answers);
|
|
|
|
|
+ project.setHealthRecommendations(safeJson(recommendations));
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否推荐菌群检测(任一维度风险 >= 2)
|
|
|
|
|
+ boolean recommendTest = riskScores.values().stream().anyMatch(s -> s >= 2);
|
|
|
|
|
+ project.setMicrobeTestRecommended(recommendTest ? 1 : 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 生成一周行动任务
|
|
|
|
|
+ List<Map<String, Object>> tasks = generateActionTasks(riskScores);
|
|
|
|
|
+ project.setActionTasks(safeJson(tasks));
|
|
|
|
|
+ project.setCompletedTasksCount(0);
|
|
|
|
|
+
|
|
|
|
|
+ // 授予会员价购买菌群检测资格(不自动开通会员)
|
|
|
|
|
+ project.setMemberDiscountQualified(1);
|
|
|
|
|
+ project.setStatus("completed");
|
|
|
|
|
+ project.setFamilyMembershipQualified(0);
|
|
|
|
|
+ project.setFamilyMembershipActivated(0);
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.updateById(project);
|
|
|
|
|
+ project.setFamilyMembershipActivated(0);
|
|
|
|
|
+ project.setCompletedAt(new Date());
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.updateById(project);
|
|
|
|
|
+
|
|
|
|
|
+ // 返回结果
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("project", toProjectVO(project));
|
|
|
|
|
+ result.put("riskScores", riskScores);
|
|
|
|
|
+ result.put("recommendations", recommendations);
|
|
|
|
|
+ result.put("microbeTestRecommended", recommendTest);
|
|
|
|
|
+ result.put("tasks", tasks);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 任务打卡 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取行动任务列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<Map<String, Object>> getActionTasks(Long userId, Long projectId) {
|
|
|
|
|
+ GutHealthProject project = getOwnedProject(userId, projectId);
|
|
|
|
|
+ if (project == null || project.getActionTasks() == null) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ return parseJsonList(project.getActionTasks());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打卡某个任务
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Map<String, Object> checkinTask(Long userId, Long projectId, int taskIndex) {
|
|
|
|
|
+ GutHealthProject project = getOwnedProject(userId, projectId);
|
|
|
|
|
+ if (project == null) {
|
|
|
|
|
+ throw new RuntimeException("项目不存在或无权操作");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!"completed".equals(project.getStatus())) {
|
|
|
|
|
+ throw new RuntimeException("请先完成问卷后再打卡");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (project.getCfRefunded() != null && project.getCfRefunded() == 1) {
|
|
|
|
|
+ throw new RuntimeException("任务已全部完成,CF值已返还");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> tasks = parseJsonList(project.getActionTasks());
|
|
|
|
|
+ if (taskIndex < 0 || taskIndex >= tasks.size()) {
|
|
|
|
|
+ throw new RuntimeException("任务索引越界");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> task = tasks.get(taskIndex);
|
|
|
|
|
+ if (Boolean.TRUE.equals(task.get("done"))) {
|
|
|
|
|
+ throw new RuntimeException("该任务已打卡");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ task.put("done", true);
|
|
|
|
|
+ task.put("doneAt", new Date().getTime());
|
|
|
|
|
+ project.setActionTasks(safeJson(tasks));
|
|
|
|
|
+
|
|
|
|
|
+ int doneCount = (int) tasks.stream().filter(t -> Boolean.TRUE.equals(t.get("done"))).count();
|
|
|
|
|
+ project.setCompletedTasksCount(doneCount);
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.updateById(project);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("doneCount", doneCount);
|
|
|
|
|
+ result.put("totalTasks", tasks.size());
|
|
|
|
|
+ result.put("allDone", doneCount >= tasks.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 全部完成 → 返还 CF 值
|
|
|
|
|
+ if (doneCount >= tasks.size()) {
|
|
|
|
|
+ result.put("cfRefunded", refundCf(project));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.put("cfRefunded", false);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取打卡进度
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> getCheckinStatus(Long userId, Long projectId) {
|
|
|
|
|
+ GutHealthProject project = getOwnedProject(userId, projectId);
|
|
|
|
|
+ if (project == null) {
|
|
|
|
|
+ return Collections.emptyMap();
|
|
|
|
|
+ }
|
|
|
|
|
+ List<Map<String, Object>> tasks = parseJsonList(project.getActionTasks());
|
|
|
|
|
+ int doneCount = (int) tasks.stream().filter(t -> Boolean.TRUE.equals(t.get("done"))).count();
|
|
|
|
|
+ int total = tasks.size();
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("projectId", project.getId());
|
|
|
|
|
+ result.put("tasks", tasks);
|
|
|
|
|
+ result.put("doneCount", doneCount);
|
|
|
|
|
+ result.put("totalTasks", total);
|
|
|
|
|
+ result.put("allDone", total > 0 && doneCount >= total);
|
|
|
|
|
+ result.put("cfRefunded", project.getCfRefunded() != null && project.getCfRefunded() == 1);
|
|
|
|
|
+ result.put("cfRefundAmount", project.getCfRefundAmount() != null ? project.getCfRefundAmount() : 0);
|
|
|
|
|
+ result.put("memberDiscountQualified", project.getMemberDiscountQualified() != null && project.getMemberDiscountQualified() == 1);
|
|
|
|
|
+ result.put("familyMembershipQualified", project.getFamilyMembershipQualified() != null && project.getFamilyMembershipQualified() == 1);
|
|
|
|
|
+ result.put("familyMembershipActivated", project.getFamilyMembershipActivated() != null && project.getFamilyMembershipActivated() == 1);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 全部任务完成 → 返还同等金额 CF 值
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean refundCf(GutHealthProject project) {
|
|
|
|
|
+ if (project.getCfRefunded() != null && project.getCfRefunded() == 1) {
|
|
|
|
|
+ return true; // 幂等
|
|
|
|
|
+ }
|
|
|
|
|
+ int amount = project.getCfRefundAmount() != null ? project.getCfRefundAmount() : 0;
|
|
|
|
|
+ if (amount <= 0) {
|
|
|
|
|
+ amount = getProjectPrice();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ platformPointsService.earn(project.getUserId(), amount, "GUT_HEALTH", project.getId(),
|
|
|
|
|
+ "菌群健康调研一周打卡完成,返还CF值");
|
|
|
|
|
+ project.setCfRefunded(1);
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.updateById(project);
|
|
|
|
|
+ log.info("菌群健康项目CF值返还: userId={}, projectId={}, amount={}", project.getUserId(), project.getId(), amount);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("CF值返还失败: userId={}, projectId={}", project.getUserId(), project.getId(), e);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 奖励激活 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 激活一年家庭会员(从资格中激活)
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Map<String, Object> activateFamilyMembership(Long userId, Long projectId) {
|
|
|
|
|
+ GutHealthProject project = getOwnedProject(userId, projectId);
|
|
|
|
|
+ if (project == null) {
|
|
|
|
|
+ throw new RuntimeException("项目不存在或无权操作");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (project.getFamilyMembershipQualified() == null || project.getFamilyMembershipQualified() != 1) {
|
|
|
|
|
+ throw new RuntimeException("您尚未获得一年家庭会员资格,请先完成健康问卷");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (project.getFamilyMembershipActivated() != null && project.getFamilyMembershipActivated() == 1) {
|
|
|
|
|
+ throw new RuntimeException("一年家庭会员已激活,请勿重复操作");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 通过会员体系开通一年家庭会员
|
|
|
|
|
+ Long familyId = project.getFamilyId() != null ? project.getFamilyId() : getUserFamilyId(userId);
|
|
|
|
|
+ if (familyId == null) {
|
|
|
|
|
+ throw new RuntimeException("请先创建或加入家庭");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date expireTime = addYears(new Date(), 1);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyMembership membership = new FamilyMembership();
|
|
|
|
|
+ membership.setFamilyId(familyId);
|
|
|
|
|
+ membership.setLevelCode("FAMILY");
|
|
|
|
|
+ membership.setStartDate(new Date());
|
|
|
|
|
+ membership.setEndDate(expireTime);
|
|
|
|
|
+ membership.setPaymentMethod("gut_health_reward");
|
|
|
|
|
+ membership.setPaymentStatus("paid");
|
|
|
|
|
+ membership.setOrderNo("GH-" + project.getId());
|
|
|
|
|
+ membership.setAmount(0);
|
|
|
|
|
+ membership.setCreatedAt(new Date());
|
|
|
|
|
+ membership.setUpdatedAt(new Date());
|
|
|
|
|
+ familyMembershipMapper.insert(membership);
|
|
|
|
|
+
|
|
|
|
|
+ // 同步全家会员身份
|
|
|
|
|
+ try {
|
|
|
|
|
+ java.lang.reflect.Method syncMethod = MembershipService.class.getDeclaredMethod(
|
|
|
|
|
+ "syncMembershipToFamilyMembers", Long.class, String.class, Date.class, String.class, Long.class);
|
|
|
|
|
+ syncMethod.setAccessible(true);
|
|
|
|
|
+ syncMethod.invoke(membershipService, familyId, "FAMILY", expireTime, "gut_health_reward", null);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("同步家庭会员失败,将单独更新用户: {}", e.getMessage());
|
|
|
|
|
+ // 兜底:直接更新用户
|
|
|
|
|
+ User u = userMapper.selectById(userId);
|
|
|
|
|
+ if (u != null) {
|
|
|
|
|
+ u.setMemberLevel("FAMILY");
|
|
|
|
|
+ u.setMemberExpireTime(expireTime);
|
|
|
|
|
+ u.setUpdatedAt(new Date());
|
|
|
|
|
+ userMapper.updateById(u);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ project.setFamilyMembershipActivated(1);
|
|
|
|
|
+ project.setFamilyMembershipQualified(1);
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.updateById(project);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("activated", true);
|
|
|
|
|
+ result.put("levelCode", "FAMILY");
|
|
|
|
|
+ result.put("expireTime", expireTime);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 购买菌群检测后激活一年家庭会员
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 流程:用户使用会员价购买菌群检测商品 → 调用本方法 → 自动开通一年家庭会员
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Map<String, Object> activateAfterPurchase(Long userId, Long projectId) {
|
|
|
|
|
+ GutHealthProject project = getOwnedProject(userId, projectId);
|
|
|
|
|
+ if (project == null) {
|
|
|
|
|
+ throw new RuntimeException("项目不存在或无权操作");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (project.getMemberDiscountQualified() == null || project.getMemberDiscountQualified() != 1) {
|
|
|
|
|
+ throw new RuntimeException("您尚未获得会员价购买菌群检测资格,请先完成健康问卷");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (project.getFamilyMembershipActivated() != null && project.getFamilyMembershipActivated() == 1) {
|
|
|
|
|
+ throw new RuntimeException("一年家庭会员已激活,请勿重复操作");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 通过会员体系开通一年家庭会员
|
|
|
|
|
+ Long familyId = project.getFamilyId() != null ? project.getFamilyId() : getUserFamilyId(userId);
|
|
|
|
|
+ if (familyId == null) {
|
|
|
|
|
+ throw new RuntimeException("请先创建或加入家庭");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date expireTime = addYears(new Date(), 1);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyMembership membership = new FamilyMembership();
|
|
|
|
|
+ membership.setFamilyId(familyId);
|
|
|
|
|
+ membership.setLevelCode("FAMILY");
|
|
|
|
|
+ membership.setStartDate(new Date());
|
|
|
|
|
+ membership.setEndDate(expireTime);
|
|
|
|
|
+ membership.setPaymentMethod("gut_health_reward");
|
|
|
|
|
+ membership.setPaymentStatus("paid");
|
|
|
|
|
+ membership.setOrderNo("GH-" + project.getId());
|
|
|
|
|
+ membership.setAmount(0);
|
|
|
|
|
+ membership.setCreatedAt(new Date());
|
|
|
|
|
+ membership.setUpdatedAt(new Date());
|
|
|
|
|
+ familyMembershipMapper.insert(membership);
|
|
|
|
|
+
|
|
|
|
|
+ // 方案B:全家同步会员身份(通过会员体系公共入口)
|
|
|
|
|
+ try {
|
|
|
|
|
+ membershipService.setFamilyMembershipLevel(familyId, "FAMILY", expireTime, "gut_health_purchase", "菌群检测购买赠送");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("同步家庭会员失败,将单独更新用户: {}", e.getMessage());
|
|
|
|
|
+ // 兜底:直接更新用户
|
|
|
|
|
+ User u = userMapper.selectById(userId);
|
|
|
|
|
+ if (u != null) {
|
|
|
|
|
+ u.setMemberLevel("FAMILY");
|
|
|
|
|
+ u.setMemberExpireTime(expireTime);
|
|
|
|
|
+ u.setUpdatedAt(new Date());
|
|
|
|
|
+ userMapper.updateById(u);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ project.setFamilyMembershipActivated(1);
|
|
|
|
|
+ project.setUpdatedAt(new Date());
|
|
|
|
|
+ projectMapper.updateById(project);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("activated", true);
|
|
|
|
|
+ result.put("levelCode", "FAMILY");
|
|
|
|
|
+ result.put("expireTime", expireTime);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取会员价购买菌群检测凭证
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> getMicrobeDiscountVoucher(Long userId, Long projectId) {
|
|
|
|
|
+ GutHealthProject project = getOwnedProject(userId, projectId);
|
|
|
|
|
+ if (project == null) {
|
|
|
|
|
+ throw new RuntimeException("项目不存在或无权操作");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (project.getMemberDiscountQualified() == null || project.getMemberDiscountQualified() != 1) {
|
|
|
|
|
+ throw new RuntimeException("您尚未获得会员价购买资格,请先完成健康问卷");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> voucher = new HashMap<>();
|
|
|
|
|
+ voucher.put("qualified", true);
|
|
|
|
|
+ voucher.put("voucherCode", "GH-DISCOUNT-" + project.getId());
|
|
|
|
|
+ voucher.put("validUntil", addYears(new Date(), 1));
|
|
|
|
|
+ voucher.put("projectId", project.getId());
|
|
|
|
|
+ return voucher;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 风险评分与建议生成 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算各维度风险评分(0-3分)
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Integer> computeRiskScores(List<Map<String, Object>> answers) {
|
|
|
|
|
+ Map<String, Integer> scores = new LinkedHashMap<>();
|
|
|
|
|
+ List<Map<String, Object>> questions = getQuestionnaireQuestions();
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < questions.size(); i++) {
|
|
|
|
|
+ Map<String, Object> question = questions.get(i);
|
|
|
|
|
+ String dimension = (String) question.get("dimension");
|
|
|
|
|
+ int score = 0;
|
|
|
|
|
+ if (i < answers.size() && answers.get(i) != null) {
|
|
|
|
|
+ Object answer = answers.get(i).get("score") != null
|
|
|
|
|
+ ? answers.get(i).get("score")
|
|
|
|
|
+ : answers.get(i).get("answer");
|
|
|
|
|
+ if (answer instanceof Number) {
|
|
|
|
|
+ score = ((Number) answer).intValue();
|
|
|
|
|
+ } else if (answer != null) {
|
|
|
|
|
+ // 尝试按选项文本匹配
|
|
|
|
|
+ List<Map<String, Object>> options = (List<Map<String, Object>>) question.get("options");
|
|
|
|
|
+ for (Map<String, Object> opt : options) {
|
|
|
|
|
+ if (answer.toString().equals(opt.get("text"))) {
|
|
|
|
|
+ score = ((Number) opt.get("score")).intValue();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ scores.put(dimension, score);
|
|
|
|
|
+ }
|
|
|
|
|
+ return scores;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据风险评分生成健康建议
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<Map<String, Object>> generateRecommendations(Map<String, Integer> riskScores, List<Map<String, Object>> answers) {
|
|
|
|
|
+ List<Map<String, Object>> recommendations = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 睡眠
|
|
|
|
|
+ int sleep = riskScores.getOrDefault("sleep", 0);
|
|
|
|
|
+ if (sleep >= 2) {
|
|
|
|
|
+ recommendations.add(rec("睡眠", "高风险", "睡眠质量差与肠道菌群失衡密切相关,建议规律作息(23点前入睡),睡前1小时远离电子屏幕,可尝试睡前温水泡脚或冥想。若长期失眠,建议进一步评估肠道健康。"));
|
|
|
|
|
+ } else if (sleep == 1) {
|
|
|
|
|
+ recommendations.add(rec("睡眠", "中风险", "睡眠质量略有波动,建议保持固定作息,增加日间户外活动,晚餐避免过饱,观察改善情况。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 饮食
|
|
|
|
|
+ int diet = riskScores.getOrDefault("diet", 0);
|
|
|
|
|
+ if (diet >= 2) {
|
|
|
|
|
+ recommendations.add(rec("饮食", "高风险", "饮食结构偏向高油高糖或缺乏蔬果,会抑制有益菌生长。建议每日蔬菜摄入500g以上,水果200g左右,减少油炸和加工食品,增加发酵食品(酸奶、纳豆、泡菜)。"));
|
|
|
|
|
+ } else if (diet == 1) {
|
|
|
|
|
+ recommendations.add(rec("饮食", "中风险", "饮食结构基本均衡但蔬果摄入偏少,建议每天增加一份蔬菜或水果,粗细粮搭配,促进膳食纤维多样化。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 体重
|
|
|
|
|
+ int weight = riskScores.getOrDefault("weight", 0);
|
|
|
|
|
+ if (weight >= 2) {
|
|
|
|
|
+ recommendations.add(rec("体重", "高风险", "体重超标与肠道菌群多样性下降相关。建议控制精制碳水摄入,增加优质蛋白和膳食纤维,每周进行150分钟以上中等强度运动,可考虑菌群检测了解自身菌群结构。"));
|
|
|
|
|
+ } else if (weight == 1) {
|
|
|
|
|
+ recommendations.add(rec("体重", "中风险", "体重处于临界范围,建议调整饮食结构并保持规律运动,将体重控制在健康范围。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 消化
|
|
|
|
|
+ int digestion = riskScores.getOrDefault("digestion", 0);
|
|
|
|
|
+ if (digestion >= 2) {
|
|
|
|
|
+ recommendations.add(rec("消化", "高风险", "频繁腹胀、腹泻或便秘提示肠道菌群可能失衡。建议每日补充膳食纤维,喝足量水(1500-2000ml),规律三餐,避免暴饮暴食。如症状持续,强烈建议进行菌群检测归因分析。"));
|
|
|
|
|
+ } else if (digestion == 1) {
|
|
|
|
|
+ recommendations.add(rec("消化", "中风险", "消化功能偶有波动,建议记录饮食日记,找出易引起不适的食物,逐步建立规律饮食习惯。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 运动
|
|
|
|
|
+ int exercise = riskScores.getOrDefault("exercise", 0);
|
|
|
|
|
+ if (exercise >= 2) {
|
|
|
|
|
+ recommendations.add(rec("运动", "高风险", "缺乏规律运动会减少肠道菌群多样性。建议每周至少3次30分钟以上中等强度运动(快走、慢跑、游泳),可从每天15分钟开始逐步增加。"));
|
|
|
|
|
+ } else if (exercise == 1) {
|
|
|
|
|
+ recommendations.add(rec("运动", "中风险", "运动频率偏低,建议每周增加1-2次规律运动,运动后补充水分和适量蛋白质。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 压力
|
|
|
|
|
+ int stress = riskScores.getOrDefault("stress", 0);
|
|
|
|
|
+ if (stress >= 2) {
|
|
|
|
|
+ recommendations.add(rec("压力", "高风险", "长期高压通过脑-肠轴影响肠道功能。建议每天留出10-15分钟正念呼吸或冥想,培养一项放松爱好,必要时寻求专业心理支持。"));
|
|
|
|
|
+ } else if (stress == 1) {
|
|
|
|
|
+ recommendations.add(rec("压力", "中风险", "压力水平可控,建议保持现有放松习惯,增加运动释放压力。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 膳食纤维
|
|
|
|
|
+ int fiber = riskScores.getOrDefault("fiber", 0);
|
|
|
|
|
+ if (fiber >= 2) {
|
|
|
|
|
+ recommendations.add(rec("膳食纤维", "高风险", "膳食纤维摄入不足是肠道菌群失衡常见原因。建议主食增加燕麦、糙米、藜麦等全谷物,每天摄入豆类、坚果,保证纤维来源多样化。"));
|
|
|
|
|
+ } else if (fiber == 1) {
|
|
|
|
|
+ recommendations.add(rec("膳食纤维", "中风险", "全谷物比例偏低,建议逐步用粗粮替代部分精制主食,每周3-4次。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 抗生素
|
|
|
|
|
+ int antibiotic = riskScores.getOrDefault("antibiotic", 0);
|
|
|
|
|
+ if (antibiotic >= 2) {
|
|
|
|
|
+ recommendations.add(rec("抗生素使用", "高风险", "近期抗生素使用可能已影响肠道菌群平衡。建议增加益生菌和益生元摄入(酸奶、发酵食品、菊粉),均衡饮食帮助菌群恢复。"));
|
|
|
|
|
+ } else if (antibiotic == 1) {
|
|
|
|
|
+ recommendations.add(rec("抗生素使用", "中风险", "曾有抗生素使用史,建议日常增加发酵食品摄入,帮助肠道菌群维持平衡。"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (recommendations.isEmpty()) {
|
|
|
|
|
+ recommendations.add(rec("整体", "低风险", "您的各项指标总体良好,建议保持现有健康生活方式,定期关注肠道健康。"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return recommendations;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> rec(String dimension, String level, String advice) {
|
|
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
|
|
+ m.put("dimension", dimension);
|
|
|
|
|
+ m.put("level", level);
|
|
|
|
|
+ m.put("advice", advice);
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据风险评分生成一周行动任务
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<Map<String, Object>> generateActionTasks(Map<String, Integer> riskScores) {
|
|
|
|
|
+ List<Map<String, Object>> tasks = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 1: 睡眠调整
|
|
|
|
|
+ if (riskScores.getOrDefault("sleep", 0) >= 1) {
|
|
|
|
|
+ tasks.add(task("每晚23:00前入睡,记录实际入睡时间", "sleep", "改善睡眠-肠道节律"));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tasks.add(task("保持现有作息,记录一周睡眠时长", "sleep", "巩固良好睡眠习惯"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 2: 饮食调整
|
|
|
|
|
+ if (riskScores.getOrDefault("diet", 0) >= 1) {
|
|
|
|
|
+ tasks.add(task("每天增加一份蔬菜(至少500g),减少油炸食品", "diet", "优化饮食结构"));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tasks.add(task("保持均衡饮食,每天记录三餐构成", "diet", "维持健康饮食"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 3: 运动
|
|
|
|
|
+ if (riskScores.getOrDefault("exercise", 0) >= 1) {
|
|
|
|
|
+ tasks.add(task("每天快走或慢跑20-30分钟", "exercise", "提升菌群多样性"));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tasks.add(task("每周3次30分钟运动(跑步/游泳/健身操)", "exercise", "维持菌群多样性"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 4: 膳食纤维
|
|
|
|
|
+ if (riskScores.getOrDefault("fiber", 0) >= 1) {
|
|
|
|
|
+ tasks.add(task("每天摄入一种全谷物或粗粮(燕麦/糙米/藜麦)", "fiber", "补充膳食纤维"));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tasks.add(task("保持全谷物摄入,记录每日粗粮种类", "fiber", "维持纤维摄入"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 5: 饮水
|
|
|
|
|
+ tasks.add(task("每天饮水1500-2000ml,记录饮水量", "hydration", "促进肠道蠕动"));
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 6: 压力管理
|
|
|
|
|
+ if (riskScores.getOrDefault("stress", 0) >= 1) {
|
|
|
|
|
+ tasks.add(task("每天10分钟正念呼吸或冥想", "stress", "调节脑-肠轴"));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tasks.add(task("每天10分钟放松活动(听音乐/散步/阅读)", "stress", "保持轻松状态"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 任务 7: 消化观察
|
|
|
|
|
+ if (riskScores.getOrDefault("digestion", 0) >= 1) {
|
|
|
|
|
+ tasks.add(task("记录每日排便情况和腹胀感受", "digestion", "观察消化功能变化"));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ tasks.add(task("记录一周消化状况,留意异常信号", "digestion", "持续关注消化健康"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return tasks;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> task(String content, String category, String goal) {
|
|
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
|
|
+ m.put("content", content);
|
|
|
|
|
+ m.put("category", category);
|
|
|
|
|
+ m.put("goal", goal);
|
|
|
|
|
+ m.put("done", false);
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 辅助方法 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private GutHealthProject getLatestProject(Long userId) {
|
|
|
|
|
+ return projectMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<GutHealthProject>()
|
|
|
|
|
+ .eq(GutHealthProject::getUserId, userId)
|
|
|
|
|
+ .orderByDesc(GutHealthProject::getId)
|
|
|
|
|
+ .last("LIMIT 1"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private GutHealthProject getOwnedProject(Long userId, Long projectId) {
|
|
|
|
|
+ if (projectId == null) return null;
|
|
|
|
|
+ GutHealthProject project = projectMapper.selectById(projectId);
|
|
|
|
|
+ if (project == null || !userId.equals(project.getUserId())) return null;
|
|
|
|
|
+ return project;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int getProjectPrice() {
|
|
|
|
|
+ String priceStr = sysConfigService.getValue("gut_health_project_price");
|
|
|
|
|
+ if (priceStr != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Integer.parseInt(priceStr);
|
|
|
|
|
+ } catch (NumberFormatException ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return DEFAULT_PRICE;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long getUserFamilyId(Long userId) {
|
|
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
|
|
+ return user != null ? user.getFamilyId() : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date addYears(Date date, int years) {
|
|
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
|
|
+ cal.setTime(date);
|
|
|
|
|
+ cal.add(Calendar.YEAR, years);
|
|
|
|
|
+ return cal.getTime();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> toProjectVO(GutHealthProject project) {
|
|
|
|
|
+ Map<String, Object> vo = new LinkedHashMap<>();
|
|
|
|
|
+ vo.put("id", project.getId());
|
|
|
|
|
+ vo.put("userId", project.getUserId());
|
|
|
|
|
+ vo.put("status", project.getStatus());
|
|
|
|
|
+ vo.put("paidAt", project.getPaidAt());
|
|
|
|
|
+ vo.put("completedAt", project.getCompletedAt());
|
|
|
|
|
+ vo.put("microbeTestRecommended", project.getMicrobeTestRecommended());
|
|
|
|
|
+ vo.put("memberDiscountQualified", project.getMemberDiscountQualified());
|
|
|
|
|
+ vo.put("familyMembershipQualified", project.getFamilyMembershipQualified());
|
|
|
|
|
+ vo.put("familyMembershipActivated", project.getFamilyMembershipActivated());
|
|
|
|
|
+ vo.put("cfRefundAmount", project.getCfRefundAmount());
|
|
|
|
|
+ vo.put("cfRefunded", project.getCfRefunded());
|
|
|
|
|
+ vo.put("completedTasksCount", project.getCompletedTasksCount());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String safeJson(Object obj) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new ObjectMapper().writeValueAsString(obj);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return obj.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map<String, Object>> parseJsonList(String json) {
|
|
|
|
|
+ if (json == null || json.isEmpty()) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new ObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<List<Map<String, Object>>>() {});
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return new ArrayList<>();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|