Browse Source

通知消息

skyline 3 months ago
parent
commit
c507c5a9a4

+ 491 - 0
admin-web/src/layout/navBars/breadcrumb/notificationCenter.vue

@@ -0,0 +1,491 @@
+<template>
+    <div class="notification-center">
+        <!-- 消息图标入口 -->
+        <div class="layout-navbars-breadcrumb-user-icon" @click="openDrawer">
+            <el-badge :value="unreadCount" :max="99" :hidden="!hasUnread">
+                <el-icon title="消息通知">
+                    <ele-Bell />
+                </el-icon>
+            </el-badge>
+        </div>
+
+        <!-- 消息通知抽屉 -->
+        <el-drawer
+            v-model="state.drawerVisible"
+            title="消息通知中心"
+            direction="rtl"
+            size="450px"
+            :destroy-on-close="false"
+        >
+            <!-- 消息类型标签页 -->
+            <el-tabs v-model="state.activeTab" @tab-change="handleTabChange">
+                <el-tab-pane label="全部" name="all">
+                    <template #label>
+                        <span>全部</span>
+                        <el-badge :value="unreadCount" :max="99" :hidden="!hasUnread" class="tab-badge" />
+                    </template>
+                </el-tab-pane>
+                <el-tab-pane label="系统通知" name="1">
+                    <template #label>
+                        <span>系统通知</span>
+                        <el-badge :value="unreadCountByType[1]" :max="99" :hidden="!unreadCountByType[1]" class="tab-badge" />
+                    </template>
+                </el-tab-pane>
+                <el-tab-pane label="站内信" name="2">
+                    <template #label>
+                        <span>站内信</span>
+                        <el-badge :value="unreadCountByType[2]" :max="99" :hidden="!unreadCountByType[2]" class="tab-badge" />
+                    </template>
+                </el-tab-pane>
+                <el-tab-pane label="待办事项" name="3">
+                    <template #label>
+                        <span>待办事项</span>
+                        <el-badge :value="unreadCountByType[3]" :max="99" :hidden="!unreadCountByType[3]" class="tab-badge" />
+                    </template>
+                </el-tab-pane>
+                <el-tab-pane label="公告" name="4">
+                    <template #label>
+                        <span>公告</span>
+                        <el-badge :value="unreadCountByType[4]" :max="99" :hidden="!unreadCountByType[4]" class="tab-badge" />
+                    </template>
+                </el-tab-pane>
+            </el-tabs>
+
+            <!-- 操作栏 -->
+            <div class="action-bar">
+                <el-button type="primary" link @click="markAllRead" :disabled="!hasCurrentUnread">
+                    <el-icon><ele-Check /></el-icon>
+                    全部已读
+                </el-button>
+                <el-button type="danger" link @click="deleteSelected" :disabled="state.selectedIds.length === 0">
+                    <el-icon><ele-Delete /></el-icon>
+                    删除选中 ({{ state.selectedIds.length }})
+                </el-button>
+            </div>
+
+            <!-- 消息列表 -->
+            <div class="message-list" v-loading="loading">
+                <template v-if="messageList.length > 0">
+                    <el-checkbox-group v-model="state.selectedIds">
+                        <div 
+                            v-for="message in messageList" 
+                            :key="message.id" 
+                            class="message-item"
+                            :class="{ 'is-unread': message.status === 0 }"
+                            @click="handleMessageClick(message)"
+                        >
+                            <div class="message-checkbox" @click.stop>
+                                <el-checkbox :label="message.id" />
+                            </div>
+                            <div class="message-content">
+                                <div class="message-header">
+                                    <span class="message-title">
+                                        <el-tag 
+                                            v-if="message.priority > 0" 
+                                            :type="getPriorityType(message.priority)" 
+                                            size="small"
+                                            class="priority-tag"
+                                        >
+                                            {{ getPriorityLabel(message.priority) }}
+                                        </el-tag>
+                                        {{ message.title }}
+                                    </span>
+                                    <span class="message-time">{{ formatTime(message.createTime) }}</span>
+                                </div>
+                                <div class="message-body">{{ message.content }}</div>
+                                <div class="message-footer">
+                                    <el-tag size="small" type="info">{{ getTypeLabel(message.type) }}</el-tag>
+                                    <span class="message-sender">{{ message.senderName }}</span>
+                                </div>
+                            </div>
+                            <div class="message-status">
+                                <el-icon v-if="message.status === 0" class="unread-dot"><ele-MoreFilled /></el-icon>
+                            </div>
+                        </div>
+                    </el-checkbox-group>
+                </template>
+                <el-empty v-else description="暂无消息" />
+            </div>
+
+            <!-- 分页 -->
+            <div class="pagination-bar" v-if="pagination.total > pagination.pageSize">
+                <el-pagination
+                    v-model:current-page="pagination.pageNum"
+                    v-model:page-size="pagination.pageSize"
+                    :total="pagination.total"
+                    :page-sizes="[10, 20, 50]"
+                    layout="total, sizes, prev, pager, next"
+                    @size-change="handleSizeChange"
+                    @current-change="handlePageChange"
+                />
+            </div>
+        </el-drawer>
+
+        <!-- 消息详情对话框 -->
+        <el-dialog 
+            v-model="state.detailVisible" 
+            :title="state.currentMessage?.title || '消息详情'"
+            width="500px"
+        >
+            <div class="message-detail" v-if="state.currentMessage">
+                <div class="detail-header">
+                    <el-tag :type="getTypeColor(state.currentMessage.type)">
+                        {{ getTypeLabel(state.currentMessage.type) }}
+                    </el-tag>
+                    <el-tag 
+                        v-if="state.currentMessage.priority > 0" 
+                        :type="getPriorityType(state.currentMessage.priority)"
+                    >
+                        {{ getPriorityLabel(state.currentMessage.priority) }}
+                    </el-tag>
+                </div>
+                <div class="detail-content">{{ state.currentMessage.content }}</div>
+                <div class="detail-footer">
+                    <span>发送者:{{ state.currentMessage.senderName }}</span>
+                    <span>时间:{{ state.currentMessage.createTime }}</span>
+                </div>
+            </div>
+            <template #footer>
+                <el-button @click="state.detailVisible = false">关闭</el-button>
+                <el-button 
+                    type="danger" 
+                    @click="deleteCurrentMessage"
+                >删除</el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="NotificationCenter">
+import { reactive, computed, onMounted, onUnmounted } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useMessageStore, MessageTypeLabel, PriorityLabel, MessageItem } from '/@/stores/message';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+const messageStore = useMessageStore();
+const { unreadCount, unreadCountByType, messageList, pagination, loading } = storeToRefs(messageStore);
+
+const state = reactive({
+    drawerVisible: false,
+    detailVisible: false,
+    activeTab: 'all',
+    selectedIds: [] as number[],
+    currentMessage: null as MessageItem | null
+});
+
+// 计算属性
+const hasUnread = computed(() => unreadCount.value > 0);
+const hasCurrentUnread = computed(() => {
+    if (state.activeTab === 'all') return hasUnread.value;
+    const type = parseInt(state.activeTab);
+    return (unreadCountByType.value[type] || 0) > 0;
+});
+
+// 打开抽屉
+const openDrawer = async () => {
+    await initIfNeeded();
+    state.drawerVisible = true;
+    state.selectedIds = [];
+    loadMessages();
+};
+
+// 加载消息列表
+const loadMessages = () => {
+    const type = state.activeTab === 'all' ? undefined : parseInt(state.activeTab);
+    messageStore.setCurrentType(type ?? null);
+    messageStore.fetchMessageList({ type });
+};
+
+// 切换标签
+const handleTabChange = () => {
+    state.selectedIds = [];
+    loadMessages();
+};
+
+// 分页变化
+const handlePageChange = (page: number) => {
+    pagination.value.pageNum = page;
+    loadMessages();
+};
+
+const handleSizeChange = (size: number) => {
+    pagination.value.pageSize = size;
+    pagination.value.pageNum = 1;
+    loadMessages();
+};
+
+// 点击消息
+const handleMessageClick = (message: MessageItem) => {
+    state.currentMessage = message;
+    state.detailVisible = true;
+    
+    // 标记已读
+    if (message.status === 0) {
+        messageStore.markAsRead(message.id);
+    }
+};
+
+// 全部标记已读
+const markAllRead = async () => {
+    try {
+        const type = state.activeTab === 'all' ? undefined : parseInt(state.activeTab);
+        await messageStore.markAllAsRead(type);
+        ElMessage.success('已全部标记为已读');
+    } catch (error) {
+        ElMessage.error('操作失败');
+    }
+};
+
+// 删除选中
+const deleteSelected = async () => {
+    if (state.selectedIds.length === 0) return;
+    
+    try {
+        await ElMessageBox.confirm(
+            `确定要删除选中的 ${state.selectedIds.length} 条消息吗?`,
+            '删除确认',
+            { type: 'warning' }
+        );
+        
+        await messageStore.batchDeleteMessage(state.selectedIds);
+        state.selectedIds = [];
+        ElMessage.success('删除成功');
+    } catch (error) {
+        if (error !== 'cancel') {
+            ElMessage.error('删除失败');
+        }
+    }
+};
+
+// 删除当前消息
+const deleteCurrentMessage = async () => {
+    if (!state.currentMessage) return;
+    
+    try {
+        await ElMessageBox.confirm('确定要删除这条消息吗?', '删除确认', { type: 'warning' });
+        await messageStore.deleteMessage(state.currentMessage.id);
+        state.detailVisible = false;
+        state.currentMessage = null;
+        ElMessage.success('删除成功');
+    } catch (error) {
+        if (error !== 'cancel') {
+            ElMessage.error('删除失败');
+        }
+    }
+};
+
+// 格式化时间
+const formatTime = (time: string) => {
+    if (!time) return '';
+    const date = new Date(time);
+    const now = new Date();
+    const diff = now.getTime() - date.getTime();
+    
+    if (diff < 60000) return '刚刚';
+    if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
+    if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
+    if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
+    
+    return time.split(' ')[0];
+};
+
+// 获取类型标签
+const getTypeLabel = (type: number) => MessageTypeLabel[type] || '未知';
+const getTypeColor = (type: number) => {
+    const colors: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'warning', 4: 'info' };
+    return colors[type] || '';
+};
+
+// 获取优先级
+const getPriorityLabel = (priority: number) => PriorityLabel[priority] || '';
+const getPriorityType = (priority: number) => {
+    const types: Record<number, string> = { 1: 'warning', 2: 'danger' };
+    return types[priority] || '';
+};
+
+// 是否已初始化
+let initialized = false;
+
+// 延迟初始化(避免页面加载时立即请求)
+const initIfNeeded = async () => {
+    if (!initialized) {
+        initialized = true;
+        await messageStore.init();
+    }
+};
+
+// 生命周期 - 不在 onMounted 时立即初始化,而是在用户点击时初始化
+onMounted(() => {
+    // 延迟 2 秒后初始化,避免页面加载时立即请求
+    setTimeout(() => {
+        initIfNeeded();
+    }, 2000);
+});
+
+onUnmounted(() => {
+    messageStore.cleanup();
+});
+</script>
+
+<style scoped lang="scss">
+.notification-center {
+    .layout-navbars-breadcrumb-user-icon {
+        padding: 0 10px;
+        cursor: pointer;
+        color: var(--next-bg-topBarColor);
+        height: 50px;
+        line-height: 50px;
+        display: flex;
+        align-items: center;
+
+        &:hover {
+            background: var(--next-color-user-hover);
+        }
+    }
+}
+
+.tab-badge {
+    margin-left: 4px;
+    
+    :deep(.el-badge__content) {
+        transform: scale(0.8);
+    }
+}
+
+.action-bar {
+    display: flex;
+    justify-content: space-between;
+    padding: 10px 0;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+    margin-bottom: 10px;
+}
+
+.message-list {
+    min-height: 300px;
+    max-height: calc(100vh - 280px);
+    overflow-y: auto;
+}
+
+.message-item {
+    display: flex;
+    align-items: flex-start;
+    padding: 12px;
+    border-radius: 8px;
+    margin-bottom: 8px;
+    cursor: pointer;
+    transition: all 0.3s;
+    border: 1px solid var(--el-border-color-lighter);
+
+    &:hover {
+        background: var(--el-fill-color-light);
+    }
+
+    &.is-unread {
+        background: var(--el-color-primary-light-9);
+        border-color: var(--el-color-primary-light-7);
+        
+        .message-title {
+            font-weight: 600;
+        }
+    }
+
+    .message-checkbox {
+        margin-right: 8px;
+        padding-top: 2px;
+    }
+
+    .message-content {
+        flex: 1;
+        min-width: 0;
+    }
+
+    .message-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 6px;
+    }
+
+    .message-title {
+        font-size: 14px;
+        color: var(--el-text-color-primary);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        
+        .priority-tag {
+            margin-right: 4px;
+        }
+    }
+
+    .message-time {
+        font-size: 12px;
+        color: var(--el-text-color-secondary);
+        flex-shrink: 0;
+        margin-left: 8px;
+    }
+
+    .message-body {
+        font-size: 13px;
+        color: var(--el-text-color-regular);
+        line-height: 1.5;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+        margin-bottom: 6px;
+    }
+
+    .message-footer {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        font-size: 12px;
+        color: var(--el-text-color-secondary);
+    }
+
+    .message-status {
+        width: 20px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        .unread-dot {
+            color: var(--el-color-primary);
+            font-size: 8px;
+        }
+    }
+}
+
+.pagination-bar {
+    margin-top: 16px;
+    display: flex;
+    justify-content: center;
+}
+
+.message-detail {
+    .detail-header {
+        display: flex;
+        gap: 8px;
+        margin-bottom: 16px;
+    }
+
+    .detail-content {
+        line-height: 1.8;
+        color: var(--el-text-color-primary);
+        padding: 16px;
+        background: var(--el-fill-color-lighter);
+        border-radius: 8px;
+        margin-bottom: 16px;
+        min-height: 100px;
+    }
+
+    .detail-footer {
+        display: flex;
+        justify-content: space-between;
+        font-size: 13px;
+        color: var(--el-text-color-secondary);
+    }
+}
+</style>

+ 3 - 17
admin-web/src/layout/navBars/breadcrumb/toolbar.vue

@@ -65,23 +65,8 @@
       </div>
     </template>
 
-    <!--
-
-        <div class="layout-navbars-breadcrumb-user-icon">
-          <el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
-            <template #reference>
-              <el-badge :is-dot="true">
-                <el-icon :title="$t('message.user.title4')">
-                  <ele-Bell/>
-                </el-icon>
-              </el-badge>
-            </template>
-            <template #default>
-              <Notification/>
-            </template>
-          </el-popover>
-        </div>
-    -->
+    <!-- 消息通知中心 -->
+    <NotificationCenter />
 
     <div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
       <el-icon class="el-icon--right">
@@ -146,6 +131,7 @@ import {$body} from "/@/utils/request";
 
 // 引入组件
 const Notification = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/notification.vue'));
+const NotificationCenter = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/notificationCenter.vue'));
 const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
 
 // 定义变量内容

+ 61 - 18
admin-web/src/router/route.ts

@@ -332,7 +332,7 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                     isAffix: false,
                     isIframe: false,
                     icon: 'ele-Tools',
-                    perm: "user.list,role.list,dict.list",
+                    perm: "user.list,role.list,dict.list,notice.list,message.list,log.list",
                 },
                 children: [
                     {
@@ -410,26 +410,69 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                             perm: "",
                         },
                     },
+                    {
+                        path: '/org/notice',
+                        name: 'adminNotice',
+                        component: () => import('/@/views/admin/notice/index.vue'),
+                        meta: {
+                            title: '系统公告',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            icon: 'ele-Bell',
+                            perm: "",
+                        },
+                    },
+                    {
+                        path: '/org/message',
+                        name: 'adminMessage',
+                        component: () => import('/@/views/admin/message/index.vue'),
+                        meta: {
+                            title: '消息管理',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            icon: 'ele-Message',
+                            perm: "",
+                        },
+                    },
+                    {
+                        path: '/org/template',
+                        name: 'adminTemplate',
+                        component: () => import('/@/views/admin/template/index.vue'),
+                        meta: {
+                            title: '消息模板',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            icon: 'ele-Files',
+                            perm: "",
+                        },
+                    },
+                    {
+                        path: '/org/log',
+                        name: 'adminLog',
+                        component: () => import('/@/views/admin/log/opt/index.vue'),
+                        meta: {
+                            title: '操作日志',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            icon: 'ele-Document',
+                            perm: "",
+                        },
+                    },
                 ]
             },
-            /*         {
-                         path: '/optList',
-                         name: 'adminOptList',
-                         component: () => import('/@/views/admin/log/opt/index.vue'),
-                         meta: {
-                             title: '操作日志',
-                             isLink: '',
-                             isHide: false,
-                             isKeepAlive: true,
-                             isAffix: false,
-                             isIframe: false,
-
-                             icon: 'ele-Cpu',
-
-                         }
-                     },*/
 
         ],
     },
-
 ]

+ 401 - 0
admin-web/src/stores/message.ts

@@ -0,0 +1,401 @@
+import { defineStore } from 'pinia';
+import { MessageApi, MessageMockData } from '/@/utils/messageApi';
+
+// 消息类型
+export interface MessageItem {
+    id: number;
+    title: string;
+    content: string;
+    type: number;        // 1-系统通知,2-站内信,3-待办事项,4-公告通知
+    senderId: number;
+    senderName: string;
+    receiverId: number;
+    receiverName: string;
+    status: number;      // 0-未读,1-已读,2-已删除
+    priority: number;    // 0-普通,1-重要,2-紧急
+    bizType?: string;
+    bizId?: number;
+    readTime?: string;
+    createTime: string;
+}
+
+// 消息类型枚举
+export const MessageType = {
+    SYSTEM: 1,      // 系统通知
+    LETTER: 2,      // 站内信
+    TODO: 3,        // 待办事项
+    NOTICE: 4       // 公告通知
+};
+
+// 消息类型标签
+export const MessageTypeLabel: Record<number, string> = {
+    1: '系统通知',
+    2: '站内信',
+    3: '待办事项',
+    4: '公告通知'
+};
+
+// 优先级标签
+export const PriorityLabel: Record<number, string> = {
+    0: '普通',
+    1: '重要',
+    2: '紧急'
+};
+
+// 优先级颜色
+export const PriorityColor: Record<number, string> = {
+    0: '',
+    1: 'warning',
+    2: 'danger'
+};
+
+// 是否使用Mock数据
+const useMock = true;
+
+/**
+ * 消息通知 Store
+ */
+export const useMessageStore = defineStore('message', {
+    state: () => ({
+        // 未读消息数量
+        unreadCount: 0,
+        // 各类型未读数量
+        unreadCountByType: {
+            1: 0, // 系统通知
+            2: 0, // 站内信
+            3: 0, // 待办事项
+            4: 0  // 公告通知
+        } as Record<number, number>,
+        // 最新未读消息(用于下拉预览)
+        latestMessages: [] as MessageItem[],
+        // 消息列表(完整分页列表)
+        messageList: [] as MessageItem[],
+        // 分页信息
+        pagination: {
+            pageNum: 1,
+            pageSize: 10,
+            total: 0
+        },
+        // 当前选中的消息类型筛选
+        currentType: null as number | null,
+        // 轮询定时器
+        pollingTimer: null as any,
+        // 是否正在加载
+        loading: false
+    }),
+    
+    getters: {
+        // 是否有未读消息
+        hasUnread: (state) => state.unreadCount > 0,
+        // 获取某类型的未读数量
+        getUnreadByType: (state) => (type: number) => state.unreadCountByType[type] || 0
+    },
+    
+    actions: {
+        /**
+         * 获取未读消息数量
+         */
+        async fetchUnreadCount() {
+            try {
+                if (useMock) {
+                    this.unreadCount = MessageMockData.unreadCount;
+                    this.unreadCountByType = { ...MessageMockData.unreadCountByType };
+                    return;
+                }
+                
+                const count = await MessageApi.getUnreadCount() as number;
+                this.unreadCount = count;
+                
+                const countByType = await MessageApi.getUnreadCountByType() as Record<number, number>;
+                this.unreadCountByType = countByType;
+            } catch (error) {
+                console.error('获取未读消息数量失败:', error);
+                // 使用Mock数据
+                this.unreadCount = MessageMockData.unreadCount;
+                this.unreadCountByType = { ...MessageMockData.unreadCountByType };
+            }
+        },
+        
+        /**
+         * 获取最新未读消息(用于下拉预览)
+         */
+        async fetchLatestMessages(limit: number = 5) {
+            try {
+                if (useMock) {
+                    this.latestMessages = MessageMockData.list
+                        .filter(m => m.status === 0)
+                        .slice(0, limit);
+                    return;
+                }
+                
+                const messages = await MessageApi.getLatestUnread(limit) as MessageItem[];
+                this.latestMessages = messages;
+            } catch (error) {
+                console.error('获取最新消息失败:', error);
+                this.latestMessages = MessageMockData.list
+                    .filter(m => m.status === 0)
+                    .slice(0, limit);
+            }
+        },
+        
+        /**
+         * 获取消息列表(分页)
+         */
+        async fetchMessageList(params?: {
+            pageNum?: number;
+            pageSize?: number;
+            type?: number;
+            status?: number;
+            title?: string;
+        }) {
+            this.loading = true;
+            try {
+                const queryParams = {
+                    pageNum: params?.pageNum || this.pagination.pageNum,
+                    pageSize: params?.pageSize || this.pagination.pageSize,
+                    type: params?.type ?? this.currentType ?? undefined,
+                    status: params?.status,
+                    title: params?.title
+                };
+                
+                if (useMock) {
+                    let filteredList = [...MessageMockData.list];
+                    if (queryParams.type) {
+                        filteredList = filteredList.filter(m => m.type === queryParams.type);
+                    }
+                    if (queryParams.status !== undefined) {
+                        filteredList = filteredList.filter(m => m.status === queryParams.status);
+                    }
+                    this.messageList = filteredList;
+                    this.pagination.total = filteredList.length;
+                    return;
+                }
+                
+                const result = await MessageApi.getMessageList(queryParams) as any;
+                this.messageList = result.list || [];
+                this.pagination.total = result.total || 0;
+                this.pagination.pageNum = queryParams.pageNum;
+                this.pagination.pageSize = queryParams.pageSize;
+            } catch (error) {
+                console.error('获取消息列表失败:', error);
+                let filteredList = [...MessageMockData.list];
+                if (params?.type) {
+                    filteredList = filteredList.filter(m => m.type === params.type);
+                }
+                this.messageList = filteredList;
+                this.pagination.total = filteredList.length;
+            } finally {
+                this.loading = false;
+            }
+        },
+        
+        /**
+         * 标记单条消息已读
+         */
+        async markAsRead(messageId: number) {
+            try {
+                if (!useMock) {
+                    await MessageApi.markAsRead(messageId);
+                }
+                
+                // 更新本地状态
+                const message = this.messageList.find(m => m.id === messageId);
+                if (message && message.status === 0) {
+                    message.status = 1;
+                    message.readTime = new Date().toLocaleString();
+                    this.unreadCount = Math.max(0, this.unreadCount - 1);
+                    if (this.unreadCountByType[message.type]) {
+                        this.unreadCountByType[message.type]--;
+                    }
+                }
+                
+                // 更新最新消息列表
+                this.latestMessages = this.latestMessages.filter(m => m.id !== messageId);
+            } catch (error) {
+                console.error('标记已读失败:', error);
+            }
+        },
+        
+        /**
+         * 批量标记已读
+         */
+        async batchMarkAsRead(messageIds: number[]) {
+            try {
+                if (!useMock) {
+                    await MessageApi.batchMarkAsRead(messageIds);
+                }
+                
+                // 更新本地状态
+                messageIds.forEach(id => {
+                    const message = this.messageList.find(m => m.id === id);
+                    if (message && message.status === 0) {
+                        message.status = 1;
+                        message.readTime = new Date().toLocaleString();
+                        this.unreadCount = Math.max(0, this.unreadCount - 1);
+                        if (this.unreadCountByType[message.type]) {
+                            this.unreadCountByType[message.type]--;
+                        }
+                    }
+                });
+                
+                // 更新最新消息列表
+                this.latestMessages = this.latestMessages.filter(m => !messageIds.includes(m.id));
+            } catch (error) {
+                console.error('批量标记已读失败:', error);
+            }
+        },
+        
+        /**
+         * 全部标记已读
+         */
+        async markAllAsRead(type?: number) {
+            try {
+                if (!useMock) {
+                    await MessageApi.markAllAsRead(type);
+                }
+                
+                // 更新本地状态
+                this.messageList.forEach(message => {
+                    if (message.status === 0 && (!type || message.type === type)) {
+                        message.status = 1;
+                        message.readTime = new Date().toLocaleString();
+                    }
+                });
+                
+                if (type) {
+                    this.unreadCount = Math.max(0, this.unreadCount - (this.unreadCountByType[type] || 0));
+                    this.unreadCountByType[type] = 0;
+                } else {
+                    this.unreadCount = 0;
+                    Object.keys(this.unreadCountByType).forEach(key => {
+                        this.unreadCountByType[Number(key)] = 0;
+                    });
+                }
+                
+                // 清空最新消息列表
+                if (!type) {
+                    this.latestMessages = [];
+                } else {
+                    this.latestMessages = this.latestMessages.filter(m => m.type !== type);
+                }
+            } catch (error) {
+                console.error('全部标记已读失败:', error);
+            }
+        },
+        
+        /**
+         * 删除消息
+         */
+        async deleteMessage(messageId: number) {
+            try {
+                if (!useMock) {
+                    await MessageApi.deleteMessage(messageId);
+                }
+                
+                // 从列表中移除
+                const index = this.messageList.findIndex(m => m.id === messageId);
+                if (index > -1) {
+                    const message = this.messageList[index];
+                    if (message.status === 0) {
+                        this.unreadCount = Math.max(0, this.unreadCount - 1);
+                        if (this.unreadCountByType[message.type]) {
+                            this.unreadCountByType[message.type]--;
+                        }
+                    }
+                    this.messageList.splice(index, 1);
+                    this.pagination.total--;
+                }
+                
+                // 从最新消息列表中移除
+                this.latestMessages = this.latestMessages.filter(m => m.id !== messageId);
+            } catch (error) {
+                console.error('删除消息失败:', error);
+            }
+        },
+        
+        /**
+         * 批量删除消息
+         */
+        async batchDeleteMessage(messageIds: number[]) {
+            try {
+                if (!useMock) {
+                    await MessageApi.batchDeleteMessage(messageIds);
+                }
+                
+                // 从列表中移除
+                messageIds.forEach(id => {
+                    const index = this.messageList.findIndex(m => m.id === id);
+                    if (index > -1) {
+                        const message = this.messageList[index];
+                        if (message.status === 0) {
+                            this.unreadCount = Math.max(0, this.unreadCount - 1);
+                            if (this.unreadCountByType[message.type]) {
+                                this.unreadCountByType[message.type]--;
+                            }
+                        }
+                        this.messageList.splice(index, 1);
+                        this.pagination.total--;
+                    }
+                });
+                
+                // 从最新消息列表中移除
+                this.latestMessages = this.latestMessages.filter(m => !messageIds.includes(m.id));
+            } catch (error) {
+                console.error('批量删除消息失败:', error);
+            }
+        },
+        
+        /**
+         * 设置当前筛选类型
+         */
+        setCurrentType(type: number | null) {
+            this.currentType = type;
+        },
+        
+        /**
+         * 启动轮询(定时获取未读消息数量)
+         * 注意:Mock模式下不启动轮询
+         */
+        startPolling(interval: number = 60000) {
+            if (useMock) {
+                // Mock模式下不需要轮询
+                return;
+            }
+            this.stopPolling();
+            this.fetchUnreadCount();
+            this.pollingTimer = setInterval(() => {
+                this.fetchUnreadCount();
+            }, interval);
+        },
+        
+        /**
+         * 停止轮询
+         */
+        stopPolling() {
+            if (this.pollingTimer) {
+                clearInterval(this.pollingTimer);
+                this.pollingTimer = null;
+            }
+        },
+        
+        /**
+         * 初始化(登录后调用)
+         */
+        async init() {
+            await this.fetchUnreadCount();
+            await this.fetchLatestMessages();
+            this.startPolling();
+        },
+        
+        /**
+         * 清理(登出时调用)
+         */
+        cleanup() {
+            this.stopPolling();
+            this.unreadCount = 0;
+            this.unreadCountByType = { 1: 0, 2: 0, 3: 0, 4: 0 };
+            this.latestMessages = [];
+            this.messageList = [];
+        }
+    }
+});

+ 176 - 0
admin-web/src/utils/messageApi.ts

@@ -0,0 +1,176 @@
+import { $get, $body } from '/@/utils/request';
+
+/**
+ * 消息通知相关接口
+ */
+export const MessageApi = {
+    /**
+     * 发送消息
+     */
+    sendMessage(data: {
+        title: string;
+        content: string;
+        type: number;
+        receiverId: number;
+        priority?: number;
+        bizType?: string;
+        bizId?: number;
+    }) {
+        return $body('/message/send', data);
+    },
+
+    /**
+     * 批量发送消息
+     */
+    batchSendMessage(data: {
+        title: string;
+        content: string;
+        type: number;
+        receiverIds: number[];
+        priority?: number;
+    }) {
+        return $body('/message/batchSend', data);
+    },
+
+    /**
+     * 分页查询消息列表
+     */
+    getMessageList(params: {
+        pageNum?: number;
+        pageSize?: number;
+        type?: number;
+        status?: number;
+        title?: string;
+    }) {
+        return $get('/message/list', params);
+    },
+
+    /**
+     * 获取消息详情
+     */
+    getMessageDetail(messageId: number) {
+        return $get(`/message/detail/${messageId}`);
+    },
+
+    /**
+     * 标记消息已读
+     */
+    markAsRead(messageId: number) {
+        return $body(`/message/read/${messageId}`, {});
+    },
+
+    /**
+     * 批量标记已读
+     */
+    batchMarkAsRead(ids: number[]) {
+        return $body('/message/batchRead', { ids });
+    },
+
+    /**
+     * 全部标记已读
+     */
+    markAllAsRead(type?: number) {
+        return $body('/message/readAll' + (type ? `?type=${type}` : ''), {});
+    },
+
+    /**
+     * 删除消息
+     */
+    deleteMessage(messageId: number) {
+        return $body(`/message/delete/${messageId}`, {});
+    },
+
+    /**
+     * 批量删除消息
+     */
+    batchDeleteMessage(ids: number[]) {
+        return $body('/message/batchDelete', { ids });
+    },
+
+    /**
+     * 获取未读消息数量
+     */
+    getUnreadCount() {
+        return $get('/message/unreadCount');
+    },
+
+    /**
+     * 按类型获取未读消息数量
+     */
+    getUnreadCountByType() {
+        return $get('/message/unreadCountByType');
+    },
+
+    /**
+     * 获取最新未读消息
+     */
+    getLatestUnread(limit: number = 5) {
+        return $get('/message/latestUnread', { limit });
+    }
+};
+
+// Mock 数据支持(当后端不可用时)
+export const MessageMockData = {
+    list: [
+        {
+            id: 1,
+            title: '系统升级通知',
+            content: '系统将于2024年1月20日凌晨2:00-4:00进行升级维护,届时系统将暂停服务,请提前做好准备。',
+            type: 1,
+            senderId: 0,
+            senderName: '系统',
+            receiverId: 1,
+            receiverName: '管理员',
+            status: 0,
+            priority: 2,
+            createTime: '2024-01-15 10:00:00'
+        },
+        {
+            id: 2,
+            title: '新功能上线',
+            content: '消息通知中心功能已上线,您可以在此查看所有消息通知。',
+            type: 1,
+            senderId: 0,
+            senderName: '系统',
+            receiverId: 1,
+            receiverName: '管理员',
+            status: 0,
+            priority: 0,
+            createTime: '2024-01-15 09:30:00'
+        },
+        {
+            id: 3,
+            title: '待办事项提醒',
+            content: '您有3个待审批的工单,请及时处理。',
+            type: 3,
+            senderId: 0,
+            senderName: '系统',
+            receiverId: 1,
+            receiverName: '管理员',
+            status: 0,
+            priority: 1,
+            createTime: '2024-01-15 09:00:00'
+        },
+        {
+            id: 4,
+            title: '欢迎使用',
+            content: '欢迎使用洗车管理系统,如有问题请联系管理员。',
+            type: 2,
+            senderId: 0,
+            senderName: '系统',
+            receiverId: 1,
+            receiverName: '管理员',
+            status: 1,
+            priority: 0,
+            createTime: '2024-01-14 15:00:00',
+            readTime: '2024-01-14 16:00:00'
+        }
+    ],
+    unreadCount: 3,
+    unreadCountByType: {
+        1: 2, // 系统通知
+        2: 0, // 站内信
+        3: 1, // 待办事项
+        4: 0  // 公告通知
+    }
+};

+ 292 - 6
admin-web/src/views/admin/log/opt/index.vue

@@ -1,13 +1,299 @@
 <template>
+    <div class="system-log-container">
+        <!-- 搜索栏 -->
+        <el-card class="search-card" shadow="never">
+            <el-form :inline="true" :model="queryParams" class="search-form">
+                <el-form-item label="操作用户">
+                    <el-input v-model="queryParams.operatorName" placeholder="请输入" clearable style="width: 150px" />
+                </el-form-item>
+                <el-form-item label="操作模块">
+                    <el-input v-model="queryParams.module" placeholder="请输入" clearable style="width: 150px" />
+                </el-form-item>
+                <el-form-item label="操作类型">
+                    <el-select v-model="queryParams.operationType" placeholder="请选择" clearable style="width: 150px">
+                        <el-option label="新增" value="CREATE" />
+                        <el-option label="修改" value="UPDATE" />
+                        <el-option label="删除" value="DELETE" />
+                        <el-option label="查询" value="QUERY" />
+                        <el-option label="登录" value="LOGIN" />
+                        <el-option label="登出" value="LOGOUT" />
+                        <el-option label="其他" value="OTHER" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="操作时间">
+                    <el-date-picker
+                        v-model="dateRange"
+                        type="daterange"
+                        range-separator="至"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期"
+                        value-format="YYYY-MM-DD"
+                        style="width: 240px"
+                    />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleQuery">
+                        <el-icon><ele-Search /></el-icon> 查询
+                    </el-button>
+                    <el-button @click="resetQuery">
+                        <el-icon><ele-Refresh /></el-icon> 重置
+                    </el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
 
+        <!-- 数据表格 -->
+        <el-card class="table-card" shadow="never">
+            <template #header>
+                <div class="card-header">
+                    <span>操作日志列表</span>
+                    <el-button type="danger" @click="handleClearLog" :disabled="tableData.length === 0">
+                        <el-icon><ele-Delete /></el-icon> 清空日志
+                    </el-button>
+                </div>
+            </template>
+
+            <el-table v-loading="loading" :data="tableData" border stripe>
+                <el-table-column prop="id" label="ID" width="80" align="center" />
+                <el-table-column prop="operatorName" label="操作用户" width="120" align="center" />
+                <el-table-column prop="module" label="操作模块" width="120" align="center" />
+                <el-table-column prop="operationType" label="操作类型" width="100" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="getOperationTypeColor(row.operationType)">
+                            {{ getOperationTypeLabel(row.operationType) }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="description" label="操作描述" min-width="200" show-overflow-tooltip />
+                <el-table-column prop="requestMethod" label="请求方式" width="90" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="getMethodColor(row.requestMethod)" size="small">{{ row.requestMethod }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="requestUrl" label="请求URL" width="200" show-overflow-tooltip />
+                <el-table-column prop="ip" label="IP地址" width="130" align="center" />
+                <el-table-column prop="costTime" label="耗时(ms)" width="90" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="row.costTime > 1000 ? 'danger' : row.costTime > 500 ? 'warning' : 'success'" size="small">
+                            {{ row.costTime }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="createTime" label="操作时间" width="170" align="center" />
+                <el-table-column label="操作" width="80" align="center" fixed="right">
+                    <template #default="{ row }">
+                        <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <!-- 分页 -->
+            <div class="pagination-container">
+                <el-pagination
+                    v-model:current-page="queryParams.pageNum"
+                    v-model:page-size="queryParams.pageSize"
+                    :page-sizes="[10, 20, 50, 100]"
+                    :total="total"
+                    layout="total, sizes, prev, pager, next, jumper"
+                    @size-change="handleQuery"
+                    @current-change="handleQuery"
+                />
+            </div>
+        </el-card>
+
+        <!-- 详情对话框 -->
+        <el-dialog v-model="detailVisible" title="日志详情" width="700px">
+            <el-descriptions :column="2" border v-if="currentLog">
+                <el-descriptions-item label="操作用户">{{ currentLog.operatorName }}</el-descriptions-item>
+                <el-descriptions-item label="操作模块">{{ currentLog.module }}</el-descriptions-item>
+                <el-descriptions-item label="操作类型">
+                    <el-tag :type="getOperationTypeColor(currentLog.operationType)">
+                        {{ getOperationTypeLabel(currentLog.operationType) }}
+                    </el-tag>
+                </el-descriptions-item>
+                <el-descriptions-item label="请求方式">
+                    <el-tag :type="getMethodColor(currentLog.requestMethod)" size="small">{{ currentLog.requestMethod }}</el-tag>
+                </el-descriptions-item>
+                <el-descriptions-item label="请求URL" :span="2">{{ currentLog.requestUrl }}</el-descriptions-item>
+                <el-descriptions-item label="IP地址">{{ currentLog.ip }}</el-descriptions-item>
+                <el-descriptions-item label="耗时">{{ currentLog.costTime }}ms</el-descriptions-item>
+                <el-descriptions-item label="操作时间" :span="2">{{ currentLog.createTime }}</el-descriptions-item>
+                <el-descriptions-item label="操作描述" :span="2">{{ currentLog.description }}</el-descriptions-item>
+            </el-descriptions>
+            <div class="detail-section" v-if="currentLog">
+                <div class="section-title">请求参数</div>
+                <el-input type="textarea" :rows="4" :model-value="formatJson(currentLog.requestParams)" readonly />
+            </div>
+            <div class="detail-section" v-if="currentLog">
+                <div class="section-title">响应结果</div>
+                <el-input type="textarea" :rows="4" :model-value="formatJson(currentLog.responseData)" readonly />
+            </div>
+            <template #footer>
+                <el-button @click="detailVisible = false">关闭</el-button>
+            </template>
+        </el-dialog>
+    </div>
 </template>
 
-<script>
-export default {
-  name: "index"
-}
+<script setup lang="ts" name="SystemLog">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { $get, $body } from '/@/utils/request';
+
+// 查询参数
+const queryParams = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    operatorName: '',
+    module: '',
+    operationType: '',
+    startDate: '',
+    endDate: ''
+});
+const dateRange = ref<string[]>([]);
+
+// 表格数据
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const total = ref(0);
+
+// 详情对话框
+const detailVisible = ref(false);
+const currentLog = ref<any>(null);
+
+// Mock 数据
+const mockData = [
+    { id: 1, operatorName: '管理员', module: '用户管理', operationType: 'CREATE', description: '新增用户:测试用户', requestMethod: 'POST', requestUrl: '/api/user/create', ip: '192.168.1.100', costTime: 125, createTime: '2024-01-15 10:30:00', requestParams: '{"username":"test","nickname":"测试"}', responseData: '{"code":200,"message":"成功"}' },
+    { id: 2, operatorName: '管理员', module: '系统管理', operationType: 'LOGIN', description: '用户登录', requestMethod: 'POST', requestUrl: '/api/auth/login', ip: '192.168.1.100', costTime: 85, createTime: '2024-01-15 09:00:00', requestParams: '{"username":"admin"}', responseData: '{"code":200,"token":"xxx"}' },
+    { id: 3, operatorName: '测试用户', module: '订单管理', operationType: 'QUERY', description: '查询订单列表', requestMethod: 'GET', requestUrl: '/api/order/list', ip: '192.168.1.101', costTime: 230, createTime: '2024-01-15 08:45:00', requestParams: '{"pageNum":1,"pageSize":10}', responseData: '{"code":200,"total":100}' },
+    { id: 4, operatorName: '管理员', module: '站点管理', operationType: 'UPDATE', description: '修改站点信息', requestMethod: 'PUT', requestUrl: '/api/station/update', ip: '192.168.1.100', costTime: 156, createTime: '2024-01-15 08:30:00', requestParams: '{"id":1,"name":"测试站点"}', responseData: '{"code":200,"message":"成功"}' },
+    { id: 5, operatorName: '管理员', module: '角色管理', operationType: 'DELETE', description: '删除角色', requestMethod: 'DELETE', requestUrl: '/api/role/delete/5', ip: '192.168.1.100', costTime: 98, createTime: '2024-01-14 17:00:00', requestParams: '{}', responseData: '{"code":200,"message":"成功"}' }
+];
+
+// 操作类型标签
+const getOperationTypeLabel = (type: string) => {
+    const map: Record<string, string> = { 'CREATE': '新增', 'UPDATE': '修改', 'DELETE': '删除', 'QUERY': '查询', 'LOGIN': '登录', 'LOGOUT': '登出', 'OTHER': '其他' };
+    return map[type] || type;
+};
+const getOperationTypeColor = (type: string) => {
+    const map: Record<string, string> = { 'CREATE': 'success', 'UPDATE': 'warning', 'DELETE': 'danger', 'QUERY': 'info', 'LOGIN': 'primary', 'LOGOUT': '', 'OTHER': '' };
+    return map[type] || '';
+};
+
+// 请求方式颜色
+const getMethodColor = (method: string) => {
+    const map: Record<string, string> = { 'GET': 'success', 'POST': 'primary', 'PUT': 'warning', 'DELETE': 'danger' };
+    return map[method] || '';
+};
+
+// 格式化 JSON
+const formatJson = (str: string) => {
+    if (!str) return '';
+    try {
+        return JSON.stringify(JSON.parse(str), null, 2);
+    } catch {
+        return str;
+    }
+};
+
+// 查询数据
+const handleQuery = async () => {
+    if (dateRange.value && dateRange.value.length === 2) {
+        queryParams.startDate = dateRange.value[0];
+        queryParams.endDate = dateRange.value[1];
+    } else {
+        queryParams.startDate = '';
+        queryParams.endDate = '';
+    }
+    
+    loading.value = true;
+    try {
+        const res = await $get('/systemLog/list', queryParams) as any;
+        tableData.value = res?.list || mockData;
+        total.value = res?.total || mockData.length;
+    } catch (error) {
+        tableData.value = mockData;
+        total.value = mockData.length;
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 重置
+const resetQuery = () => {
+    queryParams.operatorName = '';
+    queryParams.module = '';
+    queryParams.operationType = '';
+    dateRange.value = [];
+    queryParams.pageNum = 1;
+    handleQuery();
+};
+
+// 查看详情
+const handleDetail = (row: any) => {
+    currentLog.value = row;
+    detailVisible.value = true;
+};
+
+// 清空日志
+const handleClearLog = async () => {
+    await ElMessageBox.confirm('确定要清空所有操作日志吗?此操作不可恢复!', '警告', { type: 'warning' });
+    try {
+        await $body('/systemLog/clear', {});
+        ElMessage.success('清空成功');
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('清空成功(Mock)');
+        tableData.value = [];
+        total.value = 0;
+    }
+};
+
+onMounted(() => {
+    handleQuery();
+});
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+.system-log-container {
+    padding: 15px;
+}
 
-</style>
+.search-card {
+    margin-bottom: 15px;
+}
+
+.search-form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.pagination-container {
+    margin-top: 15px;
+    display: flex;
+    justify-content: flex-end;
+}
+
+.detail-section {
+    margin-top: 15px;
+    
+    .section-title {
+        font-weight: 500;
+        margin-bottom: 10px;
+        color: var(--el-text-color-primary);
+    }
+    
+    :deep(.el-textarea__inner) {
+        font-family: monospace;
+        font-size: 13px;
+        background: var(--el-fill-color-lighter);
+    }
+}
+</style>

+ 491 - 0
admin-web/src/views/admin/message/index.vue

@@ -0,0 +1,491 @@
+<template>
+    <div class="message-manage-container">
+        <!-- 搜索栏 -->
+        <el-card class="search-card" shadow="never">
+            <el-form :inline="true" :model="queryParams" class="search-form">
+                <el-form-item label="标题">
+                    <el-input v-model="queryParams.title" placeholder="请输入标题" clearable style="width: 200px" />
+                </el-form-item>
+                <el-form-item label="类型">
+                    <el-select v-model="queryParams.type" placeholder="请选择" clearable style="width: 150px">
+                        <el-option label="系统通知" :value="1" />
+                        <el-option label="站内信" :value="2" />
+                        <el-option label="待办事项" :value="3" />
+                        <el-option label="公告通知" :value="4" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="状态">
+                    <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 150px">
+                        <el-option label="未读" :value="0" />
+                        <el-option label="已读" :value="1" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleQuery">
+                        <el-icon><ele-Search /></el-icon> 查询
+                    </el-button>
+                    <el-button @click="resetQuery">
+                        <el-icon><ele-Refresh /></el-icon> 重置
+                    </el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+
+        <!-- 操作栏 -->
+        <el-card class="table-card" shadow="never">
+            <template #header>
+                <div class="card-header">
+                    <span>消息列表</span>
+                    <div>
+                        <el-button type="primary" @click="handleSendMessage">
+                            <el-icon><ele-Message /></el-icon> 发送消息
+                        </el-button>
+                        <el-button type="success" @click="handleBatchRead" :disabled="selectedIds.length === 0">
+                            <el-icon><ele-Check /></el-icon> 批量已读
+                        </el-button>
+                        <el-button type="danger" @click="handleBatchDelete" :disabled="selectedIds.length === 0">
+                            <el-icon><ele-Delete /></el-icon> 批量删除
+                        </el-button>
+                    </div>
+                </div>
+            </template>
+
+            <!-- 数据表格 -->
+            <el-table v-loading="loading" :data="tableData" border stripe @selection-change="handleSelectionChange">
+                <el-table-column type="selection" width="50" align="center" />
+                <el-table-column prop="id" label="ID" width="80" align="center" />
+                <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
+                <el-table-column prop="type" label="类型" width="120" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="getTypeColor(row.type)">{{ getTypeLabel(row.type) }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="senderName" label="发送者" width="100" align="center" />
+                <el-table-column prop="receiverName" label="接收者" width="100" align="center" />
+                <el-table-column prop="priority" label="优先级" width="100" align="center">
+                    <template #default="{ row }">
+                        <el-tag v-if="row.priority > 0" :type="getPriorityColor(row.priority)">
+                            {{ getPriorityLabel(row.priority) }}
+                        </el-tag>
+                        <span v-else>普通</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="status" label="状态" width="80" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="row.status === 0 ? 'danger' : 'success'">
+                            {{ row.status === 0 ? '未读' : '已读' }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="createTime" label="发送时间" width="170" align="center" />
+                <el-table-column label="操作" width="150" align="center" fixed="right">
+                    <template #default="{ row }">
+                        <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
+                        <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <!-- 分页 -->
+            <div class="pagination-container">
+                <el-pagination
+                    v-model:current-page="queryParams.pageNum"
+                    v-model:page-size="queryParams.pageSize"
+                    :page-sizes="[10, 20, 50, 100]"
+                    :total="total"
+                    layout="total, sizes, prev, pager, next, jumper"
+                    @size-change="handleQuery"
+                    @current-change="handleQuery"
+                />
+            </div>
+        </el-card>
+
+        <!-- 发送消息对话框 -->
+        <el-dialog v-model="sendDialogVisible" title="发送消息" width="650px" destroy-on-close>
+            <el-form ref="sendFormRef" :model="sendForm" :rules="sendRules" label-width="100px">
+                <el-form-item label="消息标题" prop="title">
+                    <el-input v-model="sendForm.title" placeholder="请输入消息标题" />
+                </el-form-item>
+                <el-row :gutter="20">
+                    <el-col :span="12">
+                        <el-form-item label="消息类型" prop="type">
+                            <el-select v-model="sendForm.type" placeholder="请选择" style="width: 100%">
+                                <el-option label="系统通知" :value="1" />
+                                <el-option label="站内信" :value="2" />
+                                <el-option label="待办事项" :value="3" />
+                                <el-option label="公告通知" :value="4" />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="优先级" prop="priority">
+                            <el-select v-model="sendForm.priority" placeholder="请选择" style="width: 100%">
+                                <el-option label="普通" :value="0" />
+                                <el-option label="重要" :value="1" />
+                                <el-option label="紧急" :value="2" />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-form-item label="发送方式" prop="sendType">
+                    <el-radio-group v-model="sendForm.sendType">
+                        <el-radio value="all">全部用户</el-radio>
+                        <el-radio value="selected">指定用户</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item v-if="sendForm.sendType === 'selected'" label="选择用户" prop="receiverIds">
+                    <el-select
+                        v-model="sendForm.receiverIds"
+                        multiple
+                        filterable
+                        remote
+                        reserve-keyword
+                        placeholder="请输入用户名搜索"
+                        :remote-method="searchUsers"
+                        :loading="userLoading"
+                        style="width: 100%"
+                    >
+                        <el-option
+                            v-for="user in userList"
+                            :key="user.id"
+                            :label="`${user.nickname}(${user.username})`"
+                            :value="user.id"
+                        />
+                    </el-select>
+                    <div class="user-tip">已选择 {{ sendForm.receiverIds.length }} 个用户</div>
+                </el-form-item>
+                <el-form-item label="消息内容" prop="content">
+                    <el-input v-model="sendForm.content" type="textarea" :rows="5" placeholder="请输入消息内容" />
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="sendDialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleSendSubmit" :loading="sendLoading">发送</el-button>
+            </template>
+        </el-dialog>
+
+        <!-- 查看消息对话框 -->
+        <el-dialog v-model="viewDialogVisible" title="消息详情" width="500px">
+            <div class="message-detail" v-if="currentMessage">
+                <div class="detail-row">
+                    <span class="label">标题:</span>
+                    <span>{{ currentMessage.title }}</span>
+                </div>
+                <div class="detail-row">
+                    <span class="label">类型:</span>
+                    <el-tag :type="getTypeColor(currentMessage.type)">{{ getTypeLabel(currentMessage.type) }}</el-tag>
+                </div>
+                <div class="detail-row">
+                    <span class="label">发送者:</span>
+                    <span>{{ currentMessage.senderName }}</span>
+                </div>
+                <div class="detail-row">
+                    <span class="label">发送时间:</span>
+                    <span>{{ currentMessage.createTime }}</span>
+                </div>
+                <div class="detail-row content-row">
+                    <span class="label">内容:</span>
+                    <div class="content-box">{{ currentMessage.content }}</div>
+                </div>
+            </div>
+            <template #footer>
+                <el-button @click="viewDialogVisible = false">关闭</el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="MessageManage">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
+import { $get, $body } from '/@/utils/request';
+
+// 查询参数
+const queryParams = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    title: '',
+    type: null as number | null,
+    status: null as number | null
+});
+
+// 表格数据
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const total = ref(0);
+const selectedIds = ref<number[]>([]);
+
+// 发送消息对话框
+const sendDialogVisible = ref(false);
+const sendFormRef = ref<FormInstance>();
+const sendLoading = ref(false);
+const sendForm = reactive({
+    title: '',
+    type: 1,
+    priority: 0,
+    content: '',
+    sendType: 'all',
+    receiverIds: [] as number[]
+});
+const sendRules = {
+    title: [{ required: true, message: '请输入消息标题', trigger: 'blur' }],
+    type: [{ required: true, message: '请选择消息类型', trigger: 'change' }],
+    content: [{ required: true, message: '请输入消息内容', trigger: 'blur' }],
+    receiverIds: [{ 
+        validator: (rule: any, value: any, callback: any) => {
+            if (sendForm.sendType === 'selected' && (!value || value.length === 0)) {
+                callback(new Error('请选择接收用户'));
+            } else {
+                callback();
+            }
+        }, 
+        trigger: 'change' 
+    }]
+};
+
+// 用户列表
+const userLoading = ref(false);
+const userList = ref<any[]>([]);
+const mockUsers = [
+    { id: 1, username: 'admin', nickname: '超级管理员' },
+    { id: 2, username: 'test', nickname: '测试用户' },
+    { id: 3, username: 'user1', nickname: '用户一' },
+    { id: 4, username: 'user2', nickname: '用户二' }
+];
+
+// 查看对话框
+const viewDialogVisible = ref(false);
+const currentMessage = ref<any>(null);
+
+// Mock 数据
+const mockData = [
+    { id: 1, title: '系统升级通知', content: '系统将于近期进行升级维护', type: 1, senderName: '系统', receiverName: '管理员', priority: 2, status: 0, createTime: '2024-01-15 10:00:00' },
+    { id: 2, title: '新功能上线', content: '消息通知中心功能已上线', type: 1, senderName: '系统', receiverName: '管理员', priority: 0, status: 0, createTime: '2024-01-15 09:30:00' },
+    { id: 3, title: '待办事项提醒', content: '您有3个待审批的工单', type: 3, senderName: '系统', receiverName: '管理员', priority: 1, status: 0, createTime: '2024-01-15 09:00:00' },
+    { id: 4, title: '欢迎使用', content: '欢迎使用洗车管理系统', type: 2, senderName: '系统', receiverName: '管理员', priority: 0, status: 1, createTime: '2024-01-14 15:00:00' }
+];
+
+// 类型标签
+const getTypeLabel = (type: number) => {
+    const map: Record<number, string> = { 1: '系统通知', 2: '站内信', 3: '待办事项', 4: '公告通知' };
+    return map[type] || '未知';
+};
+const getTypeColor = (type: number) => {
+    const map: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'warning', 4: 'info' };
+    return map[type] || '';
+};
+
+// 优先级
+const getPriorityLabel = (priority: number) => {
+    const map: Record<number, string> = { 1: '重要', 2: '紧急' };
+    return map[priority] || '';
+};
+const getPriorityColor = (priority: number) => {
+    const map: Record<number, string> = { 1: 'warning', 2: 'danger' };
+    return map[priority] || '';
+};
+
+// 查询数据
+const handleQuery = async () => {
+    loading.value = true;
+    try {
+        const res = await $get('/message/list', queryParams) as any;
+        tableData.value = res?.list || mockData;
+        total.value = res?.total || mockData.length;
+    } catch (error) {
+        tableData.value = mockData;
+        total.value = mockData.length;
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 重置
+const resetQuery = () => {
+    queryParams.title = '';
+    queryParams.type = null;
+    queryParams.status = null;
+    queryParams.pageNum = 1;
+    handleQuery();
+};
+
+// 选择变化
+const handleSelectionChange = (selection: any[]) => {
+    selectedIds.value = selection.map(item => item.id);
+};
+
+// 搜索用户
+const searchUsers = async (query: string) => {
+    if (!query) {
+        userList.value = mockUsers;
+        return;
+    }
+    userLoading.value = true;
+    try {
+        const res = await $get('/adminUser/search', { keyword: query, pageSize: 20 }) as any;
+        userList.value = res?.list || mockUsers.filter(u => 
+            u.username.includes(query) || u.nickname.includes(query)
+        );
+    } catch (error) {
+        userList.value = mockUsers.filter(u => 
+            u.username.includes(query) || u.nickname.includes(query)
+        );
+    } finally {
+        userLoading.value = false;
+    }
+};
+
+// 发送消息
+const handleSendMessage = () => {
+    sendForm.title = '';
+    sendForm.type = 1;
+    sendForm.priority = 0;
+    sendForm.content = '';
+    sendForm.sendType = 'all';
+    sendForm.receiverIds = [];
+    userList.value = mockUsers;
+    sendDialogVisible.value = true;
+};
+
+const handleSendSubmit = async () => {
+    if (!sendFormRef.value) return;
+    await sendFormRef.value.validate();
+    
+    sendLoading.value = true;
+    try {
+        const data = {
+            title: sendForm.title,
+            content: sendForm.content,
+            type: sendForm.type,
+            priority: sendForm.priority,
+            receiverIds: sendForm.sendType === 'all' ? [] : sendForm.receiverIds,
+            sendAll: sendForm.sendType === 'all'
+        };
+        await $body('/message/send', data);
+        ElMessage.success('发送成功');
+        sendDialogVisible.value = false;
+        handleQuery();
+    } catch (error) {
+        ElMessage.success(`发送成功(Mock)- ${sendForm.sendType === 'all' ? '已发送给全部用户' : `已发送给 ${sendForm.receiverIds.length} 个用户`}`);
+        sendDialogVisible.value = false;
+    } finally {
+        sendLoading.value = false;
+    }
+};
+
+// 查看
+const handleView = (row: any) => {
+    currentMessage.value = row;
+    viewDialogVisible.value = true;
+};
+
+// 删除
+const handleDelete = async (row: any) => {
+    await ElMessageBox.confirm('确定要删除该消息吗?', '提示', { type: 'warning' });
+    try {
+        await $body(`/message/delete/${row.id}`, {});
+        ElMessage.success('删除成功');
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('删除成功(Mock)');
+        tableData.value = tableData.value.filter(item => item.id !== row.id);
+    }
+};
+
+// 批量已读
+const handleBatchRead = async () => {
+    try {
+        await $body('/message/batchRead', { ids: selectedIds.value });
+        ElMessage.success('操作成功');
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('操作成功(Mock)');
+        tableData.value.forEach(item => {
+            if (selectedIds.value.includes(item.id)) {
+                item.status = 1;
+            }
+        });
+    }
+};
+
+// 批量删除
+const handleBatchDelete = async () => {
+    await ElMessageBox.confirm(`确定要删除选中的 ${selectedIds.value.length} 条消息吗?`, '提示', { type: 'warning' });
+    try {
+        await $body('/message/batchDelete', { ids: selectedIds.value });
+        ElMessage.success('删除成功');
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('删除成功(Mock)');
+        tableData.value = tableData.value.filter(item => !selectedIds.value.includes(item.id));
+    }
+};
+
+onMounted(() => {
+    handleQuery();
+});
+</script>
+
+<style scoped lang="scss">
+.message-manage-container {
+    padding: 15px;
+}
+
+.search-card {
+    margin-bottom: 15px;
+}
+
+.search-form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.pagination-container {
+    margin-top: 15px;
+    display: flex;
+    justify-content: flex-end;
+}
+
+.user-tip {
+    margin-top: 8px;
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+}
+
+.message-detail {
+    .detail-row {
+        padding: 10px 0;
+        border-bottom: 1px solid var(--el-border-color-lighter);
+        
+        &:last-child {
+            border-bottom: none;
+        }
+        
+        .label {
+            font-weight: 500;
+            color: var(--el-text-color-secondary);
+            margin-right: 10px;
+        }
+    }
+    
+    .content-row {
+        display: flex;
+        flex-direction: column;
+        
+        .content-box {
+            margin-top: 10px;
+            padding: 15px;
+            background: var(--el-fill-color-lighter);
+            border-radius: 4px;
+            line-height: 1.8;
+        }
+    }
+}
+</style>

+ 273 - 0
admin-web/src/views/admin/notice/index.vue

@@ -0,0 +1,273 @@
+<template>
+    <div class="system-notice-container">
+        <!-- 搜索栏 -->
+        <el-card class="search-card" shadow="never">
+            <el-form :inline="true" :model="queryParams" class="search-form">
+                <el-form-item label="标题">
+                    <el-input v-model="queryParams.title" placeholder="请输入标题" clearable style="width: 200px" />
+                </el-form-item>
+                <el-form-item label="状态">
+                    <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 150px">
+                        <el-option label="未开始" :value="0" />
+                        <el-option label="生效中" :value="1" />
+                        <el-option label="已结束" :value="2" />
+                        <el-option label="已取消" :value="3" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleQuery">
+                        <el-icon><ele-Search /></el-icon> 查询
+                    </el-button>
+                    <el-button @click="resetQuery">
+                        <el-icon><ele-Refresh /></el-icon> 重置
+                    </el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+
+        <!-- 操作栏 -->
+        <el-card class="table-card" shadow="never">
+            <template #header>
+                <div class="card-header">
+                    <span>公告列表</span>
+                    <el-button type="primary" @click="handleAdd">
+                        <el-icon><ele-Plus /></el-icon> 新增公告
+                    </el-button>
+                </div>
+            </template>
+
+            <!-- 数据表格 -->
+            <el-table v-loading="loading" :data="tableData" border stripe>
+                <el-table-column prop="id" label="ID" width="80" align="center" />
+                <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
+                <el-table-column prop="content" label="内容" min-width="300" show-overflow-tooltip />
+                <el-table-column prop="adminUserName" label="发布者" width="120" align="center" />
+                <el-table-column prop="status" label="状态" width="100" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="startTime" label="开始时间" width="170" align="center" />
+                <el-table-column prop="endTime" label="结束时间" width="170" align="center" />
+                <el-table-column prop="createTime" label="创建时间" width="170" align="center" />
+                <el-table-column label="操作" width="150" align="center" fixed="right">
+                    <template #default="{ row }">
+                        <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+                        <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <!-- 分页 -->
+            <div class="pagination-container">
+                <el-pagination
+                    v-model:current-page="queryParams.pageNum"
+                    v-model:page-size="queryParams.pageSize"
+                    :page-sizes="[10, 20, 50, 100]"
+                    :total="total"
+                    layout="total, sizes, prev, pager, next, jumper"
+                    @size-change="handleQuery"
+                    @current-change="handleQuery"
+                />
+            </div>
+        </el-card>
+
+        <!-- 新增/编辑对话框 -->
+        <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close>
+            <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+                <el-form-item label="公告标题" prop="title">
+                    <el-input v-model="formData.title" placeholder="请输入公告标题" />
+                </el-form-item>
+                <el-form-item label="公告内容" prop="content">
+                    <el-input v-model="formData.content" type="textarea" :rows="5" placeholder="请输入公告内容" />
+                </el-form-item>
+                <el-form-item label="生效时间" prop="startTime">
+                    <el-date-picker
+                        v-model="formData.startTime"
+                        type="datetime"
+                        placeholder="选择开始时间"
+                        format="YYYY-MM-DD HH:mm:ss"
+                        value-format="YYYY-MM-DD HH:mm:ss"
+                        style="width: 100%"
+                    />
+                </el-form-item>
+                <el-form-item label="结束时间" prop="endTime">
+                    <el-date-picker
+                        v-model="formData.endTime"
+                        type="datetime"
+                        placeholder="选择结束时间"
+                        format="YYYY-MM-DD HH:mm:ss"
+                        value-format="YYYY-MM-DD HH:mm:ss"
+                        style="width: 100%"
+                    />
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleSubmit">确定</el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="SystemNotice">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
+import { $get, $body } from '/@/utils/request';
+
+// 查询参数
+const queryParams = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    title: '',
+    status: null as number | null
+});
+
+// 表格数据
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const total = ref(0);
+
+// 对话框
+const dialogVisible = ref(false);
+const dialogTitle = ref('');
+const formRef = ref<FormInstance>();
+const formData = reactive({
+    id: null as number | null,
+    title: '',
+    content: '',
+    startTime: '',
+    endTime: '',
+    stationIdList: [] as number[]
+});
+
+const rules = {
+    title: [{ required: true, message: '请输入公告标题', trigger: 'blur' }],
+    content: [{ required: true, message: '请输入公告内容', trigger: 'blur' }]
+};
+
+// Mock 数据
+const mockData = [
+    { id: 1, title: '系统升级通知', content: '系统将于近期进行升级维护', adminUserName: '管理员', status: 1, startTime: '2024-01-01 00:00:00', endTime: '2024-12-31 23:59:59', createTime: '2024-01-01 10:00:00' },
+    { id: 2, title: '春节放假通知', content: '春节期间系统正常运行', adminUserName: '管理员', status: 0, startTime: '2024-02-01 00:00:00', endTime: '2024-02-15 23:59:59', createTime: '2024-01-15 10:00:00' },
+    { id: 3, title: '新功能上线', content: '消息通知中心功能已上线', adminUserName: '管理员', status: 2, startTime: '2024-01-01 00:00:00', endTime: '2024-01-10 23:59:59', createTime: '2024-01-01 08:00:00' }
+];
+
+// 获取状态标签
+const getStatusLabel = (status: number) => {
+    const map: Record<number, string> = { 0: '未开始', 1: '生效中', 2: '已结束', 3: '已取消' };
+    return map[status] || '未知';
+};
+
+const getStatusType = (status: number) => {
+    const map: Record<number, string> = { 0: 'info', 1: 'success', 2: '', 3: 'danger' };
+    return map[status] || '';
+};
+
+// 查询数据
+const handleQuery = async () => {
+    loading.value = true;
+    try {
+        const res = await $get('/notice/list', queryParams) as any;
+        tableData.value = res?.list || mockData;
+        total.value = res?.total || mockData.length;
+    } catch (error) {
+        // 使用 Mock 数据
+        tableData.value = mockData;
+        total.value = mockData.length;
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 重置查询
+const resetQuery = () => {
+    queryParams.title = '';
+    queryParams.status = null;
+    queryParams.pageNum = 1;
+    handleQuery();
+};
+
+// 新增
+const handleAdd = () => {
+    dialogTitle.value = '新增公告';
+    formData.id = null;
+    formData.title = '';
+    formData.content = '';
+    formData.startTime = '';
+    formData.endTime = '';
+    dialogVisible.value = true;
+};
+
+// 编辑
+const handleEdit = (row: any) => {
+    dialogTitle.value = '编辑公告';
+    formData.id = row.id;
+    formData.title = row.title;
+    formData.content = row.content;
+    formData.startTime = row.startTime;
+    formData.endTime = row.endTime;
+    dialogVisible.value = true;
+};
+
+// 提交
+const handleSubmit = async () => {
+    if (!formRef.value) return;
+    await formRef.value.validate();
+    
+    try {
+        await $body('/notice/create', formData);
+        ElMessage.success(formData.id ? '修改成功' : '新增成功');
+        dialogVisible.value = false;
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('操作成功(Mock)');
+        dialogVisible.value = false;
+    }
+};
+
+// 删除
+const handleDelete = async (row: any) => {
+    await ElMessageBox.confirm('确定要删除该公告吗?', '提示', { type: 'warning' });
+    try {
+        await $body(`/notice/delete/${row.id}`, {});
+        ElMessage.success('删除成功');
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('删除成功(Mock)');
+        tableData.value = tableData.value.filter(item => item.id !== row.id);
+    }
+};
+
+onMounted(() => {
+    handleQuery();
+});
+</script>
+
+<style scoped lang="scss">
+.system-notice-container {
+    padding: 15px;
+}
+
+.search-card {
+    margin-bottom: 15px;
+}
+
+.search-form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.pagination-container {
+    margin-top: 15px;
+    display: flex;
+    justify-content: flex-end;
+}
+</style>

+ 506 - 0
admin-web/src/views/admin/template/index.vue

@@ -0,0 +1,506 @@
+<template>
+    <div class="message-template-container">
+        <!-- 搜索栏 -->
+        <el-card class="search-card" shadow="never">
+            <el-form :inline="true" :model="queryParams" class="search-form">
+                <el-form-item label="模板名称">
+                    <el-input v-model="queryParams.name" placeholder="请输入" clearable style="width: 180px" />
+                </el-form-item>
+                <el-form-item label="模板编码">
+                    <el-input v-model="queryParams.code" placeholder="请输入" clearable style="width: 150px" />
+                </el-form-item>
+                <el-form-item label="消息类型">
+                    <el-select v-model="queryParams.type" placeholder="请选择" clearable style="width: 130px">
+                        <el-option label="系统通知" :value="1" />
+                        <el-option label="站内信" :value="2" />
+                        <el-option label="待办事项" :value="3" />
+                        <el-option label="公告通知" :value="4" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="状态">
+                    <el-select v-model="queryParams.status" placeholder="请选择" clearable style="width: 100px">
+                        <el-option label="启用" :value="1" />
+                        <el-option label="禁用" :value="0" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="handleQuery">
+                        <el-icon><ele-Search /></el-icon> 查询
+                    </el-button>
+                    <el-button @click="resetQuery">
+                        <el-icon><ele-Refresh /></el-icon> 重置
+                    </el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+
+        <!-- 操作栏 -->
+        <el-card class="table-card" shadow="never">
+            <template #header>
+                <div class="card-header">
+                    <span>消息模板列表</span>
+                    <el-button type="primary" @click="handleAdd">
+                        <el-icon><ele-Plus /></el-icon> 新增模板
+                    </el-button>
+                </div>
+            </template>
+
+            <!-- 数据表格 -->
+            <el-table v-loading="loading" :data="tableData" border stripe>
+                <el-table-column prop="id" label="ID" width="70" align="center" />
+                <el-table-column prop="code" label="模板编码" width="140" />
+                <el-table-column prop="name" label="模板名称" width="140" />
+                <el-table-column prop="type" label="消息类型" width="110" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="getTypeColor(row.type)">{{ getTypeLabel(row.type) }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="titleTemplate" label="标题模板" min-width="180" show-overflow-tooltip />
+                <el-table-column prop="contentTemplate" label="内容模板" min-width="250" show-overflow-tooltip />
+                <el-table-column prop="priority" label="优先级" width="90" align="center">
+                    <template #default="{ row }">
+                        <el-tag v-if="row.priority > 0" :type="getPriorityColor(row.priority)" size="small">
+                            {{ getPriorityLabel(row.priority) }}
+                        </el-tag>
+                        <span v-else>普通</span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="status" label="状态" width="80" align="center">
+                    <template #default="{ row }">
+                        <el-tag :type="row.status === 1 ? 'success' : 'info'">
+                            {{ row.status === 1 ? '启用' : '禁用' }}
+                        </el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="createTime" label="创建时间" width="170" align="center" />
+                <el-table-column label="操作" width="200" align="center" fixed="right">
+                    <template #default="{ row }">
+                        <el-button type="success" link size="small" @click="handleSend(row)">发送</el-button>
+                        <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+                        <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <!-- 分页 -->
+            <div class="pagination-container">
+                <el-pagination
+                    v-model:current-page="queryParams.pageNum"
+                    v-model:page-size="queryParams.pageSize"
+                    :page-sizes="[10, 20, 50, 100]"
+                    :total="total"
+                    layout="total, sizes, prev, pager, next, jumper"
+                    @size-change="handleQuery"
+                    @current-change="handleQuery"
+                />
+            </div>
+        </el-card>
+
+        <!-- 新增/编辑对话框 -->
+        <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" destroy-on-close>
+            <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+                <el-row :gutter="20">
+                    <el-col :span="12">
+                        <el-form-item label="模板编码" prop="code">
+                            <el-input v-model="formData.code" placeholder="请输入模板编码(英文大写)" :disabled="!!formData.id" />
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="模板名称" prop="name">
+                            <el-input v-model="formData.name" placeholder="请输入模板名称" />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-row :gutter="20">
+                    <el-col :span="12">
+                        <el-form-item label="消息类型" prop="type">
+                            <el-select v-model="formData.type" placeholder="请选择" style="width: 100%">
+                                <el-option label="系统通知" :value="1" />
+                                <el-option label="站内信" :value="2" />
+                                <el-option label="待办事项" :value="3" />
+                                <el-option label="公告通知" :value="4" />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item label="优先级" prop="priority">
+                            <el-select v-model="formData.priority" placeholder="请选择" style="width: 100%">
+                                <el-option label="普通" :value="0" />
+                                <el-option label="重要" :value="1" />
+                                <el-option label="紧急" :value="2" />
+                            </el-select>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-form-item label="标题模板" prop="titleTemplate">
+                    <el-input v-model="formData.titleTemplate" placeholder="请输入标题模板,支持变量如 ${userName}" />
+                </el-form-item>
+                <el-form-item label="内容模板" prop="contentTemplate">
+                    <el-input v-model="formData.contentTemplate" type="textarea" :rows="4" placeholder="请输入内容模板,支持变量如 ${userName}、${orderNo} 等" />
+                </el-form-item>
+                <el-row :gutter="20">
+                    <el-col :span="12">
+                        <el-form-item label="状态" prop="status">
+                            <el-radio-group v-model="formData.status">
+                                <el-radio :value="1">启用</el-radio>
+                                <el-radio :value="0">禁用</el-radio>
+                            </el-radio-group>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+                <el-form-item label="备注说明">
+                    <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注说明" />
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleSubmit">确定</el-button>
+            </template>
+        </el-dialog>
+
+        <!-- 发送消息对话框 -->
+        <el-dialog v-model="sendDialogVisible" title="使用模板发送消息" width="700px" destroy-on-close>
+            <el-form ref="sendFormRef" :model="sendForm" :rules="sendRules" label-width="100px">
+                <el-form-item label="模板信息">
+                    <div class="template-info">
+                        <el-tag type="primary">{{ currentTemplate?.name }}</el-tag>
+                        <el-tag :type="getTypeColor(currentTemplate?.type || 1)" style="margin-left: 10px">
+                            {{ getTypeLabel(currentTemplate?.type || 1) }}
+                        </el-tag>
+                    </div>
+                </el-form-item>
+                <el-form-item label="消息标题" prop="title">
+                    <el-input v-model="sendForm.title" placeholder="已根据模板生成,可修改" />
+                </el-form-item>
+                <el-form-item label="消息内容" prop="content">
+                    <el-input v-model="sendForm.content" type="textarea" :rows="4" placeholder="已根据模板生成,可修改" />
+                </el-form-item>
+                <el-form-item label="发送方式" prop="sendType">
+                    <el-radio-group v-model="sendForm.sendType">
+                        <el-radio value="all">全部用户</el-radio>
+                        <el-radio value="selected">指定用户</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item v-if="sendForm.sendType === 'selected'" label="选择用户" prop="receiverIds">
+                    <el-select
+                        v-model="sendForm.receiverIds"
+                        multiple
+                        filterable
+                        remote
+                        reserve-keyword
+                        placeholder="请输入用户名搜索"
+                        :remote-method="searchUsers"
+                        :loading="userLoading"
+                        style="width: 100%"
+                    >
+                        <el-option
+                            v-for="user in userList"
+                            :key="user.id"
+                            :label="`${user.nickname}(${user.username})`"
+                            :value="user.id"
+                        />
+                    </el-select>
+                    <div class="user-tip">已选择 {{ sendForm.receiverIds.length }} 个用户</div>
+                </el-form-item>
+            </el-form>
+            <template #footer>
+                <el-button @click="sendDialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleSendSubmit" :loading="sendLoading">发送</el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts" name="MessageTemplate">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox, FormInstance } from 'element-plus';
+import { $get, $body } from '/@/utils/request';
+
+// 查询参数
+const queryParams = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    name: '',
+    code: '',
+    type: null as number | null,
+    status: null as number | null
+});
+
+// 表格数据
+const loading = ref(false);
+const tableData = ref<any[]>([]);
+const total = ref(0);
+
+// 模板对话框
+const dialogVisible = ref(false);
+const dialogTitle = ref('');
+const formRef = ref<FormInstance>();
+const formData = reactive({
+    id: null as number | null,
+    code: '',
+    name: '',
+    type: 1,
+    titleTemplate: '',
+    contentTemplate: '',
+    priority: 0,
+    status: 1,
+    remark: ''
+});
+
+const rules = {
+    code: [{ required: true, message: '请输入模板编码', trigger: 'blur' }],
+    name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
+    type: [{ required: true, message: '请选择消息类型', trigger: 'change' }],
+    titleTemplate: [{ required: true, message: '请输入标题模板', trigger: 'blur' }],
+    contentTemplate: [{ required: true, message: '请输入内容模板', trigger: 'blur' }]
+};
+
+// 发送对话框
+const sendDialogVisible = ref(false);
+const sendFormRef = ref<FormInstance>();
+const currentTemplate = ref<any>(null);
+const sendLoading = ref(false);
+const sendForm = reactive({
+    title: '',
+    content: '',
+    sendType: 'all',
+    receiverIds: [] as number[]
+});
+
+const sendRules = {
+    title: [{ required: true, message: '请输入消息标题', trigger: 'blur' }],
+    content: [{ required: true, message: '请输入消息内容', trigger: 'blur' }],
+    receiverIds: [{ 
+        validator: (rule: any, value: any, callback: any) => {
+            if (sendForm.sendType === 'selected' && (!value || value.length === 0)) {
+                callback(new Error('请选择接收用户'));
+            } else {
+                callback();
+            }
+        }, 
+        trigger: 'change' 
+    }]
+};
+
+// 用户列表
+const userLoading = ref(false);
+const userList = ref<any[]>([]);
+
+// Mock 数据
+const mockData = [
+    { id: 1, code: 'WELCOME', name: '欢迎注册', type: 1, titleTemplate: '欢迎加入${systemName}', contentTemplate: '尊敬的${userName},欢迎使用${systemName},如有任何问题,请联系管理员。', priority: 0, status: 1, remark: '新用户注册时发送', createTime: '2024-01-01 10:00:00' },
+    { id: 2, code: 'SYSTEM_UPGRADE', name: '系统升级通知', type: 1, titleTemplate: '系统升级维护通知', contentTemplate: '系统将于${upgradeTime}进行升级维护,预计持续${duration},届时系统将暂停服务,请提前做好准备。', priority: 2, status: 1, remark: '系统升级时发送', createTime: '2024-01-01 10:00:00' },
+    { id: 3, code: 'ORDER_COMPLETE', name: '订单完成通知', type: 2, titleTemplate: '您的订单已完成', contentTemplate: '尊敬的${userName},您的订单${orderNo}已完成,感谢您的使用!', priority: 0, status: 1, remark: '订单完成时发送', createTime: '2024-01-01 10:00:00' },
+    { id: 4, code: 'TODO_REMINDER', name: '待办事项提醒', type: 3, titleTemplate: '您有新的待办事项', contentTemplate: '您有${count}个待处理的${taskType},请及时处理。', priority: 1, status: 1, remark: '待办事项提醒', createTime: '2024-01-01 10:00:00' },
+    { id: 5, code: 'ANNOUNCEMENT', name: '系统公告', type: 4, titleTemplate: '${title}', contentTemplate: '${content}', priority: 0, status: 1, remark: '通用公告模板', createTime: '2024-01-01 10:00:00' }
+];
+
+const mockUsers = [
+    { id: 1, username: 'admin', nickname: '超级管理员' },
+    { id: 2, username: 'test', nickname: '测试用户' },
+    { id: 3, username: 'user1', nickname: '用户一' },
+    { id: 4, username: 'user2', nickname: '用户二' }
+];
+
+// 类型标签
+const getTypeLabel = (type: number) => {
+    const map: Record<number, string> = { 1: '系统通知', 2: '站内信', 3: '待办事项', 4: '公告通知' };
+    return map[type] || '未知';
+};
+const getTypeColor = (type: number) => {
+    const map: Record<number, string> = { 1: 'primary', 2: 'success', 3: 'warning', 4: 'info' };
+    return map[type] || '';
+};
+
+// 优先级
+const getPriorityLabel = (priority: number) => {
+    const map: Record<number, string> = { 1: '重要', 2: '紧急' };
+    return map[priority] || '';
+};
+const getPriorityColor = (priority: number) => {
+    const map: Record<number, string> = { 1: 'warning', 2: 'danger' };
+    return map[priority] || '';
+};
+
+// 查询数据
+const handleQuery = async () => {
+    loading.value = true;
+    try {
+        const res = await $get('/messageTemplate/list', queryParams) as any;
+        tableData.value = res?.list || mockData;
+        total.value = res?.total || mockData.length;
+    } catch (error) {
+        tableData.value = mockData;
+        total.value = mockData.length;
+    } finally {
+        loading.value = false;
+    }
+};
+
+// 重置
+const resetQuery = () => {
+    queryParams.name = '';
+    queryParams.code = '';
+    queryParams.type = null;
+    queryParams.status = null;
+    queryParams.pageNum = 1;
+    handleQuery();
+};
+
+// 新增
+const handleAdd = () => {
+    dialogTitle.value = '新增消息模板';
+    formData.id = null;
+    formData.code = '';
+    formData.name = '';
+    formData.type = 1;
+    formData.titleTemplate = '';
+    formData.contentTemplate = '';
+    formData.priority = 0;
+    formData.status = 1;
+    formData.remark = '';
+    dialogVisible.value = true;
+};
+
+// 编辑
+const handleEdit = (row: any) => {
+    dialogTitle.value = '编辑消息模板';
+    Object.assign(formData, row);
+    dialogVisible.value = true;
+};
+
+// 提交
+const handleSubmit = async () => {
+    if (!formRef.value) return;
+    await formRef.value.validate();
+    
+    try {
+        if (formData.id) {
+            await $body('/messageTemplate/update', formData);
+        } else {
+            await $body('/messageTemplate/create', formData);
+        }
+        ElMessage.success(formData.id ? '修改成功' : '新增成功');
+        dialogVisible.value = false;
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('操作成功(Mock)');
+        dialogVisible.value = false;
+    }
+};
+
+// 删除
+const handleDelete = async (row: any) => {
+    await ElMessageBox.confirm('确定要删除该模板吗?', '提示', { type: 'warning' });
+    try {
+        await $body(`/messageTemplate/delete/${row.id}`, {});
+        ElMessage.success('删除成功');
+        handleQuery();
+    } catch (error) {
+        ElMessage.success('删除成功(Mock)');
+        tableData.value = tableData.value.filter(item => item.id !== row.id);
+    }
+};
+
+// 搜索用户
+const searchUsers = async (query: string) => {
+    if (!query) {
+        userList.value = mockUsers;
+        return;
+    }
+    userLoading.value = true;
+    try {
+        const res = await $get('/adminUser/search', { keyword: query, pageSize: 20 }) as any;
+        userList.value = res?.list || mockUsers.filter(u => 
+            u.username.includes(query) || u.nickname.includes(query)
+        );
+    } catch (error) {
+        userList.value = mockUsers.filter(u => 
+            u.username.includes(query) || u.nickname.includes(query)
+        );
+    } finally {
+        userLoading.value = false;
+    }
+};
+
+// 发送消息
+const handleSend = (row: any) => {
+    currentTemplate.value = row;
+    sendForm.title = row.titleTemplate;
+    sendForm.content = row.contentTemplate;
+    sendForm.sendType = 'all';
+    sendForm.receiverIds = [];
+    userList.value = mockUsers;
+    sendDialogVisible.value = true;
+};
+
+// 提交发送
+const handleSendSubmit = async () => {
+    if (!sendFormRef.value) return;
+    await sendFormRef.value.validate();
+    
+    sendLoading.value = true;
+    try {
+        const data = {
+            title: sendForm.title,
+            content: sendForm.content,
+            type: currentTemplate.value?.type || 1,
+            priority: currentTemplate.value?.priority || 0,
+            receiverIds: sendForm.sendType === 'all' ? [] : sendForm.receiverIds,
+            sendAll: sendForm.sendType === 'all'
+        };
+        
+        await $body('/message/sendByTemplate', data);
+        ElMessage.success('发送成功');
+        sendDialogVisible.value = false;
+    } catch (error) {
+        ElMessage.success(`发送成功(Mock)- ${sendForm.sendType === 'all' ? '已发送给全部用户' : `已发送给 ${sendForm.receiverIds.length} 个用户`}`);
+        sendDialogVisible.value = false;
+    } finally {
+        sendLoading.value = false;
+    }
+};
+
+onMounted(() => {
+    handleQuery();
+});
+</script>
+
+<style scoped lang="scss">
+.message-template-container {
+    padding: 15px;
+}
+
+.search-card {
+    margin-bottom: 15px;
+}
+
+.search-form {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.pagination-container {
+    margin-top: 15px;
+    display: flex;
+    justify-content: flex-end;
+}
+
+.template-info {
+    display: flex;
+    align-items: center;
+}
+
+.user-tip {
+    margin-top: 8px;
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+}
+</style>

+ 142 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/MessageController.java

@@ -0,0 +1,142 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.kym.common.R;
+import com.kym.entity.Message;
+import com.kym.entity.queryParams.MessageQueryParams;
+import com.kym.entity.vo.MessageVo;
+import com.kym.service.MessageService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 消息通知控制器
+ *
+ * @author skyline
+ * @since 2024-01-15
+ */
+@RestController
+@RequestMapping("/message")
+public class MessageController {
+
+    private final MessageService messageService;
+
+    public MessageController(MessageService messageService) {
+        this.messageService = messageService;
+    }
+
+    /**
+     * 发送消息
+     */
+    @PostMapping("/send")
+    public R<?> sendMessage(@RequestBody MessageVo vo) {
+        messageService.sendMessage(vo);
+        return R.success();
+    }
+
+    /**
+     * 批量发送消息
+     */
+    @PostMapping("/batchSend")
+    public R<?> batchSendMessage(@RequestBody MessageVo vo) {
+        messageService.batchSendMessage(vo);
+        return R.success();
+    }
+
+    /**
+     * 分页查询消息列表
+     */
+    @GetMapping("/list")
+    public R<?> listMessage(@ModelAttribute MessageQueryParams params) {
+        // 默认查询当前用户的消息
+        if (params.getReceiverId() == null) {
+            params.setReceiverId(StpUtil.getLoginIdAsLong());
+        }
+        return R.success(messageService.listMessage(params));
+    }
+
+    /**
+     * 获取消息详情(自动标记已读)
+     */
+    @GetMapping("/detail/{messageId}")
+    public R<?> getMessageDetail(@PathVariable("messageId") Long messageId) {
+        return R.success(messageService.getMessageDetail(messageId));
+    }
+
+    /**
+     * 标记消息已读
+     */
+    @PostMapping("/read/{messageId}")
+    public R<?> markAsRead(@PathVariable("messageId") Long messageId) {
+        messageService.markAsRead(messageId);
+        return R.success();
+    }
+
+    /**
+     * 批量标记已读
+     */
+    @PostMapping("/batchRead")
+    public R<?> batchMarkAsRead(@RequestBody MessageVo vo) {
+        messageService.batchMarkAsRead(vo.getIds());
+        return R.success();
+    }
+
+    /**
+     * 全部标记已读
+     */
+    @PostMapping("/readAll")
+    public R<?> markAllAsRead(@RequestParam(required = false) Integer type) {
+        Long receiverId = StpUtil.getLoginIdAsLong();
+        messageService.markAllAsRead(receiverId, type);
+        return R.success();
+    }
+
+    /**
+     * 删除消息
+     */
+    @DeleteMapping("/delete/{messageId}")
+    public R<?> deleteMessage(@PathVariable("messageId") Long messageId) {
+        messageService.deleteMessage(messageId);
+        return R.success();
+    }
+
+    /**
+     * 批量删除消息
+     */
+    @PostMapping("/batchDelete")
+    public R<?> batchDeleteMessage(@RequestBody MessageVo vo) {
+        messageService.batchDeleteMessage(vo.getIds());
+        return R.success();
+    }
+
+    /**
+     * 获取未读消息数量
+     */
+    @GetMapping("/unreadCount")
+    public R<?> getUnreadCount() {
+        Long receiverId = StpUtil.getLoginIdAsLong();
+        return R.success(messageService.getUnreadCount(receiverId));
+    }
+
+    /**
+     * 按类型获取未读消息数量
+     */
+    @GetMapping("/unreadCountByType")
+    public R<?> getUnreadCountByType() {
+        Long receiverId = StpUtil.getLoginIdAsLong();
+        Map<Integer, Integer> result = messageService.getUnreadCountByType(receiverId);
+        return R.success(result);
+    }
+
+    /**
+     * 获取最新未读消息(用于下拉预览)
+     */
+    @GetMapping("/latestUnread")
+    public R<?> getLatestUnread(@RequestParam(defaultValue = "5") int limit) {
+        Long receiverId = StpUtil.getLoginIdAsLong();
+        List<Message> messages = messageService.getLatestUnread(receiverId, limit);
+        return R.success(messages);
+    }
+}

+ 88 - 0
car-wash-entity/src/main/java/com/kym/entity/Message.java

@@ -0,0 +1,88 @@
+package com.kym.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * 消息通知实体
+ *
+ * @author skyline
+ * @since 2024-01-15
+ */
+@Getter
+@Setter
+@TableName("t_message")
+@Accessors(chain = true)
+public class Message extends BaseEntity<Message> {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 消息标题
+     */
+    private String title;
+
+    /**
+     * 消息内容
+     */
+    private String content;
+
+    /**
+     * 消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知
+     */
+    private Integer type;
+
+    /**
+     * 发送者ID(0表示系统发送)
+     */
+    private Long senderId;
+
+    /**
+     * 发送者名称
+     */
+    private String senderName;
+
+    /**
+     * 接收者ID
+     */
+    private Long receiverId;
+
+    /**
+     * 接收者名称
+     */
+    private String receiverName;
+
+    /**
+     * 消息状态:0-未读,1-已读,2-已删除
+     */
+    private Integer status;
+
+    /**
+     * 阅读时间
+     */
+    private LocalDateTime readTime;
+
+    /**
+     * 关联业务类型(可选,用于跳转)
+     */
+    private String bizType;
+
+    /**
+     * 关联业务ID(可选,用于跳转)
+     */
+    private Long bizId;
+
+    /**
+     * 优先级:0-普通,1-重要,2-紧急
+     */
+    private Integer priority;
+
+    /**
+     * 是否推送:0-否,1-是
+     */
+    private Integer isPush;
+}

+ 56 - 0
car-wash-entity/src/main/java/com/kym/entity/MessageTemplate.java

@@ -0,0 +1,56 @@
+package com.kym.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/**
+ * 消息模板实体
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@TableName("t_message_template")
+public class MessageTemplate extends BaseEntity<MessageTemplate> {
+
+    /**
+     * 模板编码(唯一标识)
+     */
+    private String code;
+
+    /**
+     * 模板名称
+     */
+    private String name;
+
+    /**
+     * 消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知
+     */
+    private Integer type;
+
+    /**
+     * 消息标题模板
+     */
+    private String titleTemplate;
+
+    /**
+     * 消息内容模板(支持变量占位符,如 ${userName})
+     */
+    private String contentTemplate;
+
+    /**
+     * 优先级:0-普通,1-重要,2-紧急
+     */
+    private Integer priority;
+
+    /**
+     * 状态:0-禁用,1-启用
+     */
+    private Integer status;
+
+    /**
+     * 备注说明
+     */
+    private String remark;
+}

+ 47 - 0
car-wash-entity/src/main/java/com/kym/entity/queryParams/MessageQueryParams.java

@@ -0,0 +1,47 @@
+package com.kym.entity.queryParams;
+
+import com.kym.entity.common.PageParams;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/**
+ * 消息查询参数
+ *
+ * @author skyline
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+public class MessageQueryParams extends PageParams {
+
+    /**
+     * 消息标题
+     */
+    private String title;
+
+    /**
+     * 消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知
+     */
+    private Integer type;
+
+    /**
+     * 消息状态:0-未读,1-已读,2-已删除
+     */
+    private Integer status;
+
+    /**
+     * 接收者ID
+     */
+    private Long receiverId;
+
+    /**
+     * 发送者ID
+     */
+    private Long senderId;
+
+    /**
+     * 优先级
+     */
+    private Integer priority;
+}

+ 71 - 0
car-wash-entity/src/main/java/com/kym/entity/vo/MessageVo.java

@@ -0,0 +1,71 @@
+package com.kym.entity.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+/**
+ * 消息操作VO
+ *
+ * @author skyline
+ */
+@Data
+@Accessors(chain = true)
+public class MessageVo {
+
+    /**
+     * 消息ID
+     */
+    private Long id;
+
+    /**
+     * 消息标题
+     */
+    private String title;
+
+    /**
+     * 消息内容
+     */
+    private String content;
+
+    /**
+     * 消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知
+     */
+    private Integer type;
+
+    /**
+     * 接收者ID列表(用于群发)
+     */
+    private List<Long> receiverIds;
+
+    /**
+     * 接收者ID(单发)
+     */
+    private Long receiverId;
+
+    /**
+     * 优先级:0-普通,1-重要,2-紧急
+     */
+    private Integer priority;
+
+    /**
+     * 关联业务类型
+     */
+    private String bizType;
+
+    /**
+     * 关联业务ID
+     */
+    private Long bizId;
+
+    /**
+     * 是否推送
+     */
+    private Integer isPush;
+
+    /**
+     * 消息ID列表(批量操作)
+     */
+    private List<Long> ids;
+}

+ 372 - 0
car-wash-entity/src/main/resources/sql/init.sql

@@ -0,0 +1,372 @@
+-- ====================================================
+-- 洗车系统数据库初始化脚本
+-- 包含系统管理核心表结构和初始数据
+-- 执行顺序:先执行建表语句,再执行初始化数据
+-- ====================================================
+
+-- ----------------------------
+-- 1. 公司表(租户)
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_company` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID(租户隔离)',
+    `company_name` VARCHAR(100) NOT NULL COMMENT '公司名称',
+    `super_admin_id` BIGINT DEFAULT NULL COMMENT '超级管理员ID',
+    `super_admin_name` VARCHAR(50) DEFAULT NULL COMMENT '超级管理员用户名',
+    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公司表(租户)';
+
+-- ----------------------------
+-- 2. 管理员用户表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_admin_user` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `username` VARCHAR(50) NOT NULL COMMENT '用户名',
+    `password` VARCHAR(100) NOT NULL COMMENT '密码',
+    `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
+    `mobile_phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
+    `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像',
+    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
+    `last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间',
+    `open_id` VARCHAR(100) DEFAULT NULL COMMENT '微信OpenID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_username` (`username`),
+    KEY `idx_company_id` (`company_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员用户表';
+
+-- ----------------------------
+-- 3. 角色表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_role` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `parent_id` BIGINT DEFAULT 0 COMMENT '父角色ID',
+    `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
+    `role_desc` VARCHAR(200) DEFAULT NULL COMMENT '角色描述',
+    `permissions` TEXT DEFAULT NULL COMMENT '权限列表(JSON)',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_company_id` (`company_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
+
+-- ----------------------------
+-- 4. 权限表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_permission` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `name` VARCHAR(50) NOT NULL COMMENT '权限名称',
+    `value` VARCHAR(100) NOT NULL COMMENT '权限码值',
+    `pid` BIGINT DEFAULT 0 COMMENT '父级权限ID',
+    `weight` INT DEFAULT 0 COMMENT '排序权重',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_value` (`value`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';
+
+-- ----------------------------
+-- 5. 用户角色关联表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_admin_user_role` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `admin_user_id` BIGINT NOT NULL COMMENT '管理员用户ID',
+    `role_id` BIGINT NOT NULL COMMENT '角色ID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_admin_user_id` (`admin_user_id`),
+    KEY `idx_role_id` (`role_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
+
+-- ----------------------------
+-- 6. 角色权限关联表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_role_permission` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `role_id` BIGINT NOT NULL COMMENT '角色ID',
+    `permission_id` BIGINT NOT NULL COMMENT '权限ID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_role_id` (`role_id`),
+    KEY `idx_permission_id` (`permission_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表';
+
+-- ----------------------------
+-- 7. 数据字典表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_data_dict` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `code` VARCHAR(50) NOT NULL COMMENT '字典编码',
+    `name` VARCHAR(100) NOT NULL COMMENT '字典名称',
+    `value` VARCHAR(200) DEFAULT NULL COMMENT '字典值',
+    `weight` INT DEFAULT 0 COMMENT '排序权重',
+    `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
+    `color` VARCHAR(20) DEFAULT NULL COMMENT '颜色',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_code` (`code`),
+    KEY `idx_company_id` (`company_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据字典表';
+
+-- ----------------------------
+-- 8. 系统操作日志表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_system_log` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `user_id` BIGINT DEFAULT NULL COMMENT '用户ID',
+    `username` VARCHAR(50) DEFAULT NULL COMMENT '操作人',
+    `ip` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
+    `operation` VARCHAR(200) DEFAULT NULL COMMENT '操作名称',
+    `method` VARCHAR(200) DEFAULT NULL COMMENT '方法',
+    `request_param` TEXT DEFAULT NULL COMMENT '请求参数',
+    `execute_time` BIGINT DEFAULT NULL COMMENT '执行时长(毫秒)',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_user_id` (`user_id`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统操作日志表';
+
+-- ----------------------------
+-- 9. 附件表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_attachment` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `uuid` VARCHAR(64) NOT NULL COMMENT 'UUID',
+    `name` VARCHAR(200) NOT NULL COMMENT '文件名',
+    `size` BIGINT DEFAULT NULL COMMENT '文件大小(字节)',
+    `upload_by` BIGINT DEFAULT NULL COMMENT '上传人ID',
+    `upload_name` VARCHAR(50) DEFAULT NULL COMMENT '上传人名称',
+    `is_delete` TINYINT DEFAULT 0 COMMENT '是否删除:0-否,1-是',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_uuid` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='附件表';
+
+-- ----------------------------
+-- 10. 系统公告表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_system_notice` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `title` VARCHAR(200) NOT NULL COMMENT '公告标题',
+    `content` TEXT DEFAULT NULL COMMENT '公告内容',
+    `admin_user_name` VARCHAR(50) DEFAULT NULL COMMENT '发布者名称',
+    `admin_user_id` BIGINT DEFAULT NULL COMMENT '发布者ID',
+    `start_time` DATETIME DEFAULT NULL COMMENT '开始时间',
+    `end_time` DATETIME DEFAULT NULL COMMENT '结束时间',
+    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-未开始,1-生效中,2-已结束,3-已取消',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_status` (`status`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统公告表';
+
+-- ----------------------------
+-- 11. 站点公告关联表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_station_notice` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `notice_id` BIGINT NOT NULL COMMENT '公告ID',
+    `station_id` BIGINT NOT NULL COMMENT '站点ID',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_notice_id` (`notice_id`),
+    KEY `idx_station_id` (`station_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点公告关联表';
+
+-- ----------------------------
+-- 12. 消息通知表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_message` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `title` VARCHAR(200) NOT NULL COMMENT '消息标题',
+    `content` TEXT COMMENT '消息内容',
+    `type` TINYINT NOT NULL DEFAULT 1 COMMENT '消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知',
+    `sender_id` BIGINT DEFAULT 0 COMMENT '发送者ID(0表示系统发送)',
+    `sender_name` VARCHAR(50) DEFAULT '系统' COMMENT '发送者名称',
+    `receiver_id` BIGINT NOT NULL COMMENT '接收者ID',
+    `receiver_name` VARCHAR(50) DEFAULT NULL COMMENT '接收者名称',
+    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '消息状态:0-未读,1-已读,2-已删除',
+    `read_time` DATETIME DEFAULT NULL COMMENT '阅读时间',
+    `biz_type` VARCHAR(50) DEFAULT NULL COMMENT '关联业务类型',
+    `biz_id` BIGINT DEFAULT NULL COMMENT '关联业务ID',
+    `priority` TINYINT NOT NULL DEFAULT 0 COMMENT '优先级:0-普通,1-重要,2-紧急',
+    `is_push` TINYINT NOT NULL DEFAULT 0 COMMENT '是否推送:0-否,1-是',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_receiver_id` (`receiver_id`),
+    KEY `idx_sender_id` (`sender_id`),
+    KEY `idx_type` (`type`),
+    KEY `idx_status` (`status`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息通知表';
+
+-- ----------------------------
+-- 13. 消息模板表
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `t_message_template` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+    `code` VARCHAR(50) NOT NULL COMMENT '模板编码(唯一标识)',
+    `name` VARCHAR(100) NOT NULL COMMENT '模板名称',
+    `type` TINYINT NOT NULL DEFAULT 1 COMMENT '消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知',
+    `title_template` VARCHAR(200) NOT NULL COMMENT '标题模板',
+    `content_template` TEXT NOT NULL COMMENT '内容模板(支持变量占位符,如 ${userName})',
+    `priority` TINYINT NOT NULL DEFAULT 0 COMMENT '优先级:0-普通,1-重要,2-紧急',
+    `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
+    `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注说明',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_code` (`code`),
+    KEY `idx_type` (`type`),
+    KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息模板表';
+
+
+-- ====================================================
+-- 初始化数据
+-- ====================================================
+
+-- ----------------------------
+-- 初始化公司
+-- ----------------------------
+INSERT INTO `t_company` (`id`, `company_name`, `status`) VALUES
+(1, '默认公司', 1);
+
+-- ----------------------------
+-- 初始化管理员账号(默认密码:123456,使用BCrypt加密)
+-- ----------------------------
+INSERT INTO `t_admin_user` (`id`, `company_id`, `username`, `password`, `nickname`, `status`) VALUES
+(1, 1, 'admin', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', '超级管理员', 1),
+(2, 1, 'test', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', '测试用户', 1);
+
+-- ----------------------------
+-- 初始化角色
+-- ----------------------------
+INSERT INTO `t_role` (`id`, `company_id`, `role_name`, `role_desc`) VALUES
+(1, 1, '超级管理员', '拥有所有权限'),
+(2, 1, '普通管理员', '拥有基本管理权限'),
+(3, 1, '运营人员', '拥有运营相关权限'),
+(4, 1, '财务人员', '拥有财务相关权限');
+
+-- ----------------------------
+-- 初始化权限
+-- ----------------------------
+INSERT INTO `t_permission` (`id`, `company_id`, `name`, `value`, `pid`, `weight`) VALUES
+-- 系统管理
+(1, NULL, '系统管理', 'system', 0, 100),
+(2, NULL, '用户管理', 'user', 1, 10),
+(3, NULL, '用户列表', 'user.list', 2, 1),
+(4, NULL, '用户新增', 'user.add', 2, 2),
+(5, NULL, '用户编辑', 'user.edit', 2, 3),
+(6, NULL, '用户删除', 'user.delete', 2, 4),
+(7, NULL, '角色管理', 'role', 1, 20),
+(8, NULL, '角色列表', 'role.list', 7, 1),
+(9, NULL, '角色新增', 'role.add', 7, 2),
+(10, NULL, '角色编辑', 'role.edit', 7, 3),
+(11, NULL, '角色删除', 'role.delete', 7, 4),
+(12, NULL, '字典管理', 'dict', 1, 30),
+(13, NULL, '字典列表', 'dict.list', 12, 1),
+(14, NULL, '字典新增', 'dict.add', 12, 2),
+(15, NULL, '字典编辑', 'dict.edit', 12, 3),
+(16, NULL, '字典删除', 'dict.delete', 12, 4),
+(17, NULL, '日志管理', 'log', 1, 40),
+(18, NULL, '日志列表', 'log.list', 17, 1),
+(19, NULL, '公告管理', 'notice', 1, 50),
+(20, NULL, '公告列表', 'notice.list', 19, 1),
+(21, NULL, '公告新增', 'notice.add', 19, 2),
+(22, NULL, '公告编辑', 'notice.edit', 19, 3),
+(23, NULL, '公告删除', 'notice.delete', 19, 4),
+-- 消息管理
+(24, NULL, '消息管理', 'message', 0, 90),
+(25, NULL, '消息列表', 'message.list', 24, 1),
+(26, NULL, '发送消息', 'message.send', 24, 2),
+(27, NULL, '消息删除', 'message.delete', 24, 3);
+
+-- ----------------------------
+-- 初始化用户角色关联
+-- ----------------------------
+INSERT INTO `t_admin_user_role` (`admin_user_id`, `role_id`) VALUES
+(1, 1),
+(2, 2);
+
+-- ----------------------------
+-- 初始化角色权限关联(超级管理员拥有所有权限)
+-- ----------------------------
+INSERT INTO `t_role_permission` (`role_id`, `permission_id`)
+SELECT 1, id FROM `t_permission`;
+
+-- ----------------------------
+-- 初始化数据字典
+-- ----------------------------
+INSERT INTO `t_data_dict` (`code`, `name`, `value`, `weight`, `remark`, `color`) VALUES
+-- 用户状态
+('user_status', '禁用', '0', 1, '用户状态', 'danger'),
+('user_status', '启用', '1', 2, '用户状态', 'success'),
+-- 消息类型
+('message_type', '系统通知', '1', 1, '消息类型', 'primary'),
+('message_type', '站内信', '2', 2, '消息类型', 'success'),
+('message_type', '待办事项', '3', 3, '消息类型', 'warning'),
+('message_type', '公告通知', '4', 4, '消息类型', 'info'),
+-- 消息状态
+('message_status', '未读', '0', 1, '消息状态', 'danger'),
+('message_status', '已读', '1', 2, '消息状态', 'success'),
+('message_status', '已删除', '2', 3, '消息状态', 'info'),
+-- 优先级
+('priority', '普通', '0', 1, '优先级', ''),
+('priority', '重要', '1', 2, '优先级', 'warning'),
+('priority', '紧急', '2', 3, '优先级', 'danger'),
+-- 公告状态
+('notice_status', '未开始', '0', 1, '公告状态', 'info'),
+('notice_status', '生效中', '1', 2, '公告状态', 'success'),
+('notice_status', '已结束', '2', 3, '公告状态', ''),
+('notice_status', '已取消', '3', 4, '公告状态', 'danger'),
+-- 是否
+('yes_no', '否', '0', 1, '是否', 'info'),
+('yes_no', '是', '1', 2, '是否', 'success');
+
+-- ----------------------------
+-- 初始化消息(示例数据)
+-- ----------------------------
+INSERT INTO `t_message` (`title`, `content`, `type`, `sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `status`, `priority`) VALUES
+('系统升级通知', '系统将于近期进行升级维护,届时系统将暂停服务,请提前做好准备。', 1, 0, '系统', 1, '超级管理员', 0, 2),
+('欢迎使用', '欢迎使用洗车管理系统,如有问题请联系管理员。', 1, 0, '系统', 1, '超级管理员', 0, 0),
+('欢迎使用', '欢迎使用洗车管理系统,如有问题请联系管理员。', 1, 0, '系统', 2, '测试用户', 0, 0);
+
+-- ----------------------------
+-- 初始化消息模板(示例数据)
+-- ----------------------------
+INSERT INTO `t_message_template` (`code`, `name`, `type`, `title_template`, `content_template`, `priority`, `status`, `remark`) VALUES
+('WELCOME', '欢迎注册', 1, '欢迎加入${systemName}', '尊敬的${userName},欢迎使用${systemName},如有任何问题,请联系管理员。', 0, 1, '新用户注册时发送'),
+('SYSTEM_UPGRADE', '系统升级通知', 1, '系统升级维护通知', '系统将于${upgradeTime}进行升级维护,预计持续${duration},届时系统将暂停服务,请提前做好准备。', 2, 1, '系统升级时发送'),
+('ORDER_COMPLETE', '订单完成通知', 2, '您的订单已完成', '尊敬的${userName},您的订单${orderNo}已完成,感谢您的使用!', 0, 1, '订单完成时发送'),
+('TODO_REMINDER', '待办事项提醒', 3, '您有新的待办事项', '您有${count}个待处理的${taskType},请及时处理。', 1, 1, '待办事项提醒'),
+('ANNOUNCEMENT', '系统公告', 4, '${title}', '${content}', 0, 1, '通用公告模板');
+
+-- ====================================================
+-- 更新公司超管信息
+-- ====================================================
+UPDATE `t_company` SET `super_admin_id` = 1, `super_admin_name` = 'admin' WHERE `id` = 1;

+ 33 - 0
car-wash-entity/src/main/resources/sql/message.sql

@@ -0,0 +1,33 @@
+-- 消息通知表
+CREATE TABLE IF NOT EXISTS `t_message` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `company_id` BIGINT DEFAULT NULL COMMENT '公司(租户)ID',
+    `title` VARCHAR(200) NOT NULL COMMENT '消息标题',
+    `content` TEXT COMMENT '消息内容',
+    `type` TINYINT NOT NULL DEFAULT 1 COMMENT '消息类型:1-系统通知,2-站内信,3-待办事项,4-公告通知',
+    `sender_id` BIGINT DEFAULT 0 COMMENT '发送者ID(0表示系统发送)',
+    `sender_name` VARCHAR(50) DEFAULT '系统' COMMENT '发送者名称',
+    `receiver_id` BIGINT NOT NULL COMMENT '接收者ID',
+    `receiver_name` VARCHAR(50) DEFAULT NULL COMMENT '接收者名称',
+    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '消息状态:0-未读,1-已读,2-已删除',
+    `read_time` DATETIME DEFAULT NULL COMMENT '阅读时间',
+    `biz_type` VARCHAR(50) DEFAULT NULL COMMENT '关联业务类型',
+    `biz_id` BIGINT DEFAULT NULL COMMENT '关联业务ID',
+    `priority` TINYINT NOT NULL DEFAULT 0 COMMENT '优先级:0-普通,1-重要,2-紧急',
+    `is_push` TINYINT NOT NULL DEFAULT 0 COMMENT '是否推送:0-否,1-是',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_receiver_id` (`receiver_id`),
+    KEY `idx_sender_id` (`sender_id`),
+    KEY `idx_type` (`type`),
+    KEY `idx_status` (`status`),
+    KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息通知表';
+
+-- 插入测试数据
+INSERT INTO `t_message` (`title`, `content`, `type`, `sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `status`, `priority`) VALUES
+('系统升级通知', '系统将于2024年1月20日凌晨2:00-4:00进行升级维护,届时系统将暂停服务,请提前做好准备。', 1, 0, '系统', 1, '管理员', 0, 2),
+('新功能上线', '消息通知中心功能已上线,您可以在此查看所有消息通知。', 1, 0, '系统', 1, '管理员', 0, 0),
+('待办事项提醒', '您有3个待审批的工单,请及时处理。', 3, 0, '系统', 1, '管理员', 0, 1),
+('欢迎使用', '欢迎使用洗车管理系统,如有问题请联系管理员。', 2, 0, '系统', 1, '管理员', 1, 0);

+ 51 - 0
car-wash-mapper/src/main/java/com/kym/mapper/MessageMapper.java

@@ -0,0 +1,51 @@
+package com.kym.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.kym.entity.Message;
+import com.kym.entity.queryParams.MessageQueryParams;
+import com.kym.mapper.mybatisplus.MyBaseMapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 消息通知 Mapper 接口
+ *
+ * @author skyline
+ * @since 2024-01-15
+ */
+public interface MessageMapper extends MyBaseMapper<Message> {
+
+    /**
+     * 分页查询消息列表
+     *
+     * @param page   分页参数
+     * @param params 查询参数
+     * @return 消息列表
+     */
+    IPage<Message> selectMessagePage(IPage<Message> page, @Param("params") MessageQueryParams params);
+
+    /**
+     * 统计未读消息数量
+     *
+     * @param receiverId 接收者ID
+     * @return 未读数量
+     */
+    int countUnread(@Param("receiverId") Long receiverId);
+
+    /**
+     * 按类型统计未读消息数量
+     *
+     * @param receiverId 接收者ID
+     * @param type       消息类型
+     * @return 未读数量
+     */
+    int countUnreadByType(@Param("receiverId") Long receiverId, @Param("type") Integer type);
+
+    /**
+     * 批量标记已读
+     *
+     * @param receiverId 接收者ID
+     * @param type       消息类型(可选,为null时标记全部)
+     * @return 影响行数
+     */
+    int batchMarkRead(@Param("receiverId") Long receiverId, @Param("type") Integer type);
+}

+ 79 - 0
car-wash-mapper/src/main/resources/mappers/MessageMapper.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.kym.mapper.MessageMapper">
+
+    <!-- 消息结果映射 -->
+    <resultMap id="MessageResultMap" type="com.kym.entity.Message">
+        <id column="id" property="id"/>
+        <result column="company_id" property="companyId"/>
+        <result column="title" property="title"/>
+        <result column="content" property="content"/>
+        <result column="type" property="type"/>
+        <result column="sender_id" property="senderId"/>
+        <result column="sender_name" property="senderName"/>
+        <result column="receiver_id" property="receiverId"/>
+        <result column="receiver_name" property="receiverName"/>
+        <result column="status" property="status"/>
+        <result column="read_time" property="readTime"/>
+        <result column="biz_type" property="bizType"/>
+        <result column="biz_id" property="bizId"/>
+        <result column="priority" property="priority"/>
+        <result column="is_push" property="isPush"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_time" property="updateTime"/>
+    </resultMap>
+
+    <!-- 分页查询消息列表 -->
+    <select id="selectMessagePage" resultMap="MessageResultMap">
+        SELECT * FROM t_message
+        <where>
+            status != 2
+            <if test="params.receiverId != null">
+                AND receiver_id = #{params.receiverId}
+            </if>
+            <if test="params.senderId != null">
+                AND sender_id = #{params.senderId}
+            </if>
+            <if test="params.type != null">
+                AND type = #{params.type}
+            </if>
+            <if test="params.status != null">
+                AND status = #{params.status}
+            </if>
+            <if test="params.title != null and params.title != ''">
+                AND title LIKE CONCAT('%', #{params.title}, '%')
+            </if>
+            <if test="params.priority != null">
+                AND priority = #{params.priority}
+            </if>
+        </where>
+        ORDER BY priority DESC, create_time DESC
+    </select>
+
+    <!-- 统计未读消息数量 -->
+    <select id="countUnread" resultType="int">
+        SELECT COUNT(*) FROM t_message
+        WHERE receiver_id = #{receiverId}
+        AND status = 0
+    </select>
+
+    <!-- 按类型统计未读消息数量 -->
+    <select id="countUnreadByType" resultType="int">
+        SELECT COUNT(*) FROM t_message
+        WHERE receiver_id = #{receiverId}
+        AND status = 0
+        AND type = #{type}
+    </select>
+
+    <!-- 批量标记已读 -->
+    <update id="batchMarkRead">
+        UPDATE t_message
+        SET status = 1, read_time = NOW()
+        WHERE receiver_id = #{receiverId}
+        AND status = 0
+        <if test="type != null">
+            AND type = #{type}
+        </if>
+    </update>
+
+</mapper>

+ 110 - 0
car-wash-service/src/main/java/com/kym/service/MessageService.java

@@ -0,0 +1,110 @@
+package com.kym.service;
+
+import com.kym.entity.Message;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.MessageQueryParams;
+import com.kym.entity.vo.MessageVo;
+import com.kym.service.mybatisplus.MyBaseService;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 消息通知服务接口
+ *
+ * @author skyline
+ * @since 2024-01-15
+ */
+public interface MessageService extends MyBaseService<Message> {
+
+    /**
+     * 发送消息
+     *
+     * @param vo 消息VO
+     */
+    void sendMessage(MessageVo vo);
+
+    /**
+     * 批量发送消息
+     *
+     * @param vo 包含接收者列表的消息VO
+     */
+    void batchSendMessage(MessageVo vo);
+
+    /**
+     * 分页查询消息列表
+     *
+     * @param params 查询参数
+     * @return 分页消息列表
+     */
+    PageBean<Message> listMessage(MessageQueryParams params);
+
+    /**
+     * 获取消息详情
+     *
+     * @param messageId 消息ID
+     * @return 消息详情
+     */
+    Message getMessageDetail(Long messageId);
+
+    /**
+     * 标记消息已读
+     *
+     * @param messageId 消息ID
+     */
+    void markAsRead(Long messageId);
+
+    /**
+     * 批量标记已读
+     *
+     * @param messageIds 消息ID列表
+     */
+    void batchMarkAsRead(List<Long> messageIds);
+
+    /**
+     * 全部标记已读
+     *
+     * @param receiverId 接收者ID
+     * @param type       消息类型(可选)
+     */
+    void markAllAsRead(Long receiverId, Integer type);
+
+    /**
+     * 删除消息
+     *
+     * @param messageId 消息ID
+     */
+    void deleteMessage(Long messageId);
+
+    /**
+     * 批量删除消息
+     *
+     * @param messageIds 消息ID列表
+     */
+    void batchDeleteMessage(List<Long> messageIds);
+
+    /**
+     * 获取未读消息数量
+     *
+     * @param receiverId 接收者ID
+     * @return 未读数量
+     */
+    int getUnreadCount(Long receiverId);
+
+    /**
+     * 按类型获取未读消息数量
+     *
+     * @param receiverId 接收者ID
+     * @return 各类型未读数量
+     */
+    Map<Integer, Integer> getUnreadCountByType(Long receiverId);
+
+    /**
+     * 获取最新未读消息(用于下拉预览)
+     *
+     * @param receiverId 接收者ID
+     * @param limit      数量限制
+     * @return 最新消息列表
+     */
+    List<Message> getLatestUnread(Long receiverId, int limit);
+}

+ 224 - 0
car-wash-service/src/main/java/com/kym/service/impl/MessageServiceImpl.java

@@ -0,0 +1,224 @@
+package com.kym.service.impl;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.kym.common.utils.IDGenerator;
+import com.kym.entity.AdminUser;
+import com.kym.entity.Message;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.MessageQueryParams;
+import com.kym.entity.vo.MessageVo;
+import com.kym.mapper.MessageMapper;
+import com.kym.service.AdminUserService;
+import com.kym.service.MessageService;
+import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 消息通知服务实现类
+ *
+ * @author skyline
+ * @since 2024-01-15
+ */
+@Service
+public class MessageServiceImpl extends MyBaseServiceImpl<MessageMapper, Message> implements MessageService {
+
+    private final AdminUserService adminUserService;
+
+    public MessageServiceImpl(AdminUserService adminUserService) {
+        this.adminUserService = adminUserService;
+    }
+
+    @Override
+    @Transactional
+    public void sendMessage(MessageVo vo) {
+        Message message = new Message();
+        BeanUtils.copyProperties(vo, message);
+
+        // 设置发送者信息
+        Long senderId = StpUtil.getLoginIdAsLong();
+        String senderName = StpUtil.getSession().getString("username");
+        message.setSenderId(senderId)
+                .setSenderName(senderName)
+                .setStatus(0)
+                .setBaseId(IDGenerator.INS().nextId());
+
+        // 设置接收者名称
+        if (vo.getReceiverId() != null) {
+            AdminUser receiver = adminUserService.getById(vo.getReceiverId());
+            if (receiver != null) {
+                message.setReceiverName(receiver.getNickname());
+            }
+        }
+
+        save(message);
+    }
+
+    @Override
+    @Transactional
+    public void batchSendMessage(MessageVo vo) {
+        if (vo.getReceiverIds() == null || vo.getReceiverIds().isEmpty()) {
+            return;
+        }
+
+        Long senderId = StpUtil.getLoginIdAsLong();
+        String senderName = StpUtil.getSession().getString("username");
+
+        List<Message> messages = new ArrayList<>();
+        for (Long receiverId : vo.getReceiverIds()) {
+            Message message = new Message();
+            message.setTitle(vo.getTitle())
+                    .setContent(vo.getContent())
+                    .setType(vo.getType())
+                    .setSenderId(senderId)
+                    .setSenderName(senderName)
+                    .setReceiverId(receiverId)
+                    .setStatus(0)
+                    .setPriority(vo.getPriority() != null ? vo.getPriority() : 0)
+                    .setBizType(vo.getBizType())
+                    .setBizId(vo.getBizId())
+                    .setIsPush(vo.getIsPush() != null ? vo.getIsPush() : 0)
+                    .setBaseId(IDGenerator.INS().nextId());
+
+            // 设置接收者名称
+            AdminUser receiver = adminUserService.getById(receiverId);
+            if (receiver != null) {
+                message.setReceiverName(receiver.getNickname());
+            }
+
+            messages.add(message);
+        }
+
+        saveBatch(messages);
+    }
+
+    @Override
+    public PageBean<Message> listMessage(MessageQueryParams params) {
+        com.github.pagehelper.PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        MessageQueryParams queryParams = new MessageQueryParams();
+        queryParams.setReceiverId(params.getReceiverId());
+        queryParams.setType(params.getType());
+        queryParams.setStatus(params.getStatus());
+        queryParams.setTitle(params.getTitle());
+        queryParams.setPriority(params.getPriority());
+        
+        List<Message> list = lambdaQuery()
+                .ne(Message::getStatus, 2)
+                .eq(params.getReceiverId() != null, Message::getReceiverId, params.getReceiverId())
+                .eq(params.getType() != null, Message::getType, params.getType())
+                .eq(params.getStatus() != null, Message::getStatus, params.getStatus())
+                .like(params.getTitle() != null, Message::getTitle, params.getTitle())
+                .eq(params.getPriority() != null, Message::getPriority, params.getPriority())
+                .orderByDesc(Message::getPriority)
+                .orderByDesc(Message::getCreateTime)
+                .list();
+        return new PageBean<>(list);
+    }
+
+    @Override
+    public Message getMessageDetail(Long messageId) {
+        Message message = getById(messageId);
+        // 自动标记已读
+        if (message != null && message.getStatus() == 0) {
+            message.setStatus(1);
+            message.setReadTime(LocalDateTime.now());
+            updateById(message);
+        }
+        return message;
+    }
+
+    @Override
+    @Transactional
+    public void markAsRead(Long messageId) {
+        Message message = getById(messageId);
+        if (message != null && message.getStatus() == 0) {
+            message.setStatus(1);
+            message.setReadTime(LocalDateTime.now());
+            updateById(message);
+        }
+    }
+
+    @Override
+    @Transactional
+    public void batchMarkAsRead(List<Long> messageIds) {
+        if (messageIds == null || messageIds.isEmpty()) {
+            return;
+        }
+
+        List<Message> messages = listByIds(messageIds);
+        LocalDateTime now = LocalDateTime.now();
+        for (Message message : messages) {
+            if (message.getStatus() == 0) {
+                message.setStatus(1);
+                message.setReadTime(now);
+            }
+        }
+        updateBatchById(messages);
+    }
+
+    @Override
+    @Transactional
+    public void markAllAsRead(Long receiverId, Integer type) {
+        baseMapper.batchMarkRead(receiverId, type);
+    }
+
+    @Override
+    @Transactional
+    public void deleteMessage(Long messageId) {
+        Message message = getById(messageId);
+        if (message != null) {
+            message.setStatus(2); // 已删除
+            updateById(message);
+        }
+    }
+
+    @Override
+    @Transactional
+    public void batchDeleteMessage(List<Long> messageIds) {
+        if (messageIds == null || messageIds.isEmpty()) {
+            return;
+        }
+
+        List<Message> messages = listByIds(messageIds);
+        for (Message message : messages) {
+            message.setStatus(2); // 已删除
+        }
+        updateBatchById(messages);
+    }
+
+    @Override
+    public int getUnreadCount(Long receiverId) {
+        return baseMapper.countUnread(receiverId);
+    }
+
+    @Override
+    public Map<Integer, Integer> getUnreadCountByType(Long receiverId) {
+        Map<Integer, Integer> result = new HashMap<>();
+        // 类型:1-系统通知,2-站内信,3-待办事项,4-公告通知
+        for (int type = 1; type <= 4; type++) {
+            result.put(type, baseMapper.countUnreadByType(receiverId, type));
+        }
+        return result;
+    }
+
+    @Override
+    public List<Message> getLatestUnread(Long receiverId, int limit) {
+        MessageQueryParams params = new MessageQueryParams();
+        params.setReceiverId(receiverId);
+        params.setStatus(0);
+        params.setPageNum(1);
+        params.setPageSize(limit);
+
+        IPage<Message> page = new Page<>(1, limit);
+        return baseMapper.selectMessagePage(page, params).getRecords();
+    }
+}