提交修改,后台管理页面bug修复,已经发布后台管理界面V1.0版本

This commit is contained in:
2025-12-25 17:56:09 +08:00
parent 845f1c6618
commit b1da484431
23 changed files with 614 additions and 257 deletions

View File

@@ -190,6 +190,80 @@ namespace WxCheckMvc.Controllers
} }
} }
} }
[HttpGet]
public async Task<IActionResult> QueryStats()
{
try
{
if (_connection.State != ConnectionState.Open)
{
await _connection.OpenAsync();
}
// 1) 活跃用户:最近 7 天登录UpdateTime且 UserKey/PhoneNumber 不为空
long activeUsers;
using (MySqlCommand cmd = new(@"
SELECT COUNT(1)
FROM xcx_users
WHERE UpdateTime >= DATE_SUB(NOW(), INTERVAL 7 DAY)
AND UserKey IS NOT NULL AND UserKey <> ''
AND PhoneNumber IS NOT NULL AND PhoneNumber <> ''", _connection))
{
activeUsers = Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
// 2) 总会话记录数
long totalConversations;
using (MySqlCommand cmd = new("SELECT COUNT(1) FROM xcx_conversation", _connection))
{
totalConversations = Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
// 3) 今日新增会话记录CreateTime 在今天内)
long todayNewConversations;
using (MySqlCommand cmd = new(@"
SELECT COUNT(1)
FROM xcx_conversation
WHERE CreateTime >= CURDATE()
AND CreateTime < DATE_ADD(CURDATE(), INTERVAL 1 DAY)", _connection))
{
todayNewConversations = Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
// 4) 总用户数UserKey/PhoneNumber 不为空
long totalUsers;
using (MySqlCommand cmd = new(@"
SELECT COUNT(1)
FROM xcx_users
WHERE UserKey IS NOT NULL AND UserKey <> ''
AND PhoneNumber IS NOT NULL AND PhoneNumber <> ''", _connection))
{
totalUsers = Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
var data = new AdminStatsResponse
{
ActiveUsers = activeUsers,
TotalConversations = totalConversations,
TodayNewConversations = todayNewConversations,
TotalUsers = totalUsers
};
return Ok(new { success = true, data });
}
catch (Exception ex)
{
return StatusCode(500, new { success = false, message = "查询失败", error = ex.Message });
}
finally
{
if (_connection.State == ConnectionState.Open)
{
await _connection.CloseAsync();
}
}
}
} }
public class ConversationQueryRequest public class ConversationQueryRequest
@@ -240,4 +314,12 @@ namespace WxCheckMvc.Controllers
public string AvatarUrl { get; set; } public string AvatarUrl { get; set; }
public string Department { get; set; } public string Department { get; set; }
} }
public class AdminStatsResponse
{
public long ActiveUsers { get; set; }
public long TotalConversations { get; set; }
public long TodayNewConversations { get; set; }
public long TotalUsers { get; set; }
}
} }

View File

@@ -266,6 +266,15 @@ namespace WxCheckMvc.Controllers
await insertCmd.ExecuteNonQueryAsync(); await insertCmd.ExecuteNonQueryAsync();
} }
} }
else
{
// 用户已存在:更新最后一次接口调用时间
using (MySqlCommand updateCmd = new MySqlCommand("UPDATE xcx_users SET UpdateTime = NOW() WHERE UserKey = @UserKey", _connection))
{
updateCmd.Parameters.AddWithValue("@UserKey", openId);
await updateCmd.ExecuteNonQueryAsync();
}
}
} }
// 获取用户信息 // 获取用户信息

View File

@@ -3,7 +3,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<_PublishTargetUrl>E:\Project_Class\WX_XCX\Wx_WxCheck_Prod\WxCheckMvc\bin\Release\net8.0\publish\</_PublishTargetUrl> <_PublishTargetUrl>E:\Project_Class\WX_XCX\Wx_WxCheck_Prod\WxCheckMvc\bin\Release\net8.0\publish\</_PublishTargetUrl>
<History>True|2025-12-24T12:05:02.2999541Z||;True|2025-12-24T16:33:44.2108439+08:00||;True|2025-12-24T15:32:13.8037439+08:00||;True|2025-12-12T11:09:28.8147447+08:00||;True|2025-12-11T17:04:53.2856075+08:00||;True|2025-12-11T17:04:22.0809574+08:00||;True|2025-12-05T18:56:51.7439135+08:00||;True|2025-12-05T17:44:11.4130698+08:00||;</History> <History>True|2025-12-25T06:00:56.3451051Z||;True|2025-12-24T20:05:02.2999541+08:00||;True|2025-12-24T16:33:44.2108439+08:00||;True|2025-12-24T15:32:13.8037439+08:00||;True|2025-12-12T11:09:28.8147447+08:00||;True|2025-12-11T17:04:53.2856075+08:00||;True|2025-12-11T17:04:22.0809574+08:00||;True|2025-12-05T18:56:51.7439135+08:00||;True|2025-12-05T17:44:11.4130698+08:00||;</History>
<LastFailureDetails /> <LastFailureDetails />
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin-web</title> <title>宝来威办公助手后台</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -6,9 +6,9 @@
type="text" type="text"
:icon="Menu" :icon="Menu"
@click="toggleSidebar" @click="toggleSidebar"
></el-button> >菜单</el-button>
<div class="logo"> <div class="logo">
<h1>后台管理</h1> <h1>宝来威办公助手后台</h1>
</div> </div>
</div> </div>
@@ -47,6 +47,7 @@ import { Menu, ArrowDown, SwitchButton } from '@element-plus/icons-vue'
import { useAuthStore } from '../../store/auth' import { useAuthStore } from '../../store/auth'
import ThemeSwitcher from '../ThemeSwitcher.vue' import ThemeSwitcher from '../ThemeSwitcher.vue'
// 路由实例 // 路由实例
const router = useRouter() const router = useRouter()
@@ -56,9 +57,6 @@ const authStore = useAuthStore()
// 用户名 // 用户名
const username = computed(() => authStore.username) const username = computed(() => authStore.username)
// 侧边栏显示状态(手机端)
const isSidebarOpen = ref(false)
// 响应式布局:判断是否为手机端 // 响应式布局:判断是否为手机端
const isMobile = ref(false) const isMobile = ref(false)
@@ -78,11 +76,9 @@ onUnmounted(() => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
}) })
// 切换侧边栏显示(手机端) // 打开侧边栏抽屉(手机端)
const toggleSidebar = () => { const toggleSidebar = () => {
isSidebarOpen.value = !isSidebarOpen.value emit('open-menu')
// 发送事件给父组件
emit('toggleSidebar', isSidebarOpen.value)
} }
// 登出处理 // 登出处理
@@ -92,10 +88,10 @@ const handleLogout = () => {
} }
// 定义事件 // 定义事件
const emit = defineEmits(['toggleSidebar']) const emit = defineEmits(['open-menu'])
</script> </script>
<style scoped> <style scoped lang="scss">
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -146,7 +142,7 @@ const emit = defineEmits(['toggleSidebar'])
} }
.logo h1 { .logo h1 {
font-size: 18px; display: none;
} }
.username { .username {

View File

@@ -1,16 +1,13 @@
<template> <template>
<div class="layout-container"> <div class="layout-container">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<Header @toggle-sidebar="handleToggleSidebar" /> <Header @open-menu="handleOpenMenu" />
<!-- 侧边栏 --> <!-- 侧边栏 -->
<Sidebar <Sidebar v-model="isMenuOpen" />
:is-open="isSidebarOpen"
@close-sidebar="handleCloseSidebar"
/>
<!-- 主内容区域 --> <!-- 主内容区域 -->
<main class="main-content" :class="{ 'main-content--open': isSidebarOpen }"> <main class="main-content">
<div class="content-wrapper"> <div class="content-wrapper">
<router-view /> <router-view />
</div> </div>
@@ -24,20 +21,15 @@ import Header from './Header.vue'
import Sidebar from './Sidebar.vue' import Sidebar from './Sidebar.vue'
// 侧边栏显示状态(手机端) // 侧边栏显示状态(手机端)
const isSidebarOpen = ref(false) const isMenuOpen = ref(false)
// 处理侧边栏切换事件来自Header组件 // Header 触发:打开抽屉菜单(手机端
const handleToggleSidebar = (isOpen) => { const handleOpenMenu = () => {
isSidebarOpen.value = isOpen isMenuOpen.value = true
}
// 处理侧边栏关闭事件来自Sidebar组件
const handleCloseSidebar = () => {
isSidebarOpen.value = false
} }
</script> </script>
<style scoped> <style scoped lang="scss">
.layout-container { .layout-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -64,13 +56,6 @@ const handleCloseSidebar = () => {
} }
} }
/* 手机端侧边栏打开时,主内容区域添加左侧边距 */
.main-content--open {
@media (max-width: 767px) {
margin-left: 0;
}
}
.content-wrapper { .content-wrapper {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;

View File

@@ -1,105 +1,105 @@
<template> <template>
<aside <!-- 桌面/平板固定侧边栏不做展开/收起动画 -->
class="sidebar" <aside v-if="!isMobile" class="sidebar-desktop">
:class="{ 'sidebar--open': isOpen }" <el-menu router :default-active="activePath" class="sidebar-menu" @select="handleSelect">
> <el-menu-item index="/">
<nav class="sidebar-nav"> <el-icon><HomeFilled /></el-icon>
<ul class="nav-list"> <span>首页</span>
<li class="nav-item" v-for="menu in menuList" :key="menu.path"> </el-menu-item>
<router-link <el-menu-item index="/conversations">
:to="menu.path" <el-icon><Message /></el-icon>
class="nav-link" <span>会话记录管理</span>
:class="{ 'is-active': $route.path === menu.path }" </el-menu-item>
@click="handleMenuClick" <el-menu-item index="/users">
> <el-icon><User /></el-icon>
<el-icon class="nav-icon"><component :is="menu.icon" /></el-icon> <span>用户管理</span>
<span class="nav-text">{{ menu.title }}</span> </el-menu-item>
</router-link> </el-menu>
</li>
</ul>
</nav>
<!-- 遮罩层手机端 -->
<div
v-if="isOpen && isMobile"
class="sidebar-mask"
@click="handleMaskClick"
></div>
</aside> </aside>
<!-- 手机端抽屉菜单Element Plus 自带隐藏/遮罩/动画 -->
<el-drawer
v-else
v-model="drawerOpen"
direction="ltr"
size="250px"
:with-header="false"
>
<el-menu router :default-active="activePath" class="drawer-menu" @select="handleSelect">
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/conversations">
<el-icon><Message /></el-icon>
<span>会话记录管理</span>
</el-menu-item>
<el-menu-item index="/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
</el-menu>
</el-drawer>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { HomeFilled, Message, User } from '@element-plus/icons-vue' import { HomeFilled, Message, User } from '@element-plus/icons-vue'
// 菜单数据
const menuList = [
{
path: '/',
title: '首页',
icon: HomeFilled
},
{
path: '/conversations',
title: '会话记录管理',
icon: Message
},
{
path: '/users',
title: '用户管理',
icon: User
}
]
// 侧边栏显示状态(手机端)
const props = defineProps({ const props = defineProps({
isOpen: { modelValue: {
type: Boolean, type: Boolean,
default: false default: false
} }
}) })
// 定义事件 const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(['closeSidebar'])
const route = useRoute()
// 响应式布局:判断是否为手机端 // 响应式布局:判断是否为手机端
const isMobile = ref(false) const isMobile = ref(false)
// 监听窗口大小变化
const handleResize = () => { const handleResize = () => {
isMobile.value = window.innerWidth < 768 isMobile.value = window.innerWidth < 768
// 如果不是手机端,关闭侧边栏 // 切回非手机端时,确保抽屉关闭
if (!isMobile.value) { if (!isMobile.value && props.modelValue) {
emit('closeSidebar') emit('update:modelValue', false)
} }
} }
// 初始化响应式状态
onMounted(() => { onMounted(() => {
handleResize() handleResize()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })
// 清理事件监听
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
}) })
// 菜单点击处理(手机端) const activePath = computed(() => route.path)
const handleMenuClick = () => {
if (isMobile.value) {
emit('closeSidebar')
}
}
// 遮罩层点击处理(手机端) // el-drawer v-model 适配
const handleMaskClick = () => { const drawerOpen = computed({
emit('closeSidebar') get() {
return props.modelValue
},
set(val) {
emit('update:modelValue', val)
}
})
const handleSelect = () => {
// 手机端点击菜单项后自动关闭抽屉
if (isMobile.value) {
emit('update:modelValue', false)
}
} }
</script> </script>
<style scoped> <style scoped lang="scss">
.sidebar { .sidebar-desktop {
position: fixed; position: fixed;
top: 64px; top: 64px;
left: 0; left: 0;
@@ -107,86 +107,20 @@ const handleMaskClick = () => {
width: 250px; width: 250px;
background-color: var(--background-color); background-color: var(--background-color);
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
transition: all 0.3s ease;
z-index: 99; z-index: 99;
overflow-y: auto; overflow-y: auto;
/* 桌面端默认显示,手机端默认隐藏 */ @media (max-width: 1023px) {
@media (max-width: 767px) {
transform: translateX(-100%);
}
}
/* 侧边栏打开状态(手机端) */
.sidebar--open {
@media (max-width: 767px) {
transform: translateX(0);
}
}
.sidebar-nav {
padding: 20px 0;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
}
.nav-item {
margin-bottom: 4px;
}
.nav-link {
display: flex;
align-items: center;
padding: 12px 24px;
color: var(--text-color);
text-decoration: none;
transition: all 0.3s;
&:hover {
background-color: var(--border-color-lighter);
color: var(--primary-color);
}
&.is-active {
background-color: rgba(64, 158, 255, 0.1);
color: var(--primary-color);
border-right: 3px solid var(--primary-color);
}
}
.nav-icon {
font-size: 18px;
margin-right: 12px;
width: 20px;
text-align: center;
}
.nav-text {
font-size: 14px;
}
/* 遮罩层(手机端) */
.sidebar-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 98;
@media (min-width: 768px) {
display: none;
}
}
/* 平板端侧边栏宽度调整 */
@media (min-width: 768px) and (max-width: 1023px) {
.sidebar {
width: 200px; width: 200px;
} }
} }
.sidebar-menu,
.drawer-menu {
border-right: none;
}
:deep(.el-drawer__body) {
padding: 0;
}
</style> </style>

View File

@@ -2,14 +2,16 @@ import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () => ({ state: () => ({
isLoggedIn: false, isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
username: '' username: localStorage.getItem('username') || ''
}), }),
actions: { actions: {
login(username, password) { login(username, password) {
if (username === 'Admin' && password === 'Admin') { if (username === 'Admin' && password === 'Admin') {
this.isLoggedIn = true this.isLoggedIn = true
this.username = username this.username = username
localStorage.setItem('isLoggedIn', 'true')
localStorage.setItem('username', username)
return true return true
} }
return false return false
@@ -17,6 +19,8 @@ export const useAuthStore = defineStore('auth', {
logout() { logout() {
this.isLoggedIn = false this.isLoggedIn = false
this.username = '' this.username = ''
localStorage.removeItem('isLoggedIn')
localStorage.removeItem('username')
} }
} }
}) })

View File

@@ -13,9 +13,37 @@
</div> </div>
</template> </template>
<el-form :model="filterForm" inline label-width="80px" size="small"> <el-form :model="filterForm" inline label-width="80px" size="small">
<el-form-item label="时间范围"> <el-form-item label="快捷范围">
<el-date-picker v-model="filterForm.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" <el-select v-model="filterForm.quickRange" placeholder="请选择" clearable style="width: 150px" @change="handleQuickRangeChange">
end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker> <el-option label="今天" value="today"></el-option>
<el-option label="最近3天" value="last3"></el-option>
<el-option label="最近7天" value="last7"></el-option>
<el-option label="最近30天" value="last30"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="filterForm.startDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
class="date-picker"
@change="handleManualDateChange"
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="filterForm.endDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
class="date-picker"
@change="handleManualDateChange"
/>
</el-form-item> </el-form-item>
<el-form-item label="用户"> <el-form-item label="用户">
@@ -110,12 +138,51 @@ const emptyText = computed(() => loading.value ? '加载中...' : '暂无数据'
// 筛选表单数据 // 筛选表单数据
const filterForm = reactive({ const filterForm = reactive({
dateRange: [], quickRange: 'last7',
startDate: '',
endDate: '',
userKey: '', userKey: '',
messageType: '', messageType: '',
department: '' department: ''
}) })
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
const addDays = (date, days) => {
const d = new Date(date)
d.setDate(d.getDate() + days)
return d
}
const applyQuickRange = (range) => {
const today = new Date()
const end = formatDate(today)
let start = end
if (range === 'today') start = end
if (range === 'last3') start = formatDate(addDays(today, -2))
if (range === 'last7') start = formatDate(addDays(today, -6))
if (range === 'last30') start = formatDate(addDays(today, -29))
filterForm.startDate = start
filterForm.endDate = end
}
const handleQuickRangeChange = (value) => {
if (!value) return
applyQuickRange(value)
}
const handleManualDateChange = () => {
// 手动修改开始/结束日期时,取消快捷范围选中态
filterForm.quickRange = ''
}
// 会话记录列表 // 会话记录列表
const conversationList = ref([]) const conversationList = ref([])
@@ -135,12 +202,13 @@ const handleQuery = () => {
const handleReset = () => { const handleReset = () => {
// 重置表单 // 重置表单
Object.keys(filterForm).forEach(key => { Object.keys(filterForm).forEach(key => {
if (key === 'dateRange') { filterForm[key] = ''
filterForm[key] = []
} else {
filterForm[key] = ''
}
}) })
// 默认最近7天
filterForm.quickRange = 'last7'
applyQuickRange('last7')
currentPage.value = 1 currentPage.value = 1
fetchConversations() fetchConversations()
} }
@@ -196,8 +264,8 @@ const fetchConversations = async (isLoadMore = false) => {
try { try {
// 构建请求参数 // 构建请求参数
const params = { const params = {
startTime: filterForm.dateRange[0] ? new Date(filterForm.dateRange[0]).getTime() : null, startTime: filterForm.startDate ? `${filterForm.startDate}T00:00:00` : null,
endTime: filterForm.dateRange[1] ? new Date(filterForm.dateRange[1]).getTime() + 24 * 60 * 60 * 1000 - 1 : null, endTime: filterForm.endDate ? `${filterForm.endDate}T23:59:59.999` : null,
userKey: filterForm.userKey, userKey: filterForm.userKey,
messageType: filterForm.messageType ? Number(filterForm.messageType) : null, messageType: filterForm.messageType ? Number(filterForm.messageType) : null,
department: filterForm.department, department: filterForm.department,
@@ -272,6 +340,8 @@ const fetchUsers = async () => {
// 页面初始化 // 页面初始化
onMounted(() => { onMounted(() => {
// 默认最近7天
applyQuickRange('last7')
fetchUsers() fetchUsers()
fetchConversations() fetchConversations()
}) })

View File

@@ -4,13 +4,26 @@
<!-- 系统统计卡片 --> <!-- 系统统计卡片 -->
<div class="stats-container"> <div class="stats-container">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon department-icon">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="stat-info">
<h3>{{ totalUserCount }}</h3>
<p>总用户数</p>
</div>
</div>
</el-card>
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
<div class="stat-icon user-icon"> <div class="stat-icon user-icon">
<el-icon><User /></el-icon> <el-icon><User /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ userCount }}</h3> <h3>{{ activeUserCount }}</h3>
<p>活跃用户</p> <p>活跃用户</p>
</div> </div>
</div> </div>
@@ -22,23 +35,12 @@
<el-icon><Message /></el-icon> <el-icon><Message /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ conversationCount }}</h3> <h3>{{ totalConversationCount }}</h3>
<p>会话记录</p> <p>会话记录</p>
</div> </div>
</div> </div>
</el-card> </el-card>
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon department-icon">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="stat-info">
<h3>{{ departmentCount }}</h3>
<p>部门数量</p>
</div>
</div>
</el-card>
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
@@ -46,8 +48,8 @@
<el-icon><Calendar /></el-icon> <el-icon><Calendar /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ todayCount }}</h3> <h3>{{ todayNewConversationCount }}</h3>
<p>今日新增</p> <p>今日会话</p>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -95,51 +97,25 @@ const authStore = useAuthStore()
const username = computed(() => authStore.username) const username = computed(() => authStore.username)
// 统计数据 // 统计数据
const userCount = ref(0) const activeUserCount = ref(0)
const conversationCount = ref(0) const totalConversationCount = ref(0)
const departmentCount = ref(0) const totalUserCount = ref(0)
const todayCount = ref(0) const todayNewConversationCount = ref(0)
// 最近会话记录 // 最近会话记录
const recentConversations = ref([]) const recentConversations = ref([])
// 数据转换:将后端返回的会话数据转换为前端所需格式
const convertConversationData = (data) => {
return data.map(item => ({
id: item.Id,
recordTime: new Date(item.RecordTime).toLocaleString('zh-CN'),
recordTimeUTCStamp: item.RecordTimeUTCStamp,
userName: item.UserName,
conversationContent: item.ConversationContent,
messageType: item.MessageType
}))
}
// 获取系统统计数据 // 获取系统统计数据
const fetchStats = async () => { const fetchStats = async () => {
try { try {
// 获取用户列表数据,用于统计 const res = await request.get('/Admin/QueryStats')
const usersData = await request.get('/Admin/QueryUsers') const stats = res?.data ?? res
const users = Array.isArray(usersData) ? usersData : usersData.data || []
// 获取会话记录数据,用于统计 activeUserCount.value = stats?.activeUsers ?? 0
const conversationsData = await request.post('/Admin/QueryConversations', {}) totalConversationCount.value = stats?.totalConversations ?? 0
const conversations = Array.isArray(conversationsData) ? conversationsData : conversationsData.data || [] totalUserCount.value = stats?.totalUsers ?? 0
todayNewConversationCount.value = stats?.todayNewConversations ?? 0
// 计算统计数据
userCount.value = users.length
conversationCount.value = conversations.length
// 计算部门数量
const departments = new Set(users.map(user => user.Department))
departmentCount.value = departments.size
// 计算今日新增会话数
const today = new Date().toDateString()
const todayConversations = conversations.filter(conv => {
return new Date(conv.RecordTime).toDateString() === today
})
todayCount.value = todayConversations.length
// 获取最近会话记录 // 获取最近会话记录
await fetchRecentConversations() await fetchRecentConversations()
@@ -152,21 +128,28 @@ const fetchStats = async () => {
// 获取最近会话记录 // 获取最近会话记录
const fetchRecentConversations = async () => { const fetchRecentConversations = async () => {
try { try {
// 调用API获取最近的5条会话记录 const params = {
const data = await request.post('/Admin/QueryConversations', {}) startTime: null,
endTime: null,
userKey: '',
messageType: null,
department: '',
page: 1,
pageSize: 8
}
// 调用API获取会话记录
const data = await request.post('/Admin/QueryConversations', JSON.stringify(params))
// 检查返回数据格式,确保是数组 // 检查返回数据格式,确保是数组
const rawData = Array.isArray(data) ? data : data.data || [] const rawData = Array.isArray(data) ? data : data.data || []
// 转换数据格式
const convertedData = convertConversationData(rawData)
// 按时间排序最新的记录在前面并只显示最近5条 // 按时间排序最新的记录在前面并只显示最近5条
recentConversations.value = convertedData recentConversations.value = rawData
.sort((a, b) => { .sort((a, b) => {
return b.recordTimeUTCStamp - a.recordTimeUTCStamp return b.recordTimeUTCStamp - a.recordTimeUTCStamp
}) })
.slice(0, 5) .slice(0, 10)
} catch (error) { } catch (error) {
console.error('获取最近会话记录失败:', error) console.error('获取最近会话记录失败:', error)
recentConversations.value = [] recentConversations.value = []

View File

@@ -32,7 +32,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive } from 'vue' import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useAuthStore } from '../store/auth' import { useAuthStore } from '../store/auth'
@@ -45,7 +45,7 @@ const loading = ref(false)
// 登录表单数据 // 登录表单数据
const loginForm = reactive({ const loginForm = reactive({
username: '', username: 'Admin',
password: '' password: ''
}) })
@@ -65,6 +65,13 @@ const router = useRouter()
// 认证状态管理 // 认证状态管理
const authStore = useAuthStore() const authStore = useAuthStore()
// 处理键盘按键事件
const handleKeyPress = (event) => {
if (event.key === 'Enter') {
handleLogin()
}
}
// 登录处理 // 登录处理
const handleLogin = async () => { const handleLogin = async () => {
// 表单验证 // 表单验证
@@ -92,6 +99,16 @@ const handleLogin = async () => {
loading.value = false loading.value = false
} }
} }
// 组件挂载时添加键盘监听
onMounted(() => {
window.addEventListener('keypress', handleKeyPress)
})
// 组件卸载时移除键盘监听
onUnmounted(() => {
window.removeEventListener('keypress', handleKeyPress)
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -3,7 +3,7 @@
<h2>用户管理</h2> <h2>用户管理</h2>
<!-- 筛选表单 --> <!-- 筛选表单 -->
<el-card class="filter-card"> <!-- <el-card class="filter-card">
<el-form :model="filterForm" inline label-width="80px"> <el-form :model="filterForm" inline label-width="80px">
<el-form-item label="用户名"> <el-form-item label="用户名">
<el-input <el-input
@@ -44,7 +44,7 @@
<el-button @click="handleReset">重置</el-button> <el-button @click="handleReset">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card> -->
<!-- 用户列表表格 --> <!-- 用户列表表格 -->
<el-card class="table-card"> <el-card class="table-card">

View File

@@ -0,0 +1,19 @@
# Change: 管理端侧边栏改为 Element Plus Drawer + Menu事件触发
## Why
当前移动端侧边栏的显示/隐藏依赖 class + CSStransform实现在不同样式覆盖或构建产物下容易出现“class 变化但 UI 无反应”的问题,导致点击菜单按钮无法可靠展开。
## What Changes
- 移动端侧边栏改为使用 Element Plus 的 `el-drawer` 作为抽屉容器,依靠组件自带的显示/隐藏机制。
- 菜单改为 Element Plus 的 `el-menu`router 模式),用于导航到:首页、会话记录管理、用户管理。
- Header 菜单按钮改为**事件触发打开抽屉**(不再通过切换 class 驱动 CSS 动画)。
## Non-goals
- 不新增页面、不新增路由、不改动权限/鉴权逻辑。
## Impact
- Affected specs: `openspec/specs/backend-admin/spec.md`
- Affected code:
- `admin-web/src/components/Layout/Header.vue`
- `admin-web/src/components/Layout/Sidebar.vue`
- `admin-web/src/components/Layout/Layout.vue`

View File

@@ -0,0 +1,18 @@
## MODIFIED Requirements
### Requirement: 菜单布局Element Plus Drawer + Menu
管理端导航菜单 SHALL 使用 Element Plus 组件实现,并在移动端使用抽屉式菜单以保证可用性:
- 桌面/平板端显示固定侧边栏菜单Element Plus `el-menu`
- 手机端:点击 Header 菜单按钮后打开抽屉Element Plus `el-drawer`),抽屉内部为 `el-menu`
- 抽屉关闭:由 `el-drawer` 自带交互完成(点击遮罩/关闭动作),不依赖 class + CSS transform
#### Scenario: 手机端点击按钮打开菜单
- **WHEN** 管理员在手机端点击 Header 的菜单按钮
- **THEN** 系统通过事件触发打开 `el-drawer`
- **AND** 抽屉内显示 `el-menu` 导航项
#### Scenario: 手机端点击菜单项自动收起
- **WHEN** 管理员在抽屉中点击任一菜单项
- **THEN** 路由跳转到对应页面
- **AND** 抽屉自动关闭

View File

@@ -0,0 +1,9 @@
# Tasks — 管理端侧边栏改为 Element Plus Drawer + Menu
- [x] 更新规范:在本 change 下补充 delta specbackend-admin
- [x] 更新现行 spec`openspec/specs/backend-admin/spec.md` 的“菜单布局”要求
- [x] Header 改造点击菜单按钮触发事件open
- [x] Sidebar 改造:移动端使用 `el-drawer`,内部使用 `el-menu`
- [x] Layout 改造:接收 Header 事件,控制抽屉打开;抽屉关闭由 `el-drawer` v-model 驱动
- [x] 删除旧的 class+CSS 开关逻辑(不再依赖 `.sidebar--open`、transform
- [ ] 前端构建验证:`npm run build`admin-web

View File

@@ -0,0 +1,15 @@
# Change: 优化管理端会话列表日期筛选(移动端)
## Why
当前管理端会话列表使用单个 `daterange` 控件,在移动端交互与可用性较差;且缺少常用快捷时间范围,导致筛选效率低。
## What Changes
- 将“时间范围”从单个 `daterange` 控件改为 **两个独立日期选择器(开始日期、结束日期)**
- 新增 **快捷范围选择器**今天、最近3天、最近7天、最近30天。
- 默认筛选范围为 **最近7天**
- 做好移动端适配:在小屏下控件纵向排列、全宽显示。
## Impact
- Affected specs: `openspec/specs/backend-admin/spec.md`
- Affected code: `admin-web/src/views/ConversationList.vue`
- Backend API 不变:继续使用 `POST /api/Admin/QueryConversations``StartTime/EndTime` 过滤能力。

View File

@@ -0,0 +1,23 @@
## MODIFIED Requirements
### Requirement: 会话记录筛选(时间范围)
管理端会话记录页面 SHALL 提供便于移动端使用的时间范围筛选:
- 使用两个独立日期选择器:开始日期、结束日期
- 提供快捷范围选择器今天、最近3天、最近7天、最近30天
- 默认范围为最近7天
#### Scenario: 默认最近7天
- **WHEN** 管理员进入会话记录页面
- **THEN** 页面默认选中“最近7天”
- **AND** 请求会携带对应的 `StartTime/EndTime`
#### Scenario: 使用快捷范围
- **WHEN** 管理员选择“今天/最近3天/最近7天/最近30天”
- **THEN** 系统更新开始/结束日期
- **AND** 点击查询后按该范围过滤会话记录
#### Scenario: 移动端可用性
- **WHEN** 管理员在移动端访问会话记录页面
- **THEN** 快捷选择器与开始/结束日期选择器纵向排列
- **AND** 控件全宽显示,避免横向挤压导致误触

View File

@@ -0,0 +1,10 @@
# Tasks — 优化管理端会话列表日期筛选(移动端)
- [x] 更新管理端会话筛选 UI`daterange` 改为开始/结束两个日期选择器
- [x] 增加快捷范围选择器今天、最近3天、最近7天、最近30天
- [x] 默认值设置为最近7天并确保“重置”后仍回到默认范围
- [x] 调整请求参数:`StartTime/EndTime` 发送为可解析的 DateTime字符串/ISO确保后端可正确绑定
- [x] 移动端适配:小屏纵向排列、控件全宽
- [x] 更新 openspec
- [x]`openspec/specs/backend-admin/spec.md` 补充/修改相关 Requirement & Scenario
- [x] 在本 change 下添加 delta spec`openspec/changes/update-admin-conversation-date-filter/specs/backend-admin/spec.md`

View File

@@ -0,0 +1,48 @@
# Implementation — 修改登录并新增后台统计接口
## 已完成改动
1. 登录逻辑:`WxCheckMvc/Controllers/LoginController.cs`
- 修改点:在 `Login` 方法中,原来的存在用户分支不做更改;现在在判断 `count != 0`(用户存在)分支下新增:
```csharp
using (MySqlCommand updateCmd = new MySqlCommand("UPDATE xcx_users SET UpdateTime = NOW() WHERE UserKey = @UserKey", _connection))
{
updateCmd.Parameters.AddWithValue("@UserKey", openId);
await updateCmd.ExecuteNonQueryAsync();
}
```
- 目的:显式写入 `UpdateTime`,表示用户在本次登录调用中活跃。
2. 新增统计接口:`WxCheckMvc/Controllers/AdminController.cs`
- 新增方法 `QueryStats()`GET
- ActiveUsersSELECT COUNT(1) FROM xcx_users WHERE UpdateTime >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND UserKey <> '' AND PhoneNumber <> ''
- TotalConversationsSELECT COUNT(1) FROM xcx_conversation
- TodayNewConversationsSELECT COUNT(1) FROM xcx_conversation WHERE CreateTime >= CURDATE() AND CreateTime < DATE_ADD(CURDATE(), INTERVAL 1 DAY)
- TotalUsersSELECT COUNT(1) FROM xcx_users WHERE UserKey <> '' AND PhoneNumber <> ''
- 返回 DTO`AdminStatsResponse { long ActiveUsers, long TotalConversations, long TodayNewConversations, long TotalUsers }`
3. 前端首页统计卡片改造:`admin-web/src/views/Home.vue`
- 修改点:首页不再通过 `QueryUsers` / `QueryConversations` 拉全量数据后在前端计算统计值;改为直接调用 `GET /api/Admin/QueryStats`
- 映射关系:
- 活跃用户卡片:`ActiveUsers`
- 会话记录卡片:`TotalConversations`
- 总用户数卡片:`TotalUsers`
- 今日新增会话卡片:`TodayNewConversations`
## 测试与验证建议
- 本地运行 `dotnet build`:确认编译通过。
- 使用 Postman / curl 调用 `POST /api/Login/Login`(传入可换取 openid 的 `code` 或在测试中直接模拟 openid确认在用户存在时 `UpdateTime` 被修改。
- 调用 `GET /api/Admin/QueryStats` 验证返回结构并和数据库结果比对。
- 访问管理端首页,确认四张统计卡片展示值与接口返回一致。
## 备注
- `xcx_users.UpdateTime` 已在数据库 schema 中定义为 `DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP`,因此显式 `UPDATE` 是可行且语义明确的(无迁移需求)。
- 建议后续将统计接口限制为管理员访问(加入 `[Authorize(Roles = "Admin")]` 或使用策略)。

View File

@@ -0,0 +1,40 @@
# 修改登录逻辑并新增后台统计接口
## 1. 问题描述
当前系统登录逻辑(`POST /api/Login/Login`
- 使用微信 `code` 换取 `openid` 并以 `openid` 作为 `UserKey`
- 若数据库中不存在该 `UserKey`,系统会自动插入一条记录。
- 如果用户已存在count != 0当前实现不对用户表做任何更新包括 `UpdateTime`)。
另外,后台管理侧需要一个简洁的统计接口,用于展示系统关键指标(活跃用户、会话统计等),当前并没有统一的聚合接口。
同时,管理端首页当前通过拉取用户列表与会话列表在前端计算统计数据,调用开销大且不一致。需要改为直接使用聚合统计接口返回的四个指标。
## 2. 目标与提议
- 修改登录逻辑:当用户存在时,**更新该用户的 `UpdateTime` 为本次接口调用时间**UTC/服务器当前时间)。该字段将作为“最近登录/活跃”标准。
- 新增后台统计接口:`GET /api/Admin/QueryStats`返回单行统计数据JSON包含
1. ActiveUsers最近 7 天有登录(`UpdateTime` 在最近 7 天)且 `UserKey``PhoneNumber` 都不为空的用户数
2. TotalConversations`xcx_conversation` 表的总记录数
3. TodayNewConversations`xcx_conversation` 表中 `CreateTime` 在“今天”内的记录数
4. TotalUsers`xcx_users` 表中 `UserKey``PhoneNumber` 都不为空的用户总数
- 前端首页统计卡片改造管理端首页HomeSHALL 直接调用 `GET /api/Admin/QueryStats`,并将返回的四个数字映射到四张统计卡片中(不再通过 QueryUsers/QueryConversations 在前端计算)。
## 3. 兼容性与风险
- 数据库层无需新增字段:`xcx_users.UpdateTime` 已存在且带有 `ON UPDATE CURRENT_TIMESTAMP`。但显式执行 `UPDATE ... SET UpdateTime = NOW()` 可以确保在登录时明确写入(而不是依赖其他 UPDATE 操作触发)。
- 该变更不影响现有注册/资料完善逻辑。
- 需要为统计接口添加合理权限控制(建议在未来迭代中加入鉴权)。
## 4. 数据验证与示例SQL
- 活跃用户最近7天
SELECT COUNT(1) FROM xcx_users WHERE UpdateTime >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND UserKey <> '' AND PhoneNumber <> '';
- 今日新增会话:
SELECT COUNT(1) FROM xcx_conversation WHERE CreateTime >= CURDATE() AND CreateTime < DATE_ADD(CURDATE(), INTERVAL 1 DAY);
- 总会话、总用户:分别用 COUNT(1) 聚合。

View File

@@ -0,0 +1,25 @@
# Tasks — 修改登录并新增后台统计接口
1. 代码实现(后端) ✅
- 修改 `WxCheckMvc/Controllers/LoginController.cs`:当已存在用户时执行 `UPDATE xcx_users SET UpdateTime = NOW() WHERE UserKey = @UserKey`
- 新增 `WxCheckMvc/Controllers/AdminController.cs``QueryStats()` GET 方法,返回聚合统计结果(见实现细节)。
2. 前端改造(后台管理首页) ✅
- 修改 `admin-web/src/views/Home.vue`:系统统计卡片改为调用 `GET /Admin/QueryStats`,四张卡片分别展示接口返回的四个数字。
3. 单元/集成测试(可选但建议)
- 登录逻辑:模拟已存在用户的登录,断言 `UpdateTime` 更新为最近时间(或至少发生了写入)。
- 统计接口:在已知测试数据上断言返回的四个指标正确。
4. 文档与 Spec ✅
- 更新 `openspec/specs/backend-api/spec.md`,记录 Login 行为变更和新增 `GET /api/Admin/QueryStats` 的 API Contract。
- 更新 `openspec/specs/backend-admin/spec.md`,记录管理端首页统计使用 `QueryStats`
- 在 openspec 新建变更记录(已完成)。
5. 发布与验证
- 运行 `dotnet build` 确认编译通过
- 本地或测试环境调用接口验证结果
6. 可选:鉴权与监控(后续迭代)
-`QueryStats` 加上管理员鉴权JWT/Role
- 添加 Prometheus 或应用层度量埋点,记录统计接口调用频次

View File

@@ -108,6 +108,22 @@
- **THEN** 显示抽屉式菜单(点击菜单按钮展开) - **THEN** 显示抽屉式菜单(点击菜单按钮展开)
- **AND** 菜单包含:首页、会话记录管理、用户管理等功能入口 - **AND** 菜单包含:首页、会话记录管理、用户管理等功能入口
实现约束(移动端可靠性):
- 手机端抽屉菜单 SHOULD 使用 Element Plus `el-drawer` 自带的显示/隐藏机制
- 菜单 SHOULD 使用 Element Plus `el-menu`router 模式)
- 抽屉的打开 SHALL 由 Header 菜单按钮通过事件触发
- 不依赖 class + CSS transform 的方式实现“展开/收起”(避免出现 class 变化但 UI 无响应)
#### Scenario: 手机端点击按钮打开菜单(事件触发)
- **WHEN** 管理员在手机端点击 Header 的菜单按钮
- **THEN** 系统通过事件触发打开 `el-drawer`
- **AND** 抽屉内显示 `el-menu` 导航项
#### Scenario: 手机端点击菜单项自动收起
- **WHEN** 管理员在抽屉中点击任一菜单项
- **THEN** 路由跳转到对应页面
- **AND** 抽屉自动关闭
### Requirement: 模块化设计 ### Requirement: 模块化设计
前端管理网站 SHALL 采用模块化设计思路。 前端管理网站 SHALL 采用模块化设计思路。
@@ -173,6 +189,23 @@
- **WHEN** 管理端提交包含多个筛选条件的查询请求 - **WHEN** 管理端提交包含多个筛选条件的查询请求
- **THEN** 系统返回同时满足所有条件的会话记录 - **THEN** 系统返回同时满足所有条件的会话记录
### Requirement: 会话记录筛选(时间范围,移动端友好)
管理端会话记录页面 SHALL 提供便于移动端使用的时间范围筛选控件:
- 使用两个独立日期选择器:开始日期、结束日期
- 提供快捷范围选择器今天、最近3天、最近7天、最近30天
- 默认范围为最近7天
#### Scenario: 默认最近7天
- **WHEN** 管理员进入会话记录页面
- **THEN** 页面默认选中“最近7天”
- **AND** 查询请求携带对应的 `StartTime/EndTime`
#### Scenario: 移动端可用性
- **WHEN** 管理员在移动端访问会话记录页面
- **THEN** 快捷选择器与开始/结束日期选择器纵向排列
- **AND** 控件全宽显示,避免横向挤压
### Requirement: 查询用户列表(管理端) ### Requirement: 查询用户列表(管理端)
系统 SHALL 提供接口返回可用用户列表,用于管理侧选择/筛选。 系统 SHALL 提供接口返回可用用户列表,用于管理侧选择/筛选。
@@ -195,6 +228,25 @@
- **THEN** 返回已完善基础信息且未禁用的用户 - **THEN** 返回已完善基础信息且未禁用的用户
- **AND** 结果按首次登录时间倒序排列 - **AND** 结果按首次登录时间倒序排列
### Requirement: 首页统计(管理端)
系统 SHALL 提供聚合统计接口供管理端首页展示关键指标。
接口:`GET /api/Admin/QueryStats`
返回(成功):
- `success: true`
- `data`:包含以下字段的对象
- `ActiveUsers`:最近 7 天内 `UpdateTime` 有更新,且 `UserKey``PhoneNumber` 均不为空的用户数
- `TotalConversations``xcx_conversation` 总记录数
- `TodayNewConversations``xcx_conversation.CreateTime` 在“今天”内的记录数
- `TotalUsers``xcx_users``UserKey``PhoneNumber` 均不为空的用户总数
#### Scenario: 首页加载统计卡片
- **WHEN** 管理员进入首页Home
- **THEN** 前端调用 `GET /api/Admin/QueryStats`
- **AND** 将返回的四个数字分别展示到“系统统计卡片”的四个位置
## Known Limitations ## Known Limitations
- 当前接口未提供分页与导出能力(若管理端数据量很大,需在后续能力中补齐)。 - 当前接口未提供分页与导出能力(若管理端数据量很大,需在后续能力中补齐)。
- 当前未强制鉴权JWT 配置存在但未启用认证中间件/授权标注)。 - 当前未强制鉴权JWT 配置存在但未启用认证中间件/授权标注)。

View File

@@ -22,6 +22,7 @@
行为: 行为:
-`xcx_users` 不存在该 `UserKey`,系统 SHALL 自动插入一条用户记录(资料可为空)。 -`xcx_users` 不存在该 `UserKey`,系统 SHALL 自动插入一条用户记录(资料可为空)。
- 若用户 `IsDisabled = 1`,系统 SHALL 返回 `success: false` - 若用户 `IsDisabled = 1`,系统 SHALL 返回 `success: false`
-`xcx_users` 中已经存在该 `UserKey`,系统 SHALL 在登录流程中**更新该记录的 `UpdateTime` 为当前服务器时间**(用于“最近登录/活跃”判断)。
#### Scenario: 首次登录自动建档 #### Scenario: 首次登录自动建档
- **WHEN** 提交的 `code` 能换取有效 `openid` - **WHEN** 提交的 `code` 能换取有效 `openid`
@@ -196,6 +197,23 @@
- **WHEN** Stream 中无可读消息 - **WHEN** Stream 中无可读消息
- **THEN** 返回 `success: true` 且数据为空 - **THEN** 返回 `success: true` 且数据为空
### Requirement: 管理端统计接口QueryStats
系统 SHALL 提供聚合统计接口 `GET /api/Admin/QueryStats`,返回单行 JSON`data` 对象字段如下:
- `ActiveUsers` (number):最近 7 天内 `UpdateTime` >= DATE_SUB(NOW(), INTERVAL 7 DAY) 且 `UserKey``PhoneNumber` 都不为空的用户数
- `TotalConversations` (number)`xcx_conversation` 的总记录数
- `TodayNewConversations` (number)`xcx_conversation``CreateTime >= CURDATE() AND CreateTime < DATE_ADD(CURDATE(), INTERVAL 1 DAY)` 的记录数
- `TotalUsers` (number)`xcx_users``UserKey``PhoneNumber` 都不为空的用户总数
响应(成功):
- `success: true`
- `data`: { `ActiveUsers`, `TotalConversations`, `TodayNewConversations`, `TotalUsers` }
行为/安全:
- 当前实现为匿名访问,建议在后续迭代中加上管理员鉴权(例如 `[Authorize(Roles = "Admin")]` 或策略)。
- 为保证查询性能,建议为 `xcx_users.UpdateTime``xcx_conversation.CreateTime` 建立适当索引(若数据量很大)。
## Known Limitations ## Known Limitations
- 当前 JWT 配置存在,但认证中间件与授权标注未启用;接口默认可匿名访问。 - 当前 JWT 配置存在,但认证中间件与授权标注未启用;接口默认可匿名访问。
- `MessageType` 的过滤条件存在实现差异:部分接口仅在 `MessageType == 1` 时追加过滤(不覆盖 2 - `MessageType` 的过滤条件存在实现差异:部分接口仅在 `MessageType == 1` 时追加过滤(不覆盖 2