Browse Source

feat: 家庭能量/积分/CF值变更通知 + 消息历史中心

- 首页新增能量/积分/CF值变更通知条(点击清除)
- 新增消息历史页面支持筛选查看全部/能量/积分/CF值/系统
- 后端 EnergyService/PointsService/PlatformPointsService 集成通知发送
- 新增 energy_change/points_change/cf_change 通知模板种子数据
- ProfileMenu 新增消息历史入口
iwt 1 day ago
parent
commit
dd01604f81

+ 4 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -9388,7 +9388,10 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
                 jdbcTemplate.execute("INSERT INTO notification_templates (type, title, content, channels) VALUES " +
                 jdbcTemplate.execute("INSERT INTO notification_templates (type, title, content, channels) VALUES " +
                         "('report_ready', '健康报告已完成分析', '您的健康报告已完成解析,可前往查看结果与成长方案。', 'in_app,sms'), " +
                         "('report_ready', '健康报告已完成分析', '您的健康报告已完成解析,可前往查看结果与成长方案。', 'in_app,sms'), " +
                         "('reward_granted', '问题域完成奖励已到账', '恭喜完成「{domainName}」全部任务,奖励 {reward} CF 已发放至家庭账户。', 'in_app,sms,wechat'), " +
                         "('reward_granted', '问题域完成奖励已到账', '恭喜完成「{domainName}」全部任务,奖励 {reward} CF 已发放至家庭账户。', 'in_app,sms,wechat'), " +
-                        "('system', '系统通知', '您有一条新的系统通知,请前往查看。', 'in_app')");
+                        "('system', '系统通知', '您有一条新的系统通知,请前往查看。', 'in_app'), " +
+                        "('energy_change', '能量变动', '{description}获得{amount}能量', 'in_app'), " +
+                        "('points_change', '积分变动', '{description}变动{amount}积分', 'in_app'), " +
+                        "('cf_change', 'CF值变动', '{remark}获得{amount} CF值', 'in_app')");
                 log.info("notification_templates 种子数据已插入");
                 log.info("notification_templates 种子数据已插入");
             } else {
             } else {
                 log.info("notification_templates 已有 {} 条数据,跳过种子插入", tplCount);
                 log.info("notification_templates 已有 {} 条数据,跳过种子插入", tplCount);

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/UserNotification.java

@@ -11,6 +11,7 @@ import java.util.Date;
 /**
 /**
  * 站内通知(改版v1,用户级主动通知)
  * 站内通知(改版v1,用户级主动通知)
  * type: report_ready=报告完成 / reward_granted=奖励到账 / logistics=物流变化 / system=系统
  * type: report_ready=报告完成 / reward_granted=奖励到账 / logistics=物流变化 / system=系统
+ *       energy_change=能量变动 / points_change=积分变动 / cf_change=CF值变动
  * 与 notices(平台公告) 并存:本表按 user_id 定向推送
  * 与 notices(平台公告) 并存:本表按 user_id 定向推送
  */
  */
 @Data
 @Data

+ 78 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -18,6 +18,7 @@ import javax.annotation.Resource;
 import java.util.*;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.SortUtil;
+import com.etotem.cfc.service.NotificationDispatchService;
 
 
 /**
 /**
  * 家庭能量沙盘 — 核心计算服务
  * 家庭能量沙盘 — 核心计算服务
@@ -34,6 +35,9 @@ public class EnergyService {
     @Resource
     @Resource
     private FamilyMemberMapper familyMemberMapper;
     private FamilyMemberMapper familyMemberMapper;
 
 
+    @Resource
+    private NotificationDispatchService notificationDispatchService;
+
     @Resource
     @Resource
     private TaskMapper taskMapper;
     private TaskMapper taskMapper;
 
 
@@ -1551,6 +1555,8 @@ public class EnergyService {
             EnergyDimension dim = getDimById(entry.getKey());
             EnergyDimension dim = getDimById(entry.getKey());
             if (dim != null) result.put(dim.getCode(), entry.getValue());
             if (dim != null) result.put(dim.getCode(), entry.getValue());
         }
         }
+        // 发送能量变更通知
+        try { sendEnergyNotification(memberId, sourceType, totalAmount, description); } catch (Exception e) { log.warn("energy change notification failed", e); }
         return result;
         return result;
     }
     }
 
 
@@ -1654,7 +1660,8 @@ public class EnergyService {
         elog.setDescription(reason);
         elog.setDescription(reason);
         elog.setCreatedAt(new Date());
         elog.setCreatedAt(new Date());
         energyLogMapper.insert(elog);
         energyLogMapper.insert(elog);
-
+        // 发送能量扣除通知
+        try { sendEnergyDeductionNotification(memberId, sourceType != null ? sourceType : "manual", amount, reason); } catch (Exception e) { log.warn("energy deduction notification failed", e); }
         return balance.getBalance();
         return balance.getBalance();
     }
     }
 
 
@@ -1837,6 +1844,76 @@ public class EnergyService {
         return result != -1;
         return result != -1;
     }
     }
 
 
+    /**
+     * 发送能量变更站内通知
+     */
+    private void sendEnergyNotification(Long memberId, String sourceType, Integer amount, String description) {
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        Long userId = member != null ? member.getUserId() : null;
+        if (userId == null) return;
+
+        String title = "";
+        String content = "";
+        switch (sourceType) {
+            case "task":
+                title = "任务完成获得能量";
+                content = String.format("完成了%s获得%d能量", description != null ? description : "任务", amount);
+                break;
+            case "product":
+                title = "购买商品扣除能量";
+                content = String.format("购买了%s扣除%d能量", description != null ? description : "商品", Math.abs(amount));
+                break;
+            case "medical_report":
+                title = "上传体检报告获得能量";
+                content = String.format("上传了%s获得%d能量", description != null ? description : "体检报告", amount);
+                break;
+            case "daily_record":
+                title = "日常记录获得能量";
+                content = String.format("完成了日常记录获得%d能量", amount);
+                break;
+            default:
+                title = "能量变动";
+                content = String.format("能量变动%d", amount);
+        }
+
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("amount", amount > 0 ? "+" : "") + amount;
+        vars.put("source", sourceType);
+        vars.put("description", description);
+        notificationDispatchService.sendByTemplate("energy_change", userId, vars, "energyLog", memberId, title, content);
+    }
+
+    /**
+     * 发送能量扣除站内通知
+     */
+    private void sendEnergyDeductionNotification(Long memberId, String sourceType, Integer amount, String description) {
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        Long userId = member != null ? member.getUserId() : null;
+        if (userId == null) return;
+
+        String title = "";
+        String content = "";
+        switch (sourceType) {
+            case "task":
+                title = "任务完成获得能量";
+                content = String.format("完成了%s获得%d能量", description != null ? description : "任务", amount);
+                break;
+            case "wish":
+                title = "心愿兑换扣除能量";
+                content = String.format("完成了心愿兑换扣除%d能量", Math.abs(amount));
+                break;
+            default:
+                title = "能量扣除";
+                content = String.format("能量扣除%d", Math.abs(amount));
+        }
+
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("amount", amount > 0 ? "+" : "") + amount;
+        vars.put("source", sourceType);
+        vars.put("description", description);
+        notificationDispatchService.sendByTemplate("energy_deduction", userId, vars, "energyLog", memberId, title, content);
+    }
+
     // ==================== 辅助方法 ====================
     // ==================== 辅助方法 ====================
 
 
     /**
     /**

+ 32 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java

@@ -12,12 +12,15 @@ import com.etotem.cfc.mapper.UserPlatformBalanceMapper;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
+import lombok.extern.slf4j.Slf4j;
+import com.etotem.cfc.service.NotificationDispatchService;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
+@Slf4j
 @Service
 @Service
 public class PlatformPointsService {
 public class PlatformPointsService {
 
 
@@ -36,6 +39,9 @@ public class PlatformPointsService {
     @Resource
     @Resource
     private UserMapper userMapper;
     private UserMapper userMapper;
 
 
+    @Resource
+    private NotificationDispatchService notificationDispatchService;
+
     /** 获取或创建用户的 CF 值余额记录 */
     /** 获取或创建用户的 CF 值余额记录 */
     private UserPlatformBalance getOrCreate(Long userId) {
     private UserPlatformBalance getOrCreate(Long userId) {
         UserPlatformBalance balance = balanceMapper.selectOne(
         UserPlatformBalance balance = balanceMapper.selectOne(
@@ -109,6 +115,7 @@ public class PlatformPointsService {
         try {
         try {
             writeLog(userId, b.getFamilyId(), TYPE_EARN, amount, safeInt(b.getAvailable()),
             writeLog(userId, b.getFamilyId(), TYPE_EARN, amount, safeInt(b.getAvailable()),
                     refType, refId, remark);
                     refType, refId, remark);
+            sendCfNotification(userId, TYPE_EARN, amount, remark);
         } catch (DuplicateKeyException e) {
         } catch (DuplicateKeyException e) {
             throw new RuntimeException("重复发放拦截: userId=" + userId + ", refType=" + refType + ", refId=" + refId, e);
             throw new RuntimeException("重复发放拦截: userId=" + userId + ", refType=" + refType + ", refId=" + refId, e);
         }
         }
@@ -131,6 +138,7 @@ public class PlatformPointsService {
         UserPlatformBalance b = getOrCreate(userId);
         UserPlatformBalance b = getOrCreate(userId);
         writeLog(userId, b.getFamilyId(), TYPE_SPEND, amount, safeInt(b.getAvailable()),
         writeLog(userId, b.getFamilyId(), TYPE_SPEND, amount, safeInt(b.getAvailable()),
                 refType, refId, remark);
                 refType, refId, remark);
+        sendCfNotification(userId, TYPE_SPEND, amount, remark);
     }
     }
 
 
     /** 提现申请:可用 → 冻结 */
     /** 提现申请:可用 → 冻结 */
@@ -216,4 +224,28 @@ public class PlatformPointsService {
                         .eq(PlatformBalanceLog::getUserId, userId)
                         .eq(PlatformBalanceLog::getUserId, userId)
                         .orderByDesc(PlatformBalanceLog::getCreatedAt));
                         .orderByDesc(PlatformBalanceLog::getCreatedAt));
     }
     }
+
+    /** 发送 CF 值变更站内通知 */
+    private void sendCfNotification(Long userId, String type, int amount, String remark) {
+        try {
+            String title, content;
+            if (TYPE_EARN.equals(type)) {
+                title = "获得CF值";
+                content = String.format("%s获得%d CF值", remark != null ? remark + " " : "", amount);
+            } else if (TYPE_SPEND.equals(type)) {
+                title = "CF值支出";
+                content = String.format("%s支出%d CF值", remark != null ? remark : "消费", amount);
+            } else {
+                title = "CF值变动";
+                content = String.format("CF值变动%d", amount);
+            }
+            Map<String, Object> vars = new HashMap<>();
+            vars.put("amount", amount);
+            vars.put("type", type);
+            vars.put("remark", remark);
+            notificationDispatchService.sendByTemplate("cf_change", userId, vars, "platformBalanceLog", null, title, content);
+        } catch (Exception e) {
+            log.warn("cf notification failed userId={}, error={}", userId, e.getMessage());
+        }
+    }
 }
 }

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.java

@@ -10,6 +10,7 @@ import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.NotificationDispatchService;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
@@ -18,7 +19,11 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.SortUtil;
+import lombok.extern.slf4j.Slf4j;
 
 
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
 @Service
 @Service
 public class PointsService implements PointsServiceInterface {
 public class PointsService implements PointsServiceInterface {
 
 
@@ -31,6 +36,9 @@ public class PointsService implements PointsServiceInterface {
     @Resource
     @Resource
     private UserMapper userMapper;
     private UserMapper userMapper;
 
 
+    @Resource
+    private NotificationDispatchService notificationDispatchService;
+
     public Map<String, Object> getBalance(Long memberId) {
     public Map<String, Object> getBalance(Long memberId) {
         FamilyMember child = familyMemberMapper.selectById(memberId);
         FamilyMember child = familyMemberMapper.selectById(memberId);
         if (child == null) {
         if (child == null) {
@@ -88,6 +96,7 @@ public class PointsService implements PointsServiceInterface {
         log.setDescription(reason);
         log.setDescription(reason);
         log.setCreatedAt(new Date());
         log.setCreatedAt(new Date());
         pointsLogMapper.insert(log);
         pointsLogMapper.insert(log);
+        try { sendPointsNotification(memberId, "adjust", amount, reason); } catch (Exception e) { log.warn("points notification failed", e); }
 
 
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
         result.put("newBalance", newBalance);
         result.put("newBalance", newBalance);
@@ -120,6 +129,7 @@ public class PointsService implements PointsServiceInterface {
         log.setDescription(reason);
         log.setDescription(reason);
         log.setCreatedAt(new Date());
         log.setCreatedAt(new Date());
         pointsLogMapper.insert(log);
         pointsLogMapper.insert(log);
+        try { sendPointsNotification(memberId, "cf_earn", amount, reason); } catch (Exception e) { log.warn("cf notification failed", e); }
 
 
         return newCfPoints;
         return newCfPoints;
     }
     }
@@ -177,6 +187,7 @@ public class PointsService implements PointsServiceInterface {
         log.setDescription(reason);
         log.setDescription(reason);
         log.setCreatedAt(new Date());
         log.setCreatedAt(new Date());
         pointsLogMapper.insert(log);
         pointsLogMapper.insert(log);
+        try { sendPointsNotification(memberId, "cf_spend", -amount, reason); } catch (Exception e) { log.warn("cf deduction notification failed", e); }
 
 
         return newCfPoints;
         return newCfPoints;
     }
     }
@@ -243,4 +254,32 @@ public class PointsService implements PointsServiceInterface {
         user.setUpdatedAt(new Date());
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         userMapper.updateById(user);
     }
     }
+
+    /** 发送积分变更站内通知 */
+    private void sendPointsNotification(Long memberId, String type, Integer amount, String description) {
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        Long userId = member != null ? member.getUserId() : null;
+        if (userId == null) return;
+
+        String title, content;
+        if ("adjust".equals(type)) {
+            title = "积分调整";
+            content = String.format("管理员调整积分 %d", amount);
+        } else if ("cf_earn".equals(type)) {
+            title = "获得CF值";
+            content = String.format("%s获得%dCF值", description != null ? description + " " : "", amount);
+        } else if ("cf_spend".equals(type)) {
+            title = "CF值扣除";
+            content = String.format("%s扣除%dCF值", description != null ? description : "消费", Math.abs(amount));
+        } else {
+            title = "积分变动";
+            content = String.format("积分变动%d", amount);
+        }
+
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("amount", amount > 0 ? "+" : "") + amount;
+        vars.put("type", type);
+        vars.put("description", description);
+        notificationDispatchService.sendByTemplate("points_change", userId, vars, "pointsLog", memberId, title, content);
+    }
 }
 }

+ 1 - 1
cfc-backend/src/main/resources/schema.sql

@@ -5221,7 +5221,7 @@ CREATE TABLE IF NOT EXISTS user_intents (
 CREATE TABLE IF NOT EXISTS user_notifications (
 CREATE TABLE IF NOT EXISTS user_notifications (
   id BIGINT AUTO_INCREMENT PRIMARY KEY,
   id BIGINT AUTO_INCREMENT PRIMARY KEY,
   user_id BIGINT NOT NULL COMMENT '接收用户ID',
   user_id BIGINT NOT NULL COMMENT '接收用户ID',
-  type VARCHAR(30) DEFAULT 'report_ready' COMMENT 'report_ready/reward_granted/logistics/system',
+  type VARCHAR(30) DEFAULT 'report_ready' COMMENT 'report_ready/reward_granted/logistics/system/energy_change/points_change/cf_change',
   title VARCHAR(200) NOT NULL,
   title VARCHAR(200) NOT NULL,
   content VARCHAR(1000) DEFAULT '',
   content VARCHAR(1000) DEFAULT '',
   ref_type VARCHAR(30) DEFAULT '' COMMENT '关联对象类型: report/package/task',
   ref_type VARCHAR(30) DEFAULT '' COMMENT '关联对象类型: report/package/task',

+ 2 - 2
cfc-frontend/pages.json

@@ -1804,9 +1804,9 @@
           }
           }
         },
         },
         {
         {
-          "path": "share",
+          "path": "message-history",
           "style": {
           "style": {
-            "navigationBarTitleText": "分享推广码"
+            "navigationBarTitleText": "消息历史"
           }
           }
         }
         }
       ]
       ]

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

@@ -103,6 +103,14 @@
       :notices="notices"
       :notices="notices"
       :showHeader="true" />
       :showHeader="true" />
 
 
+    <!-- 能量/积分/CF值变更通知条 -->
+    <view v-if="showEnergyBadge || showPointsBadge || showCfBadge" class="change-notification-bar" @click="clearChangeNotifications">
+      <text v-if="showEnergyBadge" class="change-item energy">{{ formatEnergyNotification }}</text>
+      <text v-if="showPointsBadge" class="change-item points">{{ formatPointsNotification }}</text>
+      <text v-if="showCfBadge" class="change-item cf">{{ formatCfNotification }}</text>
+      <text class="change-dismiss">✕</text>
+    </view>
+
     <!-- ④ 内容区 — 推荐文章 -->
     <!-- ④ 内容区 — 推荐文章 -->
 
 
     <view v-if="showMemberEntry" class="member-entry" @click="goMembership">
     <view v-if="showMemberEntry" class="member-entry" @click="goMembership">
@@ -422,7 +430,7 @@ import ReportUploadCard from '../../components/report-upload-card.vue'
 import OnboardingProgressBar from '../../components/OnboardingProgressBar.vue'
 import OnboardingProgressBar from '../../components/OnboardingProgressBar.vue'
 import OnboardingGuide from '../../components/OnboardingGuide.vue'
 import OnboardingGuide from '../../components/OnboardingGuide.vue'
 import ConfigurableGuide from '../../components/ConfigurableGuide.vue'
 import ConfigurableGuide from '../../components/ConfigurableGuide.vue'
-import { acceptParentInvite, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChallengeList, getGuideConfig, getGuidePopupList, consumeGuidePopup, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus, getSelfCheckStatus, getReferralCode, extractReferralCode, getAppLibrary, reportAppEnter } from '../../utils/api.js'
+import { acceptParentInvite, getActivityList, getFeaturedArticles, getEnergySandbox, getEnergyOverview, getFamilyMemberList, getChallengeList, getGuideConfig, getGuidePopupList, consumeGuidePopup, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getNotificationList, getSurveyStatus, getSelfCheckStatus, getReferralCode, extractReferralCode, getAppLibrary, reportAppEnter, getEnergyLogs, getPointsLogs, getCfList, getCfSummary } from '../../utils/api.js'
 import { findAppByKey, getShortcutableApps } from '../../utils/app-library.js'
 import { findAppByKey, getShortcutableApps } from '../../utils/app-library.js'
 import nav from '../../utils/nav.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
 import config from '@/config.js'
@@ -517,7 +525,15 @@ membershipDays: 0,
       quickApps: [],
       quickApps: [],
       myShortcuts: [],
       myShortcuts: [],
       appUsage: {},
       appUsage: {},
-      quickLoaded: false
+      quickLoaded: false,
+
+      // 能量/积分/CF值变更通知
+      energyMessages: [],
+      pointsMessages: [],
+      cfMessages: [],
+      showEnergyBadge: false,
+      showPointsBadge: false,
+      showCfBadge: false
 
 
 
 
     }
     }
@@ -674,6 +690,31 @@ title: '添加家庭成员',
         btn: '逛一逛',
         btn: '逛一逛',
         color: 'gray'
         color: 'gray'
       }
       }
+    },
+    // ===== 能量/积分/CF值变更通知格式化 =====
+    formatEnergyNotification: function() {
+      var texts = []
+      var arr = this.energyMessages || []
+      for (var i = 0; i < arr.length; i++) {
+        texts.push(arr[i].description)
+      }
+      return '⚡ ' + (texts.join(';') || '能量变动')
+    },
+    formatPointsNotification: function() {
+      var texts = []
+      var arr = this.pointsMessages || []
+      for (var i = 0; i < arr.length; i++) {
+        texts.push(arr[i].description)
+      }
+      return '🪙 ' + (texts.join(';') || '积分变动')
+    },
+    formatCfNotification: function() {
+      var texts = []
+      var arr = this.cfMessages || []
+      for (var i = 0; i < arr.length; i++) {
+        texts.push(arr[i].description)
+      }
+      return '💰 ' + (texts.join(';') || 'CF值变动')
     }
     }
   },
   },
   onLoad(options) {
   onLoad(options) {
@@ -851,6 +892,7 @@ title: '添加家庭成员',
       this.loadFamilyData()
       this.loadFamilyData()
       this.loadReferralCode()
       this.loadReferralCode()
       this.loadQuickApps()
       this.loadQuickApps()
+      this.loadEnergyPointsCFChanges()
     },
     },
     // 合并原 loadFamilyMembers + loadChildren:仅调用一次 getFamilyMemberList
     // 合并原 loadFamilyMembers + loadChildren:仅调用一次 getFamilyMemberList
     loadFamilyData: function() {
     loadFamilyData: function() {
@@ -1092,6 +1134,282 @@ title: '添加家庭成员',
         }
         }
       }).catch(function() {})
       }).catch(function() {})
     },
     },
+    }).catch(function() {})
+    },
+    // ===== 能量/积分/CF值变更通知 =====
+    loadEnergyPointsCFChanges: function() {
+      var self = this
+      var memberId = this.currentChildId || uni.getStorageSync('currentMemberId')
+      
+      if (!memberId) {
+        self.energyMessages = []
+        self.pointsMessages = []
+        self.cfMessages = []
+        self.showEnergyBadge = false
+        self.showPointsBadge = false
+        self.showCfBadge = false
+        return
+      }
+      
+      // 并行加载能量、积分、CF值变更
+      Promise.all([
+        this.loadEnergyChanges(),
+        this.loadPointsChanges(),
+        this.loadCfChanges()
+      ]).then(function(results) {
+        var energyRes = results[0]
+        var pointsRes = results[1]
+        var cfRes = results[2]
+        
+        // 处理能量变更
+        if (energyRes) {
+          self.energyMessages = energyRes.messages
+          self.showEnergyBadge = energyRes.showBadge
+        }
+        
+        // 处理积分变更
+        if (pointsRes) {
+          self.pointsMessages = pointsRes.messages
+          self.showPointsBadge = pointsRes.showBadge
+        }
+        
+        // 处理CF值变更
+        if (cfRes) {
+          self.cfMessages = cfRes.messages
+          self.showCfBadge = cfRes.showBadge
+        }
+      }).catch(function() {
+        self.energyMessages = []
+        self.pointsMessages = []
+        self.cfMessages = []
+        self.showEnergyBadge = false
+        self.showPointsBadge = false
+        self.showCfBadge = false
+      })
+    },
+    
+    loadEnergyChanges: function() {
+      var self = this
+      var memberId = this.currentChildId || uni.getStorageSync('currentMemberId')
+      
+      if (!memberId) return
+      
+      getEnergyLogs(memberId, null, 1, 10).then(function(res) {
+        if (res.code === 200 && res.data && res.data.records) {
+          var records = res.data.records
+          var messages = []
+          var totalAmount = 0
+          var count = 0
+          
+          for (var i = 0; i < records.length; i++) {
+            var record = records[i]
+            if (record.amount && record.createdAt) {
+              var date = self.parseDate(record.createdAt)
+              if (date && self.isToday(date)) {
+                totalAmount += record.amount
+                count++
+                
+                // 解析具体内容
+                var desc = record.description || ''
+                var sourceType = record.sourceType || ''
+                var contentText = ''
+                
+                switch(sourceType) {
+                  case 'task':
+                    contentText = '完成了' + (desc || '任务')
+                    break
+                  case 'product':
+                    contentText = '购买了' + (desc || '商品')
+                    break
+                  case 'medical_report':
+                    contentText = '上传了' + (desc || '体检报告')
+                    break
+                  case 'daily_record':
+                    contentText = '完成了' + (desc || '日常记录')
+                    break
+                  default:
+                    contentText = desc || '能量变动'
+                }
+                
+                messages.push({
+                  id: 'energy-' + record.id,
+                  type: 'energy',
+                  title: '能量变动',
+                  description: contentText,
+                  amount: record.amount,
+                  isRead: true,
+                  createdAt: record.createdAt,
+                  time: self.formatTime(record.createdAt)
+                })
+              }
+            }
+          }
+          
+          // 存储到实例属性(供其他方法使用,或可单独暴露)
+          // 注意:这里的 messages 仅在该方法调用后有效,实际建议使用单独的数据存储
+          self.energyMessages = messages
+          self.energyChangeTotal = totalAmount
+          self.energyChangeCount = count
+          self.showEnergyBadge = count > 0
+        }
+      }).catch(function() {})
+    },
+    
+    loadPointsChanges: function() {
+      var self = this
+      var memberId = this.currentChildId || uni.getStorageSync('currentMemberId')
+      
+      if (!memberId) return
+      
+      getPointsLogs(memberId, 1, 10).then(function(res) {
+        if (res.code === 200 && res.data && res.data.records) {
+          var records = res.data.records
+          var messages = []
+          var totalAmount = 0
+          var count = 0
+          
+          for (var i = 0; i < records.length; i++) {
+            var record = records[i]
+            if (record.amount && record.createdAt) {
+              var date = self.parseDate(record.createdAt)
+              if (date && self.isToday(date)) {
+                totalAmount += record.amount
+                count++
+                
+                // 解析具体内容
+                var desc = record.description || ''
+                var type = record.type || ''
+                
+                var contentText = ''
+                switch(type) {
+                  case 'task':
+                    contentText = '完成了' + (desc || '任务')
+                    contentText += '获得' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'
+                    break
+                  case 'reward':
+                    contentText = '完成了' + (desc || '心愿兑换')
+                    contentText += '获得' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'
+                    break
+                  case 'adjust':
+                    contentText = '管理员调整' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'
+                    break
+                  case 'expire':
+                    contentText = '积分过期扣除' + Math.abs(record.amount) + '积分'
+                    break
+                  default:
+                    contentText = (desc || '积分变动') + (record.amount >= 0 ? '获得' : '扣除') + Math.abs(record.amount) + '积分'
+                }
+                
+                messages.push({
+                  id: 'points-' + record.id,
+                  type: 'points',
+                  title: '积分变动',
+                  description: contentText,
+                  amount: record.amount,
+                  isRead: true,
+                  createdAt: record.createdAt,
+                  time: self.formatTime(record.createdAt)
+                })
+              }
+            }
+          }
+          
+          self.pointsMessages = messages
+          self.pointsChangeTotal = totalAmount
+          self.pointsChangeCount = count
+          self.showPointsBadge = count > 0
+        }
+      }).catch(function() {})
+    },
+    
+    loadCfChanges: function() {
+      var self = this
+      
+      getCfSummary().then(function(res) {
+        if (res.code === 200 && res.data) {
+          var todayStr = new Date().toISOString().substring(0, 10)
+          getCfList(1, 10).then(function(cfRes) {
+            if (cfRes.code === 200 && cfRes.data && cfRes.data.records) {
+              var records = cfRes.data.records
+              var messages = []
+              var totalAmount = 0
+              var count = 0
+              
+              for (var i = 0; i < records.length; i++) {
+                var record = records[i]
+                if (record.amount && record.createdAt) {
+                  var date = self.parseDate(record.createdAt)
+                  if (date && date.toISOString().substring(0, 10) === todayStr) {
+                    totalAmount += record.amount
+                    count++
+                    
+                    // 解析具体内容
+                    var desc = record.description || ''
+                    var action = record.action || ''
+                    
+                    var contentText = ''
+                    switch(action) {
+                      case 'transfer_send':
+                        contentText = 'CF值转出' + Math.abs(record.amount) + 'CF'
+                        break
+                      case 'transfer_receive':
+                        contentText = 'CF值转入' + record.amount + 'CF'
+                        break
+                      case 'commission':
+                        contentText = '获得' + record.amount + 'CF分佣'
+                        break
+                      default:
+                        contentText = (desc || 'CF值变动') + (record.amount >= 0 ? '+' : '') + record.amount + 'CF'
+                    }
+                    
+                    messages.push({
+                      id: 'cf-' + record.id,
+                      type: 'cf',
+                      title: 'CF值变动',
+                      description: contentText,
+                      amount: record.amount,
+                      isRead: true,
+                      createdAt: record.createdAt,
+                      time: self.formatTime(record.createdAt)
+                    })
+                  }
+                }
+              }
+              
+              self.cfMessages = messages
+              self.cfChangeTotal = totalAmount
+              self.cfChangeCount = count
+              self.showCfBadge = count > 0
+            }
+          }).catch(function() {})
+        }
+      }).catch(function() {})
+    },
+    
+    // 辅助方法:解析日期
+    parseDate: function(dateStr) {
+      if (!dateStr) return null
+      // 统一日期格式处理
+      var d = new Date(dateStr)
+      if (isNaN(d)) return null
+      return d
+    },
+    
+    isToday: function(date) {
+      var today = new Date()
+      return date.getDate() === today.getDate() &&
+             date.getMonth() === today.getMonth() &&
+             date.getFullYear() === today.getFullYear()
+    },
+    
+    clearChangeNotifications: function() {
+      this.showEnergyBadge = false
+      this.showPointsBadge = false
+      this.showCfBadge = false
+      this.energyMessages = []
+      this.pointsMessages = []
+      this.cfMessages = []
+    },
     quickCompleteTask: function(task) {
     quickCompleteTask: function(task) {
       var self = this
       var self = this
       var memberId = (task && task.familyMemberId) || self.currentChildId || uni.getStorageSync('currentMemberId')
       var memberId = (task && task.familyMemberId) || self.currentChildId || uni.getStorageSync('currentMemberId')
@@ -2506,4 +2824,34 @@ title: '添加家庭成员',
 }
 }
 .self-test-badge { display: inline-block; align-self: flex-start; margin-top: 8rpx; font-size: 20rpx; color: #F97316; background: rgba(249,115,22,0.1); padding: 4rpx 14rpx; border-radius: 999rpx; }
 .self-test-badge { display: inline-block; align-self: flex-start; margin-top: 8rpx; font-size: 20rpx; color: #F97316; background: rgba(249,115,22,0.1); padding: 4rpx 14rpx; border-radius: 999rpx; }
 
 
+/* ===== 能量/积分/CF值变更通知条 ===== */
+.change-notification-bar {
+  margin: 16rpx 30rpx;
+  padding: 16rpx 24rpx;
+  background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
+  border-radius: 16rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(249,115,22,0.12);
+  flex-wrap: wrap;
+}
+.change-item {
+  font-size: 22rpx;
+  font-weight: 500;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.change-item.energy { color: #F97316; }
+.change-item.points { color: #10B981; }
+.change-item.cf { color: #6366F1; }
+.change-dismiss {
+  margin-left: auto;
+  font-size: 24rpx;
+  color: #999;
+  flex-shrink: 0;
+}
 </style>
 </style>

+ 7 - 0
cfc-frontend/pages/profile/components/ProfileMenu.vue

@@ -4,6 +4,10 @@
       <!-- ===== 个人 ===== -->
       <!-- ===== 个人 ===== -->
       <view class="menu-group">
       <view class="menu-group">
         <view class="menu-group-title">── 个人 ──</view>
         <view class="menu-group-title">── 个人 ──</view>
+        <view class="menu-item" @click="goToMessageHistory">
+          <text>📨 消息历史</text>
+          <text class="arrow">›</text>
+        </view>
         <view class="menu-item" v-if="role === 'parent'" @click="goToFamilyMembers">
         <view class="menu-item" v-if="role === 'parent'" @click="goToFamilyMembers">
           <text>👨‍👩‍👧‍👦 家庭成员</text>
           <text>👨‍👩‍👧‍👦 家庭成员</text>
           <text class="arrow">›</text>
           <text class="arrow">›</text>
@@ -187,6 +191,9 @@ var res = await getReferralCode()
     goToDailyTasks() {
     goToDailyTasks() {
       uni.navigateTo({ url: '/pages/tasks/daily-tasks' })
       uni.navigateTo({ url: '/pages/tasks/daily-tasks' })
     },
     },
+    goToMessageHistory() {
+      uni.navigateTo({ url: '/pages/profile/message-history' })
+    },
     async onShowToFamilyChange(e) {
     async onShowToFamilyChange(e) {
       const val = e.detail.value ? 1 : 0
       const val = e.detail.value ? 1 : 0
       const uid = this.$store.state.userId || uni.getStorageSync('userId')
       const uid = this.$store.state.userId || uni.getStorageSync('userId')

+ 688 - 0
cfc-frontend/pages/profile/message-history.vue

@@ -0,0 +1,688 @@
+<template>
+  <view class="container">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-back" @click="goBack">
+        <text class="nav-back-icon">&#x2190;</text>
+      </view>
+      <text class="nav-title">消息历史</text>
+      <view class="nav-placeholder"></view>
+    </view>
+    
+    <!-- 筛选标签 -->
+    <view class="filter-tabs">
+      <view 
+        class="filter-tab" 
+        :class="{ active: activeFilter === 'all' }"
+        @click="switchFilter('all')">
+        <text>全部</text>
+      </view>
+      <view 
+        class="filter-tab" 
+        :class="{ active: activeFilter === 'energy' }"
+        @click="switchFilter('energy')">
+        <text>能量</text>
+      </view>
+      <view 
+        class="filter-tab" 
+        :class="{ active: activeFilter === 'points' }"
+        @click="switchFilter('points')">
+        <text>积分</text>
+      </view>
+      <view 
+        class="filter-tab" 
+        :class="{ active: activeFilter === 'cf' }"
+        @click="switchFilter('cf')">
+        <text>CF值</text>
+      </view>
+      <view 
+        class="filter-tab" 
+        :class="{ active: activeFilter === 'system' }"
+        @click="switchFilter('system')">
+        <text>系统</text>
+      </view>
+    </view>
+    
+    <!-- 消息列表 -->
+    <scroll-view 
+      class="message-list" 
+      scroll-y 
+      @scrolltolower="loadMore">
+      <view v-if="loading && messages.length === 0" class="loading-state">
+        <text>加载中...</text>
+      </view>
+      
+      <view v-else-if="messages.length === 0" class="empty-state">
+        <text class="empty-icon">📭</text>
+        <text class="empty-text">暂无消息</text>
+      </view>
+      
+      <template v-else>
+        <!-- 能量变更消息 -->
+        <view 
+          v-for="(msg, index) in energyMessages" 
+          :key="msg.id"
+          class="message-item"
+          :class="{ unread: !msg.isRead }"
+          @click="markAsRead(msg)">
+          <view class="message-icon" :style="{ background: 'rgba(249,115,22,0.15)' }">
+            <text>⚡</text>
+          </view>
+          <view class="message-content">
+            <text class="message-title">{{ msg.fullText || msg.description }}</text>
+            <text v-if="msg.amount !== 0" class="message-amount" :style="{ color: msg.amount >= 0 ? '#10B981' : '#EF4444' }">
+              {{ msg.amount >= 0 ? '+' : '' }}{{ msg.amount }}
+            </text>
+          </view>
+          <view class="message-meta">
+            <text class="message-time">{{ msg.time }}</text>
+          </view>
+        </view>
+        
+        <!-- 积分变更消息 -->
+        <view 
+          v-for="(msg, index) in pointsMessages" 
+          :key="msg.id"
+          class="message-item"
+          :class="{ unread: !msg.isRead }"
+          @click="markAsRead(msg)">
+          <view class="message-icon" :style="{ background: 'rgba(16,185,129,0.15)' }">
+            <text>🪙</text>
+          </view>
+          <view class="message-content">
+            <text class="message-title">{{ msg.fullText || msg.description }}</text>
+            <text v-if="msg.amount !== 0" class="message-amount" :style="{ color: msg.amount >= 0 ? '#10B981' : '#EF4444' }">
+              {{ msg.amount >= 0 ? '+' : '' }}{{ msg.amount }}
+            </text>
+          </view>
+          <view class="message-meta">
+            <text class="message-time">{{ msg.time }}</text>
+          </view>
+        </view>
+        
+        <!-- CF值变更消息 -->
+        <view 
+          v-for="(msg, index) in cfMessages" 
+          :key="msg.id"
+          class="message-item"
+          :class="{ unread: !msg.isRead }"
+          @click="markAsRead(msg)">
+          <view class="message-icon" :style="{ background: 'rgba(99,102,241,0.15)' }">
+            <text>💰</text>
+          </view>
+          <view class="message-content">
+            <text class="message-title">{{ msg.fullText || msg.description }}</text>
+            <text v-if="msg.amount !== 0" class="message-amount" :style="{ color: msg.amount >= 0 ? '#10B981' : '#EF4444' }">
+              {{ msg.amount >= 0 ? '+' : '' }}{{ msg.amount }}
+            </text>
+          </view>
+          <view class="message-meta">
+            <text class="message-time">{{ msg.time }}</text>
+          </view>
+        </view>
+        
+        <!-- 系统通知 -->
+        <view 
+          v-for="(msg, index) in systemMessages" 
+          :key="msg.id"
+          class="message-item"
+          :class="{ unread: !msg.isRead }"
+          @click="markAsRead(msg)">
+          <view class="message-icon" :style="{ background: 'rgba(100,116,139,0.15)' }">
+            <text>📢</text>
+          </view>
+          <view class="message-content">
+            <text class="message-title">{{ msg.title }}</text>
+            <text class="message-desc">{{ msg.description }}</text>
+          </view>
+          <view class="message-meta">
+            <text class="message-time">{{ msg.time }}</text>
+          </view>
+        </view>
+        
+        <!-- 加载更多 -->
+        <view v-if="loadingMore" class="loading-more">
+          <text>加载中...</text>
+        </view>
+        <view v-else-if="!hasMore && messages.length > 0" class="no-more">
+          <text>没有更多了</text>
+        </view>
+      </template>
+    </scroll-view>
+    
+    <!-- 底部操作 -->
+    <view class="bottom-actions">
+      <button class="btn-clear-all" @click="clearAllMessages">
+        清除所有消息
+      </button>
+      <button class="btn-mark-read" @click="markAllAsRead">
+        全部已读
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { 
+  getEnergyLogs, 
+  getPointsLogs, 
+  getCfList,
+  getNotificationList,
+  readNotification
+} from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+
+export default {
+  name: 'MessageHistory',
+  data() {
+    return {
+      activeFilter: 'all',
+      loading: true,
+      loadingMore: false,
+      hasMore: true,
+      currentPage: 1,
+      pageSize: 20,
+      
+      // 各类消息
+      energyMessages: [],
+      pointsMessages: [],
+      cfMessages: [],
+      systemMessages: [],
+      
+      // 用于分页
+      energyPage: 1,
+      pointsPage: 1,
+      cfPage: 1,
+      notificationPage: 1,
+    }
+  },
+  computed: {
+    messages() {
+      // 根据当前筛选返回对应消息
+      switch(this.activeFilter) {
+        case 'energy': return this.energyMessages
+        case 'points': return this.pointsMessages
+        case 'cf': return this.cfMessages
+        case 'system': return this.systemMessages
+        default: 
+          // 合并所有消息并按时间排序
+          var all = [
+            ...this.energyMessages.map(m => ({...m, type: 'energy'})),
+            ...this.pointsMessages.map(m => ({...m, type: 'points'})),
+            ...this.cfMessages.map(m => ({...m, type: 'cf'})),
+            ...this.systemMessages.map(m => ({...m, type: 'system'}))
+          ]
+          return all.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
+      }
+    }
+  },
+  onLoad() {
+    this.loadAllMessages()
+  },
+  onShow() {
+    // 每次显示时刷新未读数
+    this.refreshUnreadCounts()
+  },
+  methods: {
+    goBack() {
+      uni.navigateBack()
+    },
+    
+    switchFilter(filter) {
+      this.activeFilter = filter
+      // 如果还没加载过该类型消息,则加载
+      if (filter === 'energy' && this.energyMessages.length === 0) {
+        this.loadEnergyMessages()
+      } else if (filter === 'points' && this.pointsMessages.length === 0) {
+        this.loadPointsMessages()
+      } else if (filter === 'cf' && this.cfMessages.length === 0) {
+        this.loadCfMessages()
+      } else if (filter === 'system' && this.systemMessages.length === 0) {
+        this.loadSystemMessages()
+      }
+    },
+    
+    async loadAllMessages() {
+      this.loading = true
+      try {
+        await Promise.all([
+          this.loadEnergyMessages(),
+          this.loadPointsMessages(),
+          this.loadCfMessages(),
+          this.loadSystemMessages()
+        ])
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    async loadEnergyMessages() {
+      var memberId = uni.getStorageSync('currentMemberId') || this.currentChildId
+      if (!memberId) return
+      
+      var res = await getEnergyLogs(memberId, null, this.energyPage, this.pageSize)
+      if (res.code === 200 && res.data) {
+        var records = res.data.records || []
+        this.energyMessages = records.map(record => this.formatEnergyMessage(record))
+        this.energyPage++
+        this.hasMore = records.length === this.pageSize
+      }
+    },
+    
+    async loadPointsMessages() {
+      var memberId = uni.getStorageSync('currentMemberId') || this.currentChildId
+      if (!memberId) return
+      
+      var res = await getPointsLogs(memberId, this.pointsPage, this.pageSize)
+      if (res.code === 200 && res.data) {
+        var records = res.data.records || []
+        this.pointsMessages = records.map(record => this.formatPointsMessage(record))
+        this.pointsPage++
+        this.hasMore = records.length === this.pageSize
+      }
+    },
+    
+    async loadCfMessages() {
+      var res = await getCfList(this.cfPage, this.pageSize)
+      if (res.code === 200 && res.data) {
+        var records = res.data.records || []
+        this.cfMessages = records.map(record => this.formatCfMessage(record))
+        this.cfPage++
+        this.hasMore = records.length === this.pageSize
+      }
+    },
+    
+    async loadSystemMessages() {
+      var res = await getNotificationList()
+      if (res.code === 200 && res.data) {
+        var records = res.data || []
+        this.systemMessages = records.map(record => this.formatSystemMessage(record))
+        this.notificationPage++
+      }
+    },
+    
+    formatEnergyMessage(record) {
+      var desc = record.description || ''
+      var sourceType = record.sourceType || ''
+      
+      var contentText = ''
+      switch(sourceType) {
+        case 'task': contentText = '完成了' + (desc || '任务'); break
+        case 'product': contentText = '购买了' + (desc || '商品'); break
+        case 'medical_report': contentText = '上传了' + (desc || '体检报告'); break
+        case 'daily_record': contentText = '完成了' + (desc || '日常记录'); break
+        default: contentText = desc || '能量变动'
+      }
+      
+      return {
+        id: 'energy-' + record.id,
+        type: 'energy',
+        title: '能量变动',
+        fullText: '⚡ ' + contentText,
+        description: contentText,
+        amount: record.amount,
+        isRead: true,
+        createdAt: record.createdAt,
+        time: this.formatTime(record.createdAt)
+      }
+    },
+    
+    formatPointsMessage(record) {
+      var desc = record.description || ''
+      var type = record.type || ''
+      
+      var contentText = ''
+      switch(type) {
+        case 'task': 
+          contentText = '完成了' + (desc || '任务') + '获得' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'; break
+        case 'reward': 
+          contentText = '完成了' + (desc || '心愿兑换') + '获得' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'; break
+        case 'adjust': 
+          contentText = '管理员调整' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'; break
+        case 'expire': 
+          contentText = '积分过期扣除' + Math.abs(record.amount) + '积分'; break
+        default: 
+          contentText = (desc || '积分变动') + (record.amount >= 0 ? '获得' : '扣除') + Math.abs(record.amount) + '积分'
+      }
+      
+      return {
+        id: 'points-' + record.id,
+        type: 'points',
+        fullText: '🪙 ' + contentText,
+        description: contentText,
+        amount: record.amount,
+        isRead: true,
+        createdAt: record.createdAt,
+        time: this.formatTime(record.createdAt)
+      }
+    },
+    
+    formatCfMessage(record) {
+      var desc = record.description || ''
+      var action = record.action || ''
+      
+      var contentText = ''
+      switch(action) {
+        case 'transfer_send': contentText = 'CF值转出' + Math.abs(record.amount) + 'CF'; break
+        case 'transfer_receive': contentText = 'CF值转入' + record.amount + 'CF'; break
+        case 'commission': contentText = '获得' + record.amount + 'CF分佣'; break
+        default: contentText = (desc || 'CF值变动') + (record.amount >= 0 ? '+' : '') + record.amount + 'CF'
+      }
+      
+      return {
+        id: 'cf-' + record.id,
+        type: 'cf',
+        fullText: '💰 ' + contentText,
+        description: contentText,
+        amount: record.amount,
+        isRead: true,
+        createdAt: record.createdAt,
+        time: this.formatTime(record.createdAt)
+      }
+    },
+    
+    formatSystemMessage(record) {
+      return {
+        id: 'system-' + record.id,
+        type: 'system',
+        title: record.title || '系统通知',
+        description: record.content || '',
+        amount: 0,
+        isRead: record.isRead === 1,
+        createdAt: record.createdAt,
+        time: this.formatTime(record.createdAt),
+        record: record
+      }
+    },
+    
+    formatTime(dateStr) {
+      if (!dateStr) return ''
+      var d = parseDate(dateStr)
+      if (!d) return ''
+      
+      var now = new Date()
+      var diff = now - d
+      var minutes = Math.floor(diff / 60000)
+      
+      if (minutes < 1) return '刚刚'
+      if (minutes < 60) return minutes + '分钟前'
+      
+      var hours = Math.floor(minutes / 60)
+      if (hours < 24) return hours + '小时前'
+      
+      var days = Math.floor(hours / 24)
+      if (days === 1) return '昨天'
+      if (days < 7) return days + '天前'
+      
+      return (d.getMonth() + 1) + '月' + d.getDate() + '日'
+    },
+    
+    markAsRead(msg) {
+      msg.isRead = true
+      // 如果是系统通知,调用API标记为已读
+      if (msg.type === 'system' && msg.record && msg.record.id) {
+        readNotification({ id: msg.record.id }).catch(function() {})
+      }
+    },
+    
+    markAllAsRead() {
+      // 标记所有消息为已读
+      this.energyMessages.forEach(m => m.isRead = true)
+      this.pointsMessages.forEach(m => m.isRead = true)
+      this.cfMessages.forEach(m => m.isRead = true)
+      this.systemMessages.forEach(m => m.isRead = true)
+      
+      // 批量标记系统通知为已读
+      var unreadNotifications = this.systemMessages.filter(m => !m.isRead)
+      if (unreadNotifications.length > 0) {
+        readNotification({ ids: unreadNotifications.map(m => m.record.id) }).catch(function() {})
+      }
+      
+      uni.showToast({ title: '已全部标为已读', icon: 'success' })
+    },
+    
+    clearAllMessages() {
+      uni.showModal({
+        title: '清除消息',
+        content: '确定要清除所有历史消息吗?',
+        success: (res) => {
+          if (res.confirm) {
+            this.energyMessages = []
+            this.pointsMessages = []
+            this.cfMessages = []
+            this.systemMessages = []
+            this.currentPage = 1
+            this.energyPage = 1
+            this.pointsPage = 1
+            this.cfPage = 1
+            this.notificationPage = 1
+            uni.showToast({ title: '已清除', icon: 'success' })
+          }
+        }
+      })
+    },
+    
+    refreshUnreadCounts() {
+      // 刷新未读数(可选:从后端获取)
+    },
+    
+    loadMore() {
+      if (!this.hasMore || this.loadingMore) return
+      this.loadingMore = true
+      
+      var self = this
+      Promise.all([
+        this.energyPage < 10 ? this.loadEnergyMessages() : Promise.resolve(),
+        this.pointsPage < 10 ? this.loadPointsMessages() : Promise.resolve(),
+        this.cfPage < 10 ? this.loadCfMessages() : Promise.resolve(),
+        this.notificationPage < 10 ? this.loadSystemMessages() : Promise.resolve()
+      ]).finally(() => {
+        this.loadingMore = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f7fa;
+  display: flex;
+  flex-direction: column;
+}
+
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+  position: sticky;
+  top: 0;
+  z-index: 100;
+}
+
+.nav-back {
+  width: 60rpx;
+  height: 60rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.nav-back-icon {
+  font-size: 40rpx;
+  color: #333;
+}
+
+.nav-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.nav-placeholder {
+  width: 60rpx;
+}
+
+.filter-tabs {
+  display: flex;
+  padding: 20rpx 30rpx;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+  gap: 16rpx;
+}
+
+.filter-tab {
+  padding: 12rpx 24rpx;
+  background: #f5f5f5;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+  color: #666;
+}
+
+.filter-tab.active {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+}
+
+.message-list {
+  flex: 1;
+  padding: 20rpx 30rpx;
+  overflow-y: auto;
+}
+
+.message-item {
+  display: flex;
+  align-items: flex-start;
+  padding: 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+
+.message-item.unread {
+  border: 2rpx solid #F97316;
+}
+
+.message-icon {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.message-icon text {
+  font-size: 32rpx;
+}
+
+.message-content {
+  flex: 1;
+  min-width: 0;
+}
+
+.message-title {
+  font-size: 28rpx;
+  font-weight: 500;
+  color: #333;
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+.message-desc {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+.message-amount {
+  font-size: 28rpx;
+  font-weight: bold;
+  margin-top: 4rpx;
+}
+
+.message-meta {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 8rpx;
+}
+
+.message-time {
+  font-size: 20rpx;
+  color: #bbb;
+}
+
+.loading-state,
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 100rpx 0;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.loading-more,
+.no-more {
+  text-align: center;
+  padding: 30rpx 0;
+  font-size: 24rpx;
+  color: #999;
+}
+
+.bottom-actions {
+  display: flex;
+  gap: 20rpx;
+  padding: 30rpx;
+  background: #fff;
+  border-top: 1rpx solid #eee;
+  position: sticky;
+  bottom: 0;
+}
+
+.btn-clear-all,
+.btn-mark-read {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  font-weight: 500;
+  border: none;
+}
+
+.btn-clear-all {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.btn-mark-read {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+}
+</style>