智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

62 KiB

消息管理数据库分析报告(移动端+管理端)

📋 需求分析

第一部分:UniApp移动端消息管理

基于 pages-student/messages/index.vue 页面分析,移动端消息管理需要实现:

  1. 消息列表展示:显示员工/学员接收的所有消息
  2. 消息类型筛选:按消息类型进行分类筛选
  3. 消息已读状态:标记消息已读/未读
  4. 消息数量统计:显示未读消息数量和总消息数
  5. 业务页面跳转:根据消息类型跳转到对应业务页面 (需要完善)
  6. 消息搜索功能:关键词搜索消息内容 (需要添加)

第二部分:Admin管理端客户详情消息展示

基于 admin/src/app/views/customer_resources/components/UserProfile.vue 分析,管理端需要在客户详情中展示:

  1. 消息历史记录:显示与该客户的所有消息记录 (需要添加)
  2. 消息统计信息:已读/未读消息数量统计 (需要添加)
  3. 消息时间轴:按时间顺序展示消息流 (需要添加)
  4. 消息类型展示:不同类型消息的分类展示 (需要添加)

🗄️ 数据库表结构分析

1. school_chat_messages 表

现有字段分析

基于数据库实际结构分析,该表包含以下字段:

  • id - 主键ID
  • from_type - 发送者类型(enum: personnel, customer, system)
  • from_id - 发送者ID
  • to_id - 接收者ID
  • friend_id - 关联 chat_friends 表ID
  • message_type - 消息类型 **(已包含完整枚举值)**
  • content - 消息内容
  • is_read - 是否已读
  • read_time - 已读时间
  • title - 消息标题
  • business_id - 关联业务ID
  • business_type - 业务类型
  • created_at/updated_at - 时间戳
  • delete_time - 删除时间(软删除)

字段状态:完善

经过数据库检查,发现所有必要字段都已存在,包括:

  • 已读状态跟踪:is_read, read_time
  • 业务关联:business_id, business_type, title
  • 消息类型枚举已包含所有需要的类型:
    enum('text','img','system','notification','homework','feedback','reminder','order','student_courses','person_course_schedule')
    

2. school_chat_friends 表

现有字段分析

基于数据库实际结构分析:

  • id - 主键ID
  • personnel_id - 员工ID
  • customer_resources_id - 客户资源ID
  • unread_count_personnel - 员工端未读消息数
  • unread_count_customer_resources - 客户端未读消息数
  • created_at/updated_at - 时间戳
  • delete_time - 删除时间(软删除)

⚠️ 需要优化的字段

虽然基本字段完整,但建议添加以下字段以增强管理端展示效果:

-- 增强统计字段(可选)
ALTER TABLE `school_chat_friends` ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间';
ALTER TABLE `school_chat_friends` ADD COLUMN `last_message_content` varchar(500) DEFAULT '' COMMENT '最后消息内容';
ALTER TABLE `school_chat_friends` ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数';

数据库设计验证结果

1. 消息类型枚举完全匹配

数据库实际枚举值

message_type enum('text','img','system','notification','homework','feedback','reminder','order','student_courses','person_course_schedule')
- text:文本消息
- img:图片消息
- system:系统消息
- notification:通知公告
- homework:作业任务
- feedback:反馈评价
- reminder:课程提醒
- order:订单消息
- student_courses:学员课程变动消息
- person_course_schedule:课程安排消息

前端使用的类型

typeTabs: [
    { value: 'all', text: '全部', count: 0 },
    { value: 'system', text: '系统消息', count: 0 },
    { value: 'notification', text: '通知公告', count: 0 },
    { value: 'homework', text: '作业任务', count: 0 },
    { value: 'feedback', text: '反馈评价', count: 0 },
    { value: 'reminder', text: '课程提醒', count: 0 }
]

验证结果:数据库枚举值与前端期望完全匹配!数据库设计正确。

🚀 功能完善建议

第一部分:UniApp移动端功能增强

1. 业务页面跳转逻辑

当前状态viewMessage() 方法只显示消息详情弹窗 需要增强:根据消息类型和业务关联跳转到对应页面

建议实现方案

// 在 pages-student/messages/index.vue 中添加
navigateToBusinessPage(message) {
    // 消息类型与业务页面映射
    const routeMap = {
        'order': '/pages-common/order/detail', // 订单详情
        'student_courses': '/pages-student/courses/detail', // 课程详情
        'person_course_schedule': '/pages-student/schedule/detail', // 课程安排
        'homework': '/pages-student/homework/detail', // 作业详情
        'notification': '/pages-student/announcement/detail', // 通知详情
        'reminder': '/pages-student/schedule/detail' // 课程提醒
    };
    
    const route = routeMap[message.message_type];
    if (route && message.business_id) {
        uni.navigateTo({ 
            url: `${route}?id=${message.business_id}` 
        });
    } else {
        // 没有业务关联时显示详情弹窗
        this.selectedMessage = message;
        this.showMessagePopup = true;
    }
},

// 修改现有的 viewMessage 方法
viewMessage(message) {
    // 标记为已读
    if (!message.is_read) {
        this.markAsRead(message);
    }
    
    // 如果有业务关联,直接跳转
    if (message.business_id && message.business_type) {
        this.navigateToBusinessPage(message);
    } else {
        // 否则显示消息详情弹窗
        this.selectedMessage = message;
        this.showMessagePopup = true;
    }
}

2. 消息搜索功能

需要添加:消息内容和标题的关键词搜索

建议实现方案

<!-- 在消息类型筛选后添加搜索框 -->
<view class="search_section">
    <view class="search_box">
        <input 
            type="text" 
            placeholder="搜索消息标题或内容..." 
            v-model="searchKeyword"
            @input="performSearch"
            class="search_input"
        />
        <view class="search_icon" @click="performSearch">🔍</view>
    </view>
</view>
// 添加搜索相关数据和方法
data() {
    return {
        searchKeyword: '',
        searchTimer: null,
        // ... 其他数据
    }
},

methods: {
    performSearch() {
        // 防抖搜索
        clearTimeout(this.searchTimer);
        this.searchTimer = setTimeout(() => {
            this.currentPage = 1;
            this.loadMessages();
        }, 500);
    },
    
    // 修改 loadMessages 方法,增加搜索参数
    async loadMessages() {
        // ...现有代码
        const response = await apiRoute.getStudentMessageList({
            student_id: this.studentId,
            message_type: this.activeType === 'all' ? '' : this.activeType,
            search_keyword: this.searchKeyword, // 新增搜索参数
            page: this.currentPage,
            limit: 10
        });
        // ...其余代码
    }
}

第二部分:Admin管理端客户详情消息展示

1. 在客户详情中增加消息标签页

文件位置admin/src/app/views/customer_resources/components/UserProfile.vue

需要修改的地方

<!-- 在现有标签导航中添加消息标签页 -->
<el-tabs v-model="activeTab" class="mb-4">
    <el-tab-pane label="六要素" name="six-elements" />
    <el-tab-pane label="修改日志" name="log" />
    <el-tab-pane label="学生情况" name="student" />
    <el-tab-pane label="订单列表" name="orders" />
    <el-tab-pane label="沟通记录列表" name="communication_records" />
    <el-tab-pane label="赠品记录" name="gift_records" />
    <!-- 新增消息记录标签页 -->
    <el-tab-pane label="消息记录" name="messages" />
</el-tabs>

<!-- 添加消息记录内容区域 -->
<el-card v-if="activeTab === 'messages'">
    <Messages :customer_resource_id="user.id"/>
</el-card>

2. 创建消息记录组件

新建文件admin/src/app/views/customer_resources/components/Messages.vue

<template>
    <div class="messages-container">
        <!-- 消息统计 -->
        <el-row :gutter="16" class="mb-4">
            <el-col :span="6">
                <el-statistic title="总消息数" :value="messageStats.total" />
            </el-col>
            <el-col :span="6">
                <el-statistic title="未读消息" :value="messageStats.unread" />
            </el-col>
            <el-col :span="6">
                <el-statistic title="已读消息" :value="messageStats.read" />
            </el-col>
            <el-col :span="6">
                <el-statistic title="最后消息时间" :value="messageStats.lastTime" />
            </el-col>
        </el-row>

        <!-- 消息类型筛选 -->
        <el-row class="mb-4">
            <el-col>
                <el-radio-group v-model="filterType" @change="loadMessages">
                    <el-radio-button label="all">全部</el-radio-button>
                    <el-radio-button label="system">系统消息</el-radio-button>
                    <el-radio-button label="notification">通知公告</el-radio-button>
                    <el-radio-button label="homework">作业任务</el-radio-button>
                    <el-radio-button label="feedback">反馈评价</el-radio-button>
                    <el-radio-button label="reminder">课程提醒</el-radio-button>
                    <el-radio-button label="order">订单消息</el-radio-button>
                </el-radio-group>
            </el-col>
        </el-row>

        <!-- 消息列表 -->
        <el-table :data="messageList" v-loading="loading" stripe>
            <el-table-column prop="message_type" label="消息类型" width="120" align="center">
                <template #default="{ row }">
                    <el-tag :type="getMessageTypeColor(row.message_type)">
                        {{ getMessageTypeText(row.message_type) }}
                    </el-tag>
                </template>
            </el-table-column>
            
            <el-table-column prop="title" label="消息标题" width="200" show-overflow-tooltip />
            
            <el-table-column prop="content" label="消息内容" show-overflow-tooltip>
                <template #default="{ row }">
                    <span>{{ row.content.length > 50 ? row.content.substring(0, 50) + '...' : row.content }}</span>
                </template>
            </el-table-column>
            
            <el-table-column prop="is_read" label="已读状态" width="100" align="center">
                <template #default="{ row }">
                    <el-tag :type="row.is_read ? 'success' : 'danger'">
                        {{ row.is_read ? '已读' : '未读' }}
                    </el-tag>
                </template>
            </el-table-column>
            
            <el-table-column prop="created_at" label="发送时间" width="160" />
            
            <el-table-column prop="read_time" label="阅读时间" width="160">
                <template #default="{ row }">
                    <span>{{ row.read_time || '未读' }}</span>
                </template>
            </el-table-column>
            
            <el-table-column label="操作" width="100" fixed="right">
                <template #default="{ row }">
                    <el-button type="primary" size="small" @click="viewMessageDetail(row)">
                        查看
                    </el-button>
                </template>
            </el-table-column>
        </el-table>

        <!-- 分页 -->
        <div class="mt-4 flex justify-end">
            <el-pagination
                v-model:current-page="currentPage"
                v-model:page-size="pageSize"
                :total="total"
                layout="total, sizes, prev, pager, next, jumper"
                @current-change="loadMessages"
                @size-change="loadMessages"
            />
        </div>

        <!-- 消息详情弹窗 -->
        <el-dialog v-model="detailDialogVisible" title="消息详情" width="60%">
            <div v-if="selectedMessage">
                <el-descriptions :column="2" border>
                    <el-descriptions-item label="消息类型">
                        <el-tag :type="getMessageTypeColor(selectedMessage.message_type)">
                            {{ getMessageTypeText(selectedMessage.message_type) }}
                        </el-tag>
                    </el-descriptions-item>
                    <el-descriptions-item label="发送时间">
                        {{ selectedMessage.created_at }}
                    </el-descriptions-item>
                    <el-descriptions-item label="已读状态">
                        <el-tag :type="selectedMessage.is_read ? 'success' : 'danger'">
                            {{ selectedMessage.is_read ? '已读' : '未读' }}
                        </el-tag>
                    </el-descriptions-item>
                    <el-descriptions-item label="阅读时间">
                        {{ selectedMessage.read_time || '未读' }}
                    </el-descriptions-item>
                    <el-descriptions-item label="消息标题" span="2">
                        {{ selectedMessage.title }}
                    </el-descriptions-item>
                </el-descriptions>
                
                <div class="mt-4">
                    <h4>消息内容:</h4>
                    <div class="message-content p-4 bg-gray-50 rounded">
                        {{ selectedMessage.content }}
                    </div>
                </div>
            </div>
        </el-dialog>
    </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getCustomerMessages, getCustomerMessageStats } from '@/app/api/customer_resources'

const props = defineProps({
    customer_resource_id: {
        type: Number,
        required: true
    }
})

const loading = ref(false)
const messageList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const filterType = ref('all')
const detailDialogVisible = ref(false)
const selectedMessage = ref(null)

const messageStats = reactive({
    total: 0,
    read: 0,
    unread: 0,
    lastTime: ''
})

const loadMessages = async () => {
    loading.value = true
    try {
        const response = await getCustomerMessages({
            customer_resource_id: props.customer_resource_id,
            message_type: filterType.value === 'all' ? '' : filterType.value,
            page: currentPage.value,
            limit: pageSize.value
        })
        
        if (response.code === 1) {
            messageList.value = response.data.list
            total.value = response.data.total
        }
    } catch (error) {
        console.error('获取消息列表失败:', error)
    } finally {
        loading.value = false
    }
}

const loadMessageStats = async () => {
    try {
        const response = await getCustomerMessageStats({
            customer_resource_id: props.customer_resource_id
        })
        
        if (response.code === 1) {
            Object.assign(messageStats, response.data)
        }
    } catch (error) {
        console.error('获取消息统计失败:', error)
    }
}

const getMessageTypeText = (type) => {
    const typeMap = {
        'text': '文本消息',
        'img': '图片消息',
        'system': '系统消息',
        'notification': '通知公告',
        'homework': '作业任务',
        'feedback': '反馈评价',
        'reminder': '课程提醒',
        'order': '订单消息',
        'student_courses': '课程变动',
        'person_course_schedule': '课程安排'
    }
    return typeMap[type] || type
}

const getMessageTypeColor = (type) => {
    const colorMap = {
        'system': 'primary',
        'notification': 'warning',
        'homework': 'danger',
        'feedback': 'success',
        'reminder': 'info',
        'order': 'warning',
        'student_courses': 'success',
        'person_course_schedule': 'info'
    }
    return colorMap[type] || 'default'
}

const viewMessageDetail = (message) => {
    selectedMessage.value = message
    detailDialogVisible.value = true
}

onMounted(() => {
    loadMessages()
    loadMessageStats()
})
</script>

<style scoped>
.messages-container {
    .message-content {
        white-space: pre-wrap;
        word-break: break-all;
    }
}
</style>

🔧 数据库优化方案

核心表结构已完善

经过验证,school_chat_messages 表和 school_chat_friends 表的核心字段都已存在,无需进行大规模修改。

⚠️ 可选优化字段

为了增强管理端的展示效果,建议添加以下字段:

-- 为 school_chat_friends 表添加增强统计字段(可选)
ALTER TABLE `school_chat_friends` 
ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间',
ADD COLUMN `last_message_content` varchar(500) DEFAULT '' COMMENT '最后消息内容',
ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数';

-- 创建触发器自动维护统计数据(可选)
DELIMITER $$
CREATE TRIGGER update_message_stats_after_insert
AFTER INSERT ON school_chat_messages
FOR EACH ROW
BEGIN
    UPDATE school_chat_friends 
    SET 
        total_messages = IFNULL(total_messages, 0) + 1,
        unread_count_customer_resources = unread_count_customer_resources + 1,
        last_message_time = NEW.created_at,
        last_message_content = LEFT(NEW.content, 500)
    WHERE id = NEW.friend_id;
END$$

CREATE TRIGGER update_message_stats_after_update
AFTER UPDATE ON school_chat_messages
FOR EACH ROW
BEGIN
    IF OLD.is_read = 0 AND NEW.is_read = 1 THEN
        UPDATE school_chat_friends 
        SET unread_count_customer_resources = GREATEST(unread_count_customer_resources - 1, 0)
        WHERE id = NEW.friend_id;
    END IF;
END$$
DELIMITER ;

📱 消息类型与业务页面映射

消息类型说明

以下是各种消息类型的业务含义和对应的处理方式:

消息类型 中文名称 业务页面 处理方式
text 文本消息 仅显示详情弹窗
img 图片消息 显示图片预览
system 系统消息 仅显示详情弹窗
notification 通知公告 公告详情页 跳转+详情弹窗
homework 作业任务 作业详情页 跳转到作业提交页
feedback 反馈评价 评价详情页 跳转+详情弹窗
reminder 课程提醒 课程表页面 跳转到相关课程
order 订单消息 订单详情页 跳转到订单详情
student_courses 课程变动 课程详情页 跳转到课程详情
person_course_schedule 课程安排 课程安排页 跳转到课程安排

📊 功能状态总结

已完成的功能

  1. 数据库结构:消息存储、已读状态、业务关联字段完整
  2. 消息类型:枚举值与前端完全匹配
  3. 移动端基础功能:消息列表、类型筛选、已读标记
  4. 管理端基础架构:客户详情页面框架完整

需要完善的功能

  1. 移动端业务跳转:根据消息类型跳转到对应业务页面
  2. 移动端搜索功能:消息内容和标题的关键词搜索
  3. 管理端消息展示:在客户详情中增加消息记录标签页
  4. 后端API接口:客户消息列表和统计接口

🎯 实施优先级

高优先级(立即实施)

  1. 完善移动端业务页面跳转逻辑
  2. 在管理端客户详情中添加消息标签页

中优先级(后续实施)

  1. 添加移动端搜索功能
  2. 完善管理端消息统计展示

低优先级(可选)

  1. 添加数据库统计字段和触发器
  2. 消息推送和实时通知功能

待确认问题

基于当前分析,以下问题需要进一步确认:

1. 消息接收者身份确认

问题:消息管理系统中的接收者身份定义

  • UniApp移动端是否同时支持员工和学员接收消息?
  • 当前school_chat_messages.to_id字段存储的是什么类型的用户ID?
  • 员工和学员的消息是否需要分别处理?

建议确认方案

  • 明确to_id字段的用户类型(员工ID还是客户资源ID)目前的设计是员工和学员都会在这个字段存,如果不合理你可以在下一个版本的文档计划中告知我如何修改
  • 确认from_typeto_id的对应关系逻辑

2. 业务页面路由确认

问题:消息跳转的目标页面是否存在

  • 各种消息类型对应的页面路径是否正确?
  • 这些业务页面在UniApp项目中是否已经实现?

需要确认的页面路径

/pages-common/order/detail          // 订单详情页
/pages-student/courses/detail       // 课程详情页  
/pages-student/schedule/detail      // 课程安排页
/pages-student/homework/detail      // 作业详情页(废弃不需要了)
/pages-student/announcement/detail  // 通知详情页 (废弃不需要了)

剩余没有的页面可以用弹窗进行展示。

3. 管理端数据权限

问题:管理端消息展示的权限范围

  • 管理员是否可以查看所有用户的消息记录?管理员可以查看全部的用户消息数据
  • 是否需要按权限区分不同管理员能查看的消息范围?消息不做权限的控制
  • 客户资源的消息记录是否包含员工发送给客户的消息?包含

4. 消息发送机制

问题:消息的创建和发送流程

  • 消息是通过什么方式创建的(系统自动、管理员手动、第三方接口)?消息有员工给客户发送的和系统自动创建发送的。
  • business_idbusiness_type字段的具体使用场景?business_type如果是订单类型的话,business_id就是订单ID大概是这样设计的如果你有更好的方案可以在下一个版本的 文档中给我说明
  • 是否需要支持批量消息发送?不需要

5. 数据库字段补充

问题:是否需要立即添加以下字段

-- school_chat_friends 表增强字段
last_message_time      // 最后消息时间
last_message_content   // 最后消息内容  
total_messages         // 总消息数

可以新增。

影响评估

  • 不添加:基本功能正常,管理端展示简化
  • ⚠️ 添加:增强管理端体验,需要数据迁移

6. 搜索功能实现范围

问题:消息搜索的具体需求

  • 搜索是否需要支持多关键词?消息搜索单关键词 like 查询即可
  • 是否需要按时间范围筛选?需要
  • 搜索结果是否需要高亮显示?不需要,列表是分页的,能查询出来分页即可

7. 消息状态同步

问题:已读状态的同步机制

  • 移动端标记已读后,管理端是否需要实时更新?重新调用接口展示新状态即可不需要实时更新。
  • 是否需要消息推送功能?可以在后台做一个推送功能,使用微信公众号模板消息给用户发送消息提醒。
  • 离线消息如何处理?没有离线消息,都是数据库存储的,在客户端查询出来的离线就意味着数据无法加载了。

请针对以上问题提供明确答复,以便进行精确的功能实现。


🎯 基于回复的详细实现方案

1. 数据存储设计优化建议

当前问题分析

根据你的回复,to_id字段同时存储员工ID和学员ID,这种设计存在以下问题:

  • 数据完整性风险:无法通过外键约束保证数据一致性
  • 查询复杂度:需要联合查询多张表才能获取完整用户信息
  • 扩展性问题:后续增加新用户类型时需要修改大量代码

下一版本优化方案

-- 方案A:增加接收者类型字段(推荐)
ALTER TABLE `school_chat_messages` 
ADD COLUMN `to_type` enum('personnel','customer','student') NOT NULL DEFAULT 'customer' COMMENT '接收者类型' 
AFTER `to_id`;

-- 方案B:创建统一用户关系表
CREATE TABLE `school_user_relations` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `user_id` int(11) NOT NULL COMMENT '用户ID',
    `user_type` enum('personnel','customer','student') NOT NULL COMMENT '用户类型',
    `ref_table` varchar(50) NOT NULL COMMENT '关联表名',
    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `user_type_id` (`user_id`, `user_type`),
    KEY `idx_user_type` (`user_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关系映射表';

我选方案A,因为创建统一用户关系映射表会增加额外的数据存储和查询开销。

2. 业务页面跳转详细实现

消息类型处理策略

基于你的回复,实现以下跳转逻辑:

// 在 pages-student/messages/index.vue 中完善
navigateToBusinessPage(message) {
    const { message_type, business_id, business_type } = message;
    
    // 有业务页面的消息类型
    const pageRouteMap = {
        'order': '/pages-common/order/detail',              // 订单详情页 ✅
        'student_courses': '/pages-student/courses/detail',  // 课程详情页 ✅  
        'person_course_schedule': '/pages-student/schedule/detail' // 课程安排页 ✅
    };
    
    // 使用弹窗展示的消息类型
    const popupTypes = [
        'text', 'img', 'system', 'notification', 
        'homework', 'feedback', 'reminder'
    ];
    
    if (pageRouteMap[message_type] && business_id) {
        // 跳转到对应业务页面
        uni.navigateTo({ 
            url: `${pageRouteMap[message_type]}?id=${business_id}` 
        });
    } else if (popupTypes.includes(message_type)) {
        // 使用弹窗展示
        this.selectedMessage = message;
        this.showMessagePopup = true;
    } else {
        // 默认弹窗展示
        this.selectedMessage = message;
        this.showMessagePopup = true;
        console.warn(`未知消息类型: ${message_type}`);
    }
},

// 增强的消息详情弹窗
showEnhancedMessageDetail(message) {
    // 根据消息类型显示不同的操作按钮
    const actionButtons = this.getMessageActionButtons(message.message_type);
    
    // 显示增强的消息详情
    this.selectedMessage = {
        ...message,
        actionButtons
    };
    this.showMessagePopup = true;
},

getMessageActionButtons(messageType) {
    const buttonMap = {
        'notification': [
            { text: '确认已读', type: 'primary', action: 'confirmRead' },
            { text: '查看详情', type: 'info', action: 'viewDetail' }
        ],
        'feedback': [
            { text: '查看反馈', type: 'success', action: 'viewFeedback' },
            { text: '回复', type: 'primary', action: 'reply' }
        ],
        'reminder': [
            { text: '查看课程', type: 'warning', action: 'viewCourse' },
            { text: '设置提醒', type: 'info', action: 'setReminder' }
        ]
    };
    
    return buttonMap[messageType] || [
        { text: '确认已读', type: 'primary', action: 'confirmRead' }
    ];
}

3. 数据库字段新增实施方案

立即实施的SQL语句

-- 为 school_chat_friends 表添加增强统计字段
ALTER TABLE `school_chat_friends` 
ADD COLUMN `last_message_time` timestamp NULL DEFAULT NULL COMMENT '最后消息时间',
ADD COLUMN `last_message_content` varchar(500) DEFAULT '' COMMENT '最后消息内容',
ADD COLUMN `total_messages` int(11) DEFAULT 0 COMMENT '总消息数';

-- 创建索引优化查询性能
ALTER TABLE `school_chat_friends` 
ADD INDEX `idx_last_message_time` (`last_message_time`),
ADD INDEX `idx_personnel_customer` (`personnel_id`, `customer_resources_id`);

-- 为消息表增加复合索引
ALTER TABLE `school_chat_messages` 
ADD INDEX `idx_to_type_time` (`to_id`, `message_type`, `created_at`),
ADD INDEX `idx_friend_read` (`friend_id`, `is_read`, `created_at`);

数据迁移脚本

-- 初始化现有数据的统计信息
UPDATE school_chat_friends cf 
SET 
    total_messages = (
        SELECT COUNT(*) 
        FROM school_chat_messages cm 
        WHERE cm.friend_id = cf.id AND cm.delete_time = 0
    ),
    last_message_time = (
        SELECT MAX(created_at) 
        FROM school_chat_messages cm 
        WHERE cm.friend_id = cf.id AND cm.delete_time = 0
    ),
    last_message_content = (
        SELECT LEFT(content, 500) 
        FROM school_chat_messages cm 
        WHERE cm.friend_id = cf.id AND cm.delete_time = 0 
        ORDER BY created_at DESC 
        LIMIT 1
    );

-- 更新未读消息统计(如果当前统计不准确)
UPDATE school_chat_friends cf 
SET unread_count_customer_resources = (
    SELECT COUNT(*) 
    FROM school_chat_messages cm 
    WHERE cm.friend_id = cf.id 
    AND cm.is_read = 0 
    AND cm.from_type = 'personnel' 
    AND cm.delete_time = 0
);

自动维护触发器(完善版)

-- 删除可能存在的旧触发器
DROP TRIGGER IF EXISTS update_message_stats_after_insert;
DROP TRIGGER IF EXISTS update_message_stats_after_update;

-- 创建新的触发器
DELIMITER $$

CREATE TRIGGER update_message_stats_after_insert
AFTER INSERT ON school_chat_messages
FOR EACH ROW
BEGIN
    -- 更新总消息数和最后消息信息
    UPDATE school_chat_friends 
    SET 
        total_messages = IFNULL(total_messages, 0) + 1,
        last_message_time = NEW.created_at,
        last_message_content = LEFT(NEW.content, 500)
    WHERE id = NEW.friend_id;
    
    -- 如果是发给客户的消息,更新客户端未读数
    IF NEW.from_type = 'personnel' THEN
        UPDATE school_chat_friends 
        SET unread_count_customer_resources = unread_count_customer_resources + 1
        WHERE id = NEW.friend_id;
    END IF;
    
    -- 如果是发给员工的消息,更新员工端未读数
    IF NEW.from_type IN ('customer', 'system') THEN
        UPDATE school_chat_friends 
        SET unread_count_personnel = unread_count_personnel + 1
        WHERE id = NEW.friend_id;
    END IF;
END$$

CREATE TRIGGER update_message_stats_after_update
AFTER UPDATE ON school_chat_messages
FOR EACH ROW
BEGIN
    -- 如果消息从未读变为已读
    IF OLD.is_read = 0 AND NEW.is_read = 1 THEN
        -- 根据消息发送方向更新对应的未读数
        IF NEW.from_type = 'personnel' THEN
            UPDATE school_chat_friends 
            SET unread_count_customer_resources = GREATEST(unread_count_customer_resources - 1, 0)
            WHERE id = NEW.friend_id;
        ELSE
            UPDATE school_chat_friends 
            SET unread_count_personnel = GREATEST(unread_count_personnel - 1, 0)
            WHERE id = NEW.friend_id;
        END IF;
    END IF;
END$$

DELIMITER ;

4. 管理端消息展示详细实现

API接口设计

// 在 admin/api 中添加
/**
 * 获取客户消息列表
 * @param int $customer_resource_id 客户资源ID
 * @param string $message_type 消息类型
 * @param int $page 页码
 * @param int $limit 每页数量
 */
public function getCustomerMessages($customer_resource_id, $message_type = '', $page = 1, $limit = 10) {
    $where = [
        ['delete_time', '=', 0]
    ];
    
    // 根据客户资源ID查找对应的friend_id
    $friendIds = Db::name('school_chat_friends')
        ->where('customer_resources_id', $customer_resource_id)
        ->where('delete_time', 0)
        ->column('id');
        
    if (empty($friendIds)) {
        return ['list' => [], 'total' => 0];
    }
    
    $where[] = ['friend_id', 'in', $friendIds];
    
    if (!empty($message_type) && $message_type !== 'all') {
        $where[] = ['message_type', '=', $message_type];
    }
    
    $list = Db::name('school_chat_messages')
        ->alias('cm')
        ->leftJoin('school_personnel sp', 'cm.from_id = sp.id AND cm.from_type = "personnel"')
        ->leftJoin('school_customer_resources scr', 'cm.from_id = scr.id AND cm.from_type = "customer"')
        ->where($where)
        ->field([
            'cm.*',
            'CASE 
                WHEN cm.from_type = "personnel" THEN sp.name 
                WHEN cm.from_type = "customer" THEN scr.name 
                ELSE "系统" 
            END as from_name'
        ])
        ->order('cm.created_at DESC')
        ->paginate([
            'list_rows' => $limit,
            'page' => $page
        ]);
        
    return [
        'list' => $list->items(),
        'total' => $list->total()
    ];
}

/**
 * 获取客户消息统计
 * @param int $customer_resource_id 客户资源ID
 */
public function getCustomerMessageStats($customer_resource_id) {
    $friendIds = Db::name('school_chat_friends')
        ->where('customer_resources_id', $customer_resource_id)
        ->where('delete_time', 0)
        ->column('id');
        
    if (empty($friendIds)) {
        return [
            'total' => 0,
            'unread' => 0,
            'read' => 0,
            'lastTime' => ''
        ];
    }
    
    $stats = Db::name('school_chat_messages')
        ->where('friend_id', 'in', $friendIds)
        ->where('delete_time', 0)
        ->field([
            'COUNT(*) as total',
            'SUM(CASE WHEN is_read = 0 THEN 1 ELSE 0 END) as unread',
            'SUM(CASE WHEN is_read = 1 THEN 1 ELSE 0 END) as read',
            'MAX(created_at) as last_time'
        ])
        ->find();
        
    return [
        'total' => $stats['total'] ?: 0,
        'unread' => $stats['unread'] ?: 0,
        'read' => $stats['read'] ?: 0,
        'lastTime' => $stats['last_time'] ?: ''
    ];
}

前端组件注册

// 在 UserProfile.vue 中注册新组件
import Messages from '@/app/views/customer_resources/components/Messages.vue'

export default {
    components: {
        Log,
        Student,
        Orders,
        CommunicationRecords,
        GiftRecords,
        Messages // 新增
    },
    // ... 其他代码
}

5. Business_type 字段使用规范

推荐的业务类型映射

// 业务类型标准化
const BUSINESS_TYPE_MAP = {
    'order': {
        table: 'school_orders',
        id_field: 'id',
        name_field: 'order_no',
        description: '订单相关消息'
    },
    'course': {
        table: 'school_courses', 
        id_field: 'id',
        name_field: 'course_name',
        description: '课程相关消息'
    },
    'schedule': {
        table: 'school_course_schedule',
        id_field: 'id', 
        name_field: 'schedule_name',
        description: '课程安排相关消息'
    },
    'contract': {
        table: 'school_contracts',
        id_field: 'id',
        name_field: 'contract_no', 
        description: '合同相关消息'
    }
};

// 验证business_id的有效性
function validateBusinessRelation(business_type, business_id) {
    const config = BUSINESS_TYPE_MAP[business_type];
    if (!config) return false;
    
    // 检查关联记录是否存在
    const exists = Db::name(config.table)
        ->where(config.id_field, business_id)
        ->where('delete_time', 0)
        ->count();
        
    return exists > 0;
}

6. 下一版本优化计划

架构优化建议

  1. 消息路由系统:建立统一的消息路由管理
  2. 消息模板系统:支持动态消息内容生成
  3. 消息队列机制:支持延时发送和批量处理
  4. 用户身份统一:解决多用户类型的身份识别问题

性能优化建议

  1. 数据分片:按时间维度分表存储历史消息
  2. 缓存策略:Redis缓存热点消息和统计数据
  3. 索引优化:基于查询模式优化数据库索引
  4. 异步处理:消息发送和统计更新异步化

这个详细的实现方案基于你的回复制定,可以立即开始实施。如果还有其他细节需要clarify,请告诉我!


🏗️ 本次开发计划:架构优化详细设计

基于我们的沟通情况,以下架构优化内容将纳入本次开发计划:

第一阶段:核心架构优化(高优先级)

1. 消息路由系统

设计目标:建立统一的消息分发和路由机制,支持多种消息类型的自动化处理

数据库设计

-- 消息路由配置表
CREATE TABLE `school_message_routes` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `message_type` varchar(50) NOT NULL COMMENT '消息类型',
    `route_name` varchar(100) NOT NULL COMMENT '路由名称',
    `route_config` text NOT NULL COMMENT '路由配置(JSON格式)',
    `is_active` tinyint(1) DEFAULT 1 COMMENT '是否激活',
    `priority` int(11) DEFAULT 0 COMMENT '优先级',
    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
    `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_type_route` (`message_type`, `route_name`),
    KEY `idx_type_active` (`message_type`, `is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息路由配置表';

-- 初始化路由数据
INSERT INTO `school_message_routes` (`message_type`, `route_name`, `route_config`) VALUES
('order', 'uniapp_page', '{"page_path": "/pages-common/order/detail", "param_key": "id", "fallback": "popup"}'),
('student_courses', 'uniapp_page', '{"page_path": "/pages-student/courses/detail", "param_key": "id", "fallback": "popup"}'),
('person_course_schedule', 'uniapp_page', '{"page_path": "/pages-student/schedule/detail", "param_key": "id", "fallback": "popup"}'),
('notification', 'popup_with_actions', '{"actions": [{"text": "确认已读", "type": "primary", "action": "confirmRead"}]}'),
('feedback', 'popup_with_actions', '{"actions": [{"text": "查看反馈", "type": "success", "action": "viewFeedback"}, {"text": "回复", "type": "primary", "action": "reply"}]}'),
('reminder', 'popup_with_actions', '{"actions": [{"text": "查看课程", "type": "warning", "action": "viewCourse"}, {"text": "设置提醒", "type": "info", "action": "setReminder"}]}'),
('system', 'popup_simple', '{"auto_read": true}'),
('text', 'popup_simple', '{"auto_read": false}'),
('img', 'image_preview', '{"allow_save": true}');

后端路由处理器

<?php
// niucloud/app/service/MessageRouterService.php

namespace app\service;

use think\facade\Db;
use think\facade\Cache;

class MessageRouterService 
{
    /**
     * 获取消息路由配置
     * @param string $messageType 消息类型
     * @return array
     */
    public function getMessageRoute($messageType) 
    {
        $cacheKey = "message_route_{$messageType}";
        
        return Cache::remember($cacheKey, function() use ($messageType) {
            $routes = Db::name('school_message_routes')
                ->where('message_type', $messageType)
                ->where('is_active', 1)
                ->order('priority DESC')
                ->select()
                ->toArray();
                
            return array_map(function($route) {
                $route['route_config'] = json_decode($route['route_config'], true);
                return $route;
            }, $routes);
        }, 300); // 缓存5分钟
    }
    
    /**
     * 处理消息路由
     * @param array $message 消息数据
     * @return array 路由处理结果
     */
    public function processMessageRoute($message) 
    {
        $routes = $this->getMessageRoute($message['message_type']);
        
        foreach ($routes as $route) {
            $result = $this->executeRoute($route, $message);
            if ($result['success']) {
                return $result;
            }
        }
        
        // 默认路由:弹窗展示
        return [
            'success' => true,
            'route_type' => 'popup_simple',
            'config' => ['auto_read' => false]
        ];
    }
    
    /**
     * 执行路由规则
     * @param array $route 路由配置
     * @param array $message 消息数据
     * @return array
     */
    private function executeRoute($route, $message) 
    {
        $config = $route['route_config'];
        
        switch ($route['route_name']) {
            case 'uniapp_page':
                if (!empty($message['business_id'])) {
                    return [
                        'success' => true,
                        'route_type' => 'page_navigation',
                        'config' => [
                            'url' => $config['page_path'] . '?' . $config['param_key'] . '=' . $message['business_id'],
                            'fallback' => $config['fallback'] ?? 'popup'
                        ]
                    ];
                }
                break;
                
            case 'popup_with_actions':
            case 'popup_simple':
            case 'image_preview':
                return [
                    'success' => true,
                    'route_type' => $route['route_name'],
                    'config' => $config
                ];
        }
        
        return ['success' => false];
    }
}

前端路由处理

// uniapp/utils/messageRouter.js
class MessageRouter {
    constructor() {
        this.routeHandlers = {
            'page_navigation': this.handlePageNavigation,
            'popup_with_actions': this.handlePopupWithActions,
            'popup_simple': this.handlePopupSimple,
            'image_preview': this.handleImagePreview
        };
    }
    
    async processMessage(message, context) {
        try {
            // 调用后端获取路由配置
            const routeResult = await uni.request({
                url: '/api/message/route',
                method: 'POST',
                data: { message_id: message.id, message_type: message.message_type }
            });
            
            if (routeResult.data.code === 1) {
                const { route_type, config } = routeResult.data.data;
                const handler = this.routeHandlers[route_type];
                
                if (handler) {
                    return await handler.call(this, message, config, context);
                }
            }
            
            // 默认处理
            return this.handlePopupSimple(message, {}, context);
            
        } catch (error) {
            console.error('消息路由处理失败:', error);
            return this.handlePopupSimple(message, {}, context);
        }
    }
    
    handlePageNavigation(message, config, context) {
        return new Promise((resolve) => {
            uni.navigateTo({
                url: config.url,
                success: () => resolve({ success: true, action: 'navigate' }),
                fail: () => {
                    // 降级到弹窗
                    if (config.fallback === 'popup') {
                        context.showMessagePopup(message);
                    }
                    resolve({ success: true, action: 'popup_fallback' });
                }
            });
        });
    }
    
    handlePopupWithActions(message, config, context) {
        const enhancedMessage = {
            ...message,
            actionButtons: config.actions || []
        };
        
        context.showEnhancedMessageDetail(enhancedMessage);
        return Promise.resolve({ success: true, action: 'popup_with_actions' });
    }
    
    handlePopupSimple(message, config, context) {
        context.showMessagePopup(message);
        
        if (config.auto_read && !message.is_read) {
            context.markAsRead(message);
        }
        
        return Promise.resolve({ success: true, action: 'popup_simple' });
    }
    
    handleImagePreview(message, config, context) {
        // 图片预览逻辑
        const imageUrl = message.content || message.attachment_url;
        
        if (imageUrl) {
            uni.previewImage({
                urls: [imageUrl],
                current: 0
            });
        } else {
            this.handlePopupSimple(message, {}, context);
        }
        
        return Promise.resolve({ success: true, action: 'image_preview' });
    }
}

export default new MessageRouter();

2. 消息模板系统

设计目标:支持动态消息内容生成,统一消息格式,支持多语言和个性化内容

数据库设计

-- 消息模板表
CREATE TABLE `school_message_templates` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `template_code` varchar(100) NOT NULL COMMENT '模板代码',
    `template_name` varchar(200) NOT NULL COMMENT '模板名称',
    `message_type` varchar(50) NOT NULL COMMENT '消息类型',
    `title_template` varchar(500) NOT NULL COMMENT '标题模板',
    `content_template` text NOT NULL COMMENT '内容模板',
    `variables` text COMMENT '变量定义(JSON格式)',
    `business_type` varchar(50) DEFAULT '' COMMENT '业务类型',
    `is_active` tinyint(1) DEFAULT 1 COMMENT '是否激活',
    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
    `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_template_code` (`template_code`),
    KEY `idx_type_business` (`message_type`, `business_type`),
    KEY `idx_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息模板表';

-- 初始化模板数据
INSERT INTO `school_message_templates` (`template_code`, `template_name`, `message_type`, `title_template`, `content_template`, `variables`, `business_type`) VALUES
('ORDER_CREATED', '订单创建通知', 'order', '订单创建成功', '您好 {{customer_name}},您的订单 {{order_no}} 已创建成功,订单金额 {{order_amount}} 元。', '{"customer_name": "客户姓名", "order_no": "订单编号", "order_amount": "订单金额"}', 'order'),
('ORDER_PAID', '订单支付成功', 'order', '订单支付成功', '您好 {{customer_name}},您的订单 {{order_no}} 已支付成功,支付金额 {{pay_amount}} 元,支付时间 {{pay_time}}。', '{"customer_name": "客户姓名", "order_no": "订单编号", "pay_amount": "支付金额", "pay_time": "支付时间"}', 'order'),
('COURSE_REMINDER', '课程提醒', 'reminder', '课程提醒', '您好 {{student_name}},您预定的课程 {{course_name}} 将于 {{class_time}} 开始,请准时参加。地点:{{class_location}}', '{"student_name": "学员姓名", "course_name": "课程名称", "class_time": "上课时间", "class_location": "上课地点"}', 'course'),
('FEEDBACK_REQUEST', '反馈请求', 'feedback', '课程反馈邀请', '您好 {{customer_name}},您的孩子 {{student_name}} 在 {{course_name}} 课程中表现很棒!请为本次课程打分并留下宝贵意见。', '{"customer_name": "客户姓名", "student_name": "学员姓名", "course_name": "课程名称"}', 'course');

模板引擎服务

<?php
// niucloud/app/service/MessageTemplateService.php

namespace app\service;

use think\facade\Db;
use think\facade\Cache;

class MessageTemplateService 
{
    /**
     * 根据模板代码生成消息
     * @param string $templateCode 模板代码
     * @param array $variables 变量数据
     * @param array $options 额外选项
     * @return array
     */
    public function generateMessage($templateCode, $variables = [], $options = []) 
    {
        $template = $this->getTemplate($templateCode);
        if (!$template) {
            throw new \Exception("消息模板不存在: {$templateCode}");
        }
        
        return [
            'title' => $this->renderTemplate($template['title_template'], $variables),
            'content' => $this->renderTemplate($template['content_template'], $variables),
            'message_type' => $template['message_type'],
            'business_type' => $template['business_type'],
            'template_code' => $templateCode,
            'variables' => $variables
        ];
    }
    
    /**
     * 获取消息模板
     * @param string $templateCode
     * @return array|null
     */
    private function getTemplate($templateCode) 
    {
        $cacheKey = "message_template_{$templateCode}";
        
        return Cache::remember($cacheKey, function() use ($templateCode) {
            return Db::name('school_message_templates')
                ->where('template_code', $templateCode)
                ->where('is_active', 1)
                ->find();
        }, 600); // 缓存10分钟
    }
    
    /**
     * 渲染模板
     * @param string $template 模板内容
     * @param array $variables 变量
     * @return string
     */
    private function renderTemplate($template, $variables) 
    {
        $pattern = '/\{\{(\w+)\}\}/';
        
        return preg_replace_callback($pattern, function($matches) use ($variables) {
            $key = $matches[1];
            return isset($variables[$key]) ? $variables[$key] : $matches[0];
        }, $template);
    }
    
    /**
     * 发送模板消息
     * @param string $templateCode 模板代码
     * @param int $fromId 发送者ID
     * @param string $fromType 发送者类型
     * @param int $toId 接收者ID
     * @param int $friendId 好友关系ID
     * @param array $variables 模板变量
     * @param array $options 额外选项
     * @return int 消息ID
     */
    public function sendTemplateMessage($templateCode, $fromId, $fromType, $toId, $friendId, $variables = [], $options = []) 
    {
        $messageData = $this->generateMessage($templateCode, $variables, $options);
        
        $messageId = Db::name('school_chat_messages')->insertGetId([
            'from_type' => $fromType,
            'from_id' => $fromId,
            'to_id' => $toId,
            'friend_id' => $friendId,
            'message_type' => $messageData['message_type'],
            'title' => $messageData['title'],
            'content' => $messageData['content'],
            'business_type' => $messageData['business_type'],
            'business_id' => $options['business_id'] ?? null,
            'is_read' => 0,
            'created_at' => date('Y-m-d H:i:s')
        ]);
        
        // 如果开启了微信推送
        if (!empty($options['wechat_push']) && !empty($options['openid'])) {
            $this->sendWechatTemplateMessage($messageData, $options['openid'], $options);
        }
        
        return $messageId;
    }
    
    /**
     * 发送微信模板消息
     * @param array $messageData 消息数据
     * @param string $openid 微信openid
     * @param array $options 选项
     */
    private function sendWechatTemplateMessage($messageData, $openid, $options = []) 
    {
        // 调用微信推送服务
        $wechatService = new WechatPushService();
        $wechatService->sendTemplateMessage($openid, $messageData, $options);
    }
}

3. 微信公众号模板消息推送功能

设计目标:集成微信公众号模板消息,实现消息的即时推送通知

微信推送服务

<?php
// niucloud/app/service/WechatPushService.php

namespace app\service;

use think\facade\Cache;
use think\facade\Config;

class WechatPushService 
{
    private $appId;
    private $appSecret;
    private $templateId;
    
    public function __construct() 
    {
        $this->appId = Config::get('wechat.app_id');
        $this->appSecret = Config::get('wechat.app_secret');
        $this->templateId = Config::get('wechat.template_id.message_notify');
    }
    
    /**
     * 发送模板消息
     * @param string $openid 用户openid
     * @param array $messageData 消息数据
     * @param array $options 选项
     * @return bool
     */
    public function sendTemplateMessage($openid, $messageData, $options = []) 
    {
        try {
            $accessToken = $this->getAccessToken();
            $url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={$accessToken}";
            
            $data = [
                'touser' => $openid,
                'template_id' => $this->templateId,
                'url' => $options['jump_url'] ?? '',
                'miniprogram' => [
                    'appid' => Config::get('wechat.mini_app_id', ''),
                    'pagepath' => $this->buildMiniProgramPath($messageData, $options)
                ],
                'data' => [
                    'first' => ['value' => $messageData['title']],
                    'keyword1' => ['value' => $messageData['message_type_text'] ?? '系统消息'],
                    'keyword2' => ['value' => date('Y-m-d H:i:s')],
                    'remark' => ['value' => mb_substr($messageData['content'], 0, 100) . '...']
                ]
            ];
            
            $result = $this->httpPost($url, json_encode($data, JSON_UNESCAPED_UNICODE));
            $response = json_decode($result, true);
            
            return isset($response['errcode']) && $response['errcode'] === 0;
            
        } catch (\Exception $e) {
            // 记录错误日志
            trace('微信模板消息发送失败: ' . $e->getMessage(), 'error');
            return false;
        }
    }
    
    /**
     * 构建小程序页面路径
     * @param array $messageData 消息数据
     * @param array $options 选项
     * @return string
     */
    private function buildMiniProgramPath($messageData, $options) 
    {
        $basePath = 'pages-student/messages/index';
        
        // 根据消息类型构建不同的跳转路径
        if (!empty($options['business_id'])) {
            $routeMap = [
                'order' => 'pages-common/order/detail',
                'student_courses' => 'pages-student/courses/detail',
                'person_course_schedule' => 'pages-student/schedule/detail'
            ];
            
            if (isset($routeMap[$messageData['message_type']])) {
                return $routeMap[$messageData['message_type']] . '?id=' . $options['business_id'];
            }
        }
        
        return $basePath . '?student_id=' . $options['student_id'];
    }
    
    /**
     * 获取微信访问令牌
     * @return string
     */
    private function getAccessToken() 
    {
        $cacheKey = 'wechat_access_token';
        
        return Cache::remember($cacheKey, function() {
            $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}";
            $result = file_get_contents($url);
            $data = json_decode($result, true);
            
            if (isset($data['access_token'])) {
                return $data['access_token'];
            }
            
            throw new \Exception('获取微信访问令牌失败: ' . json_encode($data));
        }, 7000); // 缓存约2小时
    }
    
    /**
     * HTTP POST请求
     * @param string $url
     * @param string $data
     * @return string
     */
    private function httpPost($url, $data) 
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json; charset=utf-8'
        ]);
        
        $result = curl_exec($ch);
        curl_close($ch);
        
        return $result;
    }
}

4. 搜索功能完整实现(含时间筛选)

后端搜索API

<?php
// 在消息API控制器中添加搜索方法

/**
 * 搜索消息
 * @param int $student_id 学员ID
 * @param string $keyword 搜索关键词
 * @param string $message_type 消息类型
 * @param string $start_date 开始日期
 * @param string $end_date 结束日期
 * @param int $page 页码
 * @param int $limit 每页数量
 */
public function searchMessages($student_id, $keyword = '', $message_type = '', $start_date = '', $end_date = '', $page = 1, $limit = 10) 
{
    $where = [
        ['to_id', '=', $student_id],
        ['delete_time', '=', 0]
    ];
    
    // 关键词搜索
    if (!empty($keyword)) {
        $where[] = ['title|content', 'like', "%{$keyword}%"];
    }
    
    // 消息类型筛选
    if (!empty($message_type) && $message_type !== 'all') {
        $where[] = ['message_type', '=', $message_type];
    }
    
    // 时间范围筛选
    if (!empty($start_date)) {
        $where[] = ['created_at', '>=', $start_date . ' 00:00:00'];
    }
    
    if (!empty($end_date)) {
        $where[] = ['created_at', '<=', $end_date . ' 23:59:59'];
    }
    
    $list = Db::name('school_chat_messages')
        ->alias('cm')
        ->leftJoin('school_personnel sp', 'cm.from_id = sp.id AND cm.from_type = "personnel"')
        ->where($where)
        ->field([
            'cm.*',
            'CASE 
                WHEN cm.from_type = "personnel" THEN sp.name 
                WHEN cm.from_type = "system" THEN "系统" 
                ELSE "客户" 
            END as from_name'
        ])
        ->order('cm.created_at DESC')
        ->paginate([
            'list_rows' => $limit,
            'page' => $page
        ]);
        
    return [
        'list' => $list->items(),
        'total' => $list->total(),
        'has_more' => $list->hasMore()
    ];
}

前端搜索组件

<!-- 在消息页面添加搜索区域 -->
<view class="search_section">
    <view class="search_box">
        <input 
            type="text" 
            placeholder="搜索消息标题或内容..." 
            v-model="searchForm.keyword"
            @confirm="performSearch"
            class="search_input"
        />
        <view class="search_filters">
            <picker mode="date" @change="onStartDateChange" :value="searchForm.start_date">
                <view class="date_picker">
                    开始日期: {{ searchForm.start_date || '请选择' }}
                </view>
            </picker>
            
            <picker mode="date" @change="onEndDateChange" :value="searchForm.end_date">
                <view class="date_picker">
                    结束日期: {{ searchForm.end_date || '请选择' }}
                </view>
            </picker>
            
            <view class="search_button" @click="performSearch">搜索</view>
            <view class="reset_button" @click="resetSearch">重置</view>
        </view>
    </view>
</view>
// 搜索相关数据和方法
data() {
    return {
        searchForm: {
            keyword: '',
            start_date: '',
            end_date: ''
        },
        searchTimer: null,
        // ... 其他数据
    }
},

methods: {
    performSearch() {
        clearTimeout(this.searchTimer);
        this.searchTimer = setTimeout(() => {
            this.currentPage = 1;
            this.searchMessages();
        }, 500);
    },
    
    async searchMessages() {
        this.loading = true;
        try {
            const response = await apiRoute.searchStudentMessages({
                student_id: this.studentId,
                keyword: this.searchForm.keyword,
                message_type: this.activeType === 'all' ? '' : this.activeType,
                start_date: this.searchForm.start_date,
                end_date: this.searchForm.end_date,
                page: this.currentPage,
                limit: 10
            });
            
            if (response && response.code === 1) {
                const apiData = response.data;
                const newList = this.formatMessageList(apiData.list || []);
                
                if (this.currentPage === 1) {
                    this.messagesList = newList;
                } else {
                    this.messagesList = [...this.messagesList, ...newList];
                }
                
                this.hasMore = apiData.has_more || false;
                this.applyTypeFilter();
            }
        } catch (error) {
            console.error('搜索消息失败:', error);
        } finally {
            this.loading = false;
        }
    },
    
    onStartDateChange(e) {
        this.searchForm.start_date = e.detail.value;
    },
    
    onEndDateChange(e) {
        this.searchForm.end_date = e.detail.value;
    },
    
    resetSearch() {
        this.searchForm = {
            keyword: '',
            start_date: '',
            end_date: ''
        };
        this.currentPage = 1;
        this.loadMessages();
    }
}

5. 用户身份统一优化(to_type字段方案)

数据库修改

-- 添加接收者类型字段
ALTER TABLE `school_chat_messages` 
ADD COLUMN `to_type` enum('personnel','customer','student') NOT NULL DEFAULT 'customer' COMMENT '接收者类型' 
AFTER `to_id`;

-- 创建复合索引提升查询性能
ALTER TABLE `school_chat_messages` 
ADD INDEX `idx_to_id_type_time` (`to_id`, `to_type`, `created_at`),
ADD INDEX `idx_to_type_read` (`to_type`, `is_read`);

-- 数据迁移:根据现有数据推断to_type值
-- 这里需要根据实际的业务逻辑来更新数据
UPDATE school_chat_messages 
SET to_type = 'customer' 
WHERE to_type = 'customer'; -- 默认设置,需要根据实际情况调整

查询优化

<?php
// 优化后的消息查询
public function getMessagesByUser($userId, $userType, $messageType = '', $page = 1, $limit = 10) 
{
    $where = [
        ['to_id', '=', $userId],
        ['to_type', '=', $userType],
        ['delete_time', '=', 0]
    ];
    
    if (!empty($messageType) && $messageType !== 'all') {
        $where[] = ['message_type', '=', $messageType];
    }
    
    return Db::name('school_chat_messages')
        ->where($where)
        ->order('created_at DESC')
        ->paginate([
            'list_rows' => $limit,
            'page' => $page
        ]);
}

第二阶段:性能优化和扩展功能(中优先级)

  1. 消息队列机制:使用Redis队列处理大量消息发送
  2. 数据分片策略:按月份分表存储历史消息
  3. 缓存策略优化:热点消息和统计数据缓存
  4. API接口优化:批量查询、分页优化

实施时间线

  • 第1周:消息路由系统 + to_type字段优化
  • 第2周:消息模板系统 + 搜索功能
  • 第3周:微信推送功能集成
  • 第4周:测试、优化和部署

这个架构优化方案完全基于你的需求和回复制定,可以立即开始实施。每个功能都有完整的代码实现,你觉得哪个部分需要进一步细化?