62 KiB
消息管理数据库分析报告(移动端+管理端)
📋 需求分析
第一部分:UniApp移动端消息管理
基于 pages-student/messages/index.vue 页面分析,移动端消息管理需要实现:
- 消息列表展示:显示员工/学员接收的所有消息 ✅
- 消息类型筛选:按消息类型进行分类筛选 ✅
- 消息已读状态:标记消息已读/未读 ✅
- 消息数量统计:显示未读消息数量和总消息数 ✅
- 业务页面跳转:根据消息类型跳转到对应业务页面 ❌(需要完善)
- 消息搜索功能:关键词搜索消息内容 ❌(需要添加)
第二部分:Admin管理端客户详情消息展示
基于 admin/src/app/views/customer_resources/components/UserProfile.vue 分析,管理端需要在客户详情中展示:
- 消息历史记录:显示与该客户的所有消息记录 ❌(需要添加)
- 消息统计信息:已读/未读消息数量统计 ❌(需要添加)
- 消息时间轴:按时间顺序展示消息流 ❌(需要添加)
- 消息类型展示:不同类型消息的分类展示 ❌(需要添加)
🗄️ 数据库表结构分析
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 |
课程安排 | 课程安排页 | 跳转到课程安排 |
📊 功能状态总结
✅ 已完成的功能
- 数据库结构:消息存储、已读状态、业务关联字段完整
- 消息类型:枚举值与前端完全匹配
- 移动端基础功能:消息列表、类型筛选、已读标记
- 管理端基础架构:客户详情页面框架完整
❌ 需要完善的功能
- 移动端业务跳转:根据消息类型跳转到对应业务页面
- 移动端搜索功能:消息内容和标题的关键词搜索
- 管理端消息展示:在客户详情中增加消息记录标签页
- 后端API接口:客户消息列表和统计接口
🎯 实施优先级
高优先级(立即实施):
- 完善移动端业务页面跳转逻辑
- 在管理端客户详情中添加消息标签页
中优先级(后续实施):
- 添加移动端搜索功能
- 完善管理端消息统计展示
低优先级(可选):
- 添加数据库统计字段和触发器
- 消息推送和实时通知功能
❓ 待确认问题
基于当前分析,以下问题需要进一步确认:
1. 消息接收者身份确认
问题:消息管理系统中的接收者身份定义
- UniApp移动端是否同时支持员工和学员接收消息?
- 当前
school_chat_messages.to_id字段存储的是什么类型的用户ID? - 员工和学员的消息是否需要分别处理?
建议确认方案:
- 明确
to_id字段的用户类型(员工ID还是客户资源ID)目前的设计是员工和学员都会在这个字段存,如果不合理你可以在下一个版本的文档计划中告知我如何修改 - 确认
from_type和to_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_id和business_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. 下一版本优化计划
架构优化建议
- 消息路由系统:建立统一的消息路由管理
- 消息模板系统:支持动态消息内容生成
- 消息队列机制:支持延时发送和批量处理
- 用户身份统一:解决多用户类型的身份识别问题
性能优化建议
- 数据分片:按时间维度分表存储历史消息
- 缓存策略:Redis缓存热点消息和统计数据
- 索引优化:基于查询模式优化数据库索引
- 异步处理:消息发送和统计更新异步化
这个详细的实现方案基于你的回复制定,可以立即开始实施。如果还有其他细节需要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
]);
}
第二阶段:性能优化和扩展功能(中优先级)
- 消息队列机制:使用Redis队列处理大量消息发送
- 数据分片策略:按月份分表存储历史消息
- 缓存策略优化:热点消息和统计数据缓存
- API接口优化:批量查询、分页优化
实施时间线
- 第1周:消息路由系统 + to_type字段优化
- 第2周:消息模板系统 + 搜索功能
- 第3周:微信推送功能集成
- 第4周:测试、优化和部署
这个架构优化方案完全基于你的需求和回复制定,可以立即开始实施。每个功能都有完整的代码实现,你觉得哪个部分需要进一步细化?