Files
Wx_WxCheck_Prod/openspec/specs/backend-admin/spec.md

15 KiB
Raw Blame History

后台管理Admin API + 前端)规范

Purpose

本能力描述"后台管理侧"的完整实现:包括后端接口和前端管理网站。

  • 后端接口:用于查询用户与会话记录,供管理端页面/外部系统使用,以 AdminController 提供的 API 为主。
  • 前端管理网站:基于 Vue 3.x + Element Plus + Vite 构建的管理员操作界面,用于系统配置、数据管理和监控。

Requirements

Requirement: 前端技术架构

前端管理网站 SHALL 使用以下技术栈开发:

  • 前端框架Vue 3.x使用 Composition API 和 <script setup> 语法)
  • UI组件库Element Plus
  • 路由管理vue-router
  • 状态管理Pinia用于深色/浅色模式状态)
  • HTTP客户端axios
  • 构建工具Vite
  • 开发环境Node.js
  • 后端服务:与微信小程序共用 ASP.NET Core MVC 后端AdminController

Scenario: 前端项目初始化

  • WHEN 创建前端项目
  • THEN 使用 Vite 初始化 Vue 3.x 项目
  • AND 安装 Element Plus、vue-router、Pinia、axios 等依赖
  • AND 配置项目目录结构src/router、src/components、src/store、src/styles

Requirement: 用户认证(前端验证)

前端管理网站 SHALL 提供管理员登录功能,使用前端验证方式。

Scenario: 管理员登录

  • WHEN 管理员访问登录页面
  • THEN 显示登录表单(用户名、密码字段)
  • AND 输入正确的用户名和密码(均为 Admin后能够成功登录
  • AND 登录成功后跳转到首页
  • AND 登录状态保存在 Pinia store 中

Scenario: 登录验证失败

  • WHEN 管理员输入错误的用户名或密码
  • THEN 显示错误提示信息
  • AND 不允许进入系统

Scenario: 未登录访问保护页面

  • WHEN 未登录用户访问受保护页面
  • THEN 自动重定向到登录页面

Requirement: 响应式设计

前端管理网站 SHALL 支持响应式设计,优先适配手机宽度。

Scenario: 手机端访问

  • WHEN 管理员使用手机(宽度 < 768px访问后台管理网站
  • THEN 网站布局能够自动调整,完美适配手机屏幕
  • AND 菜单采用折叠或抽屉式布局
  • AND 表格支持横向滚动

Scenario: 平板端访问

  • WHEN 管理员使用平板768px <= 宽度 < 1024px访问后台管理网站
  • THEN 网站布局能够自动调整,适配平板屏幕
  • AND 菜单采用侧边栏布局

Scenario: 桌面端访问

  • WHEN 管理员使用桌面(宽度 >= 1024px访问后台管理网站
  • THEN 网站布局能够自动调整,适配桌面屏幕
  • AND 菜单采用侧边栏布局

Requirement: 深色/浅色模式切换

前端管理网站 SHALL 支持深色模式和浅色模式的自由切换。

Scenario: 切换到深色模式

  • WHEN 管理员点击深色模式切换按钮
  • THEN 网站切换到深色主题
  • AND 所有组件和页面使用深色配色
  • AND 主题状态保存在 Pinia store 中

Scenario: 切换到浅色模式

  • WHEN 管理员点击浅色模式切换按钮
  • THEN 网站切换到浅色主题
  • AND 所有组件和页面使用浅色配色
  • AND 主题状态保存在 Pinia store 中

Scenario: 主题状态持久化

  • WHEN 管理员刷新页面
  • THEN 网站保持上次选择的主题模式

Requirement: 路由管理

前端管理网站 SHALL 使用 vue-router 管理多页面路由。

Scenario: 路由配置

  • WHEN 配置路由
  • THEN 定义登录页、首页、会话记录管理页、用户管理页等路由
  • AND 设置路由守卫,未登录用户重定向到登录页

Scenario: 菜单导航

  • WHEN 管理员点击菜单项
  • THEN 路由跳转到对应页面
  • AND 菜单高亮显示当前页面

Requirement: 菜单布局

前端管理网站 SHALL 使用合理的菜单布局来控制多页面。

Scenario: 侧边栏菜单

  • WHEN 管理员在桌面或平板端访问
  • THEN 显示侧边栏菜单
  • AND 菜单包含:首页、会话记录管理、用户管理等功能入口

Scenario: 抽屉式菜单

  • WHEN 管理员在手机端访问
  • THEN 显示抽屉式菜单(点击菜单按钮展开)
  • AND 菜单包含:首页、会话记录管理、用户管理等功能入口

实现约束(移动端可靠性):

  • 手机端抽屉菜单 SHOULD 使用 Element Plus el-drawer 自带的显示/隐藏机制
  • 菜单 SHOULD 使用 Element Plus el-menurouter 模式)
  • 抽屉的打开 SHALL 由 Header 菜单按钮通过事件触发
  • 不依赖 class + CSS transform 的方式实现“展开/收起”(避免出现 class 变化但 UI 无响应)

Scenario: 手机端点击按钮打开菜单(事件触发)

  • WHEN 管理员在手机端点击 Header 的菜单按钮
  • THEN 系统通过事件触发打开 el-drawer
  • AND 抽屉内显示 el-menu 导航项

Scenario: 手机端点击菜单项自动收起

  • WHEN 管理员在抽屉中点击任一菜单项
  • THEN 路由跳转到对应页面
  • AND 抽屉自动关闭

Requirement: 模块化设计

前端管理网站 SHALL 采用模块化设计思路。

Scenario: 组件模块化

  • WHEN 开发页面功能
  • THEN 将可复用的UI元素封装为独立组件
  • AND 每个组件职责单一,可复用

Scenario: 页面模块化

  • WHEN 开发页面
  • THEN 每个页面独立管理自己的状态和逻辑
  • AND 通过 Pinia store 共享全局状态

Requirement: 查询会话记录(管理端)

系统 SHALL 提供接口按条件查询会话记录,并返回会话与用户的联合视图。

接口:POST /api/Admin/QueryConversations

过滤规则(均为可选):

  • UserKey:用户唯一标识键
  • MessageType消息类型1-公有2-私有)
  • StartTimeEndTime(按 RecordTimeUTCStamp 过滤datetime参数转换为UTC时间戳
  • Department:用户部门(来自用户表)

默认规则:

  • 仅返回 xcx_conversation.IsDeleted = 0 的记录
  • RecordTimeUTCStamp DESC 排序
  • 通过 LEFT JOIN xcx_users 补全用户字段

返回字段(会话):

  • Id, Guid, UserKey, ConversationContent, SendMethod
  • UserLocation, Latitude, Longitude
  • RecordTime, RecordTimeUTCStamp, IsDeleted, CreateTime
  • MessageType, SpeakingTime

返回字段用户通过LEFT JOIN关联

  • UserName, WeChatName, PhoneNumber, AvatarUrl, Department

Scenario: 管理端按时间范围查询

  • WHEN 管理端提交包含 StartTimeEndTime 的查询请求
  • THEN 系统将datetime参数转换为UTC时间戳
  • AND 返回 RecordTimeUTCStamp 落在区间内的会话记录
  • AND 结果按 RecordTimeUTCStamp 倒序

Scenario: 管理端按部门筛选

  • WHEN 管理端提交包含 Department 的查询请求
  • THEN 系统仅返回用户部门匹配的会话记录

Scenario: 管理端按UserKey筛选

  • WHEN 管理端提交包含 UserKey 的查询请求
  • THEN 系统仅返回指定用户的会话记录

Scenario: 管理端按MessageType筛选

  • WHEN 管理端提交包含 MessageType 的查询请求
  • THEN 系统仅返回指定消息类型的会话记录

Scenario: 管理端查询全部会话记录

  • WHEN 管理端提交不包含任何筛选条件的查询请求
  • THEN 系统返回所有 IsDeleted = 0 的会话记录
  • AND 结果按 RecordTimeUTCStamp 倒序

Scenario: 管理端组合多条件查询

  • WHEN 管理端提交包含多个筛选条件的查询请求
  • THEN 系统返回同时满足所有条件的会话记录

Requirement: 会话记录筛选(时间范围,移动端友好)

管理端会话记录页面 SHALL 提供便于移动端使用的时间范围筛选控件:

  • 使用两个独立日期选择器:开始日期、结束日期
  • 提供快捷范围选择器今天、最近3天、最近7天、最近30天
  • 默认范围为最近7天

Scenario: 默认最近7天

  • WHEN 管理员进入会话记录页面
  • THEN 页面默认选中“最近7天”
  • AND 查询请求携带对应的 StartTime/EndTime

Scenario: 移动端可用性

  • WHEN 管理员在移动端访问会话记录页面
  • THEN 快捷选择器与开始/结束日期选择器纵向排列
  • AND 控件全宽显示,避免横向挤压

Requirement: 查询用户列表(管理端)

系统 SHALL 提供接口返回可用用户列表,用于管理侧选择/筛选。

接口:GET /api/Admin/QueryUsers

过滤规则(内置):

  • PhoneNumber 不为空且不为空字符串
  • UserName 不为空且不为空字符串
  • UserKey 不为空且不为空字符串
  • IsDisabled = 0(未禁用)
  • FirstLoginTime DESC 排序

返回字段:

  • Id, UserName, UserKey, WeChatName, PhoneNumber
  • FirstLoginTime, IsDisabled, CreateTime, UpdateTime
  • AvatarUrl, Department

Scenario: 获取用户列表

  • WHEN 管理端请求用户列表
  • THEN 返回已完善基础信息且未禁用的用户
  • AND 结果按首次登录时间倒序排列

Requirement: 首页统计(管理端)

系统 SHALL 提供聚合统计接口供管理端首页展示关键指标。

接口:GET /api/Admin/QueryStats

返回(成功):

  • success: true
  • data:包含以下字段的对象
    • ActiveUsers:最近 7 天内 UpdateTime 有更新,且 UserKeyPhoneNumber 均不为空的用户数
    • TotalConversationsxcx_conversation 总记录数
    • TodayNewConversationsxcx_conversation.CreateTime 在“今天”内的记录数
    • TotalUsersxcx_usersUserKeyPhoneNumber 均不为空的用户总数

Scenario: 首页加载统计卡片

  • WHEN 管理员进入首页Home
  • THEN 前端调用 GET /api/Admin/QueryStats
  • AND 将返回的四个数字分别展示到“系统统计卡片”的四个位置

Known Limitations

  • 当前接口未提供分页与导出能力(若管理端数据量很大,需在后续能力中补齐)。
  • 当前未强制鉴权JWT 配置存在但未启用认证中间件/授权标注)。
  • 前端验证仅使用固定账号密码Admin/Admin安全性较低后续应考虑接入后端JWT认证。

Implementation Details

后端技术实现

  • 控制器文件: WxCheckMvc/Controllers/AdminController.cs
  • 数据库连接: 使用 MySqlConnection 进行数据库操作
  • 路由模式: [Route("api/[controller]/[action]")]
  • API特性: [ApiController] 提供自动模型验证和绑定

前端技术实现

  • 项目目录: admin-web/
  • 构建工具: Vite
  • 路由配置: src/router/index.js
  • 状态管理: src/store/index.jsPinia
  • 样式目录: src/styles/
  • 组件目录: src/components/
  • 页面目录: src/views/

项目结构

admin-web/
├── src/
│   ├── assets/          # 静态资源
│   ├── components/       # 可复用组件
│   │   ├── Layout/       # 布局组件(侧边栏、顶部栏)
│   │   ├── ThemeSwitcher.vue  # 主题切换组件
│   │   └── ...
│   ├── router/           # 路由配置
│   │   └── index.js
│   ├── store/            # Pinia store
│   │   ├── index.js
│   │   ├── auth.js       # 认证状态
│   │   └── theme.js      # 主题状态
│   ├── styles/           # 全局样式
│   │   ├── variables.scss  # CSS变量深色/浅色主题)
│   │   ├── responsive.scss # 响应式样式
│   │   └── main.scss
│   ├── utils/            # 工具函数
│   │   └── request.js    # axios封装
│   ├── views/            # 页面组件
│   │   ├── Login.vue     # 登录页
│   │   ├── Home.vue      # 首页
│   │   ├── ConversationList.vue  # 会话记录管理页
│   │   └── UserList.vue  # 用户管理页
│   ├── App.vue
│   └── main.js
├── index.html
├── package.json
└── vite.config.js

Pinia Store 结构

auth.js认证状态
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    isLoggedIn: false,
    username: ''
  }),
  actions: {
    login(username, password) {
      if (username === 'Admin' && password === 'Admin') {
        this.isLoggedIn = true
        this.username = username
        return true
      }
      return false
    },
    logout() {
      this.isLoggedIn = false
      this.username = ''
    }
  }
})
theme.js主题状态
import { defineStore } from 'pinia'

export const useThemeStore = defineStore('theme', {
  state: () => ({
    isDark: localStorage.getItem('theme') === 'dark'
  }),
  actions: {
    toggleTheme() {
      this.isDark = !this.isDark
      localStorage.setItem('theme', this.isDark ? 'dark' : 'light')
      document.documentElement.setAttribute('data-theme', this.isDark ? 'dark' : 'light')
    },
    initTheme() {
      const theme = localStorage.getItem('theme') || 'light'
      this.isDark = theme === 'dark'
      document.documentElement.setAttribute('data-theme', theme)
    }
  }
})

响应式断点

  • 手机端: < 768px
  • 平板端: 768px - 1023px
  • 桌面端: >= 1024px

数据模型

ConversationQueryRequest

public class ConversationQueryRequest
{
    public DateTime? StartTime { get; set; }
    public DateTime? EndTime { get; set; }
    public string UserKey { get; set; }
    public int? MessageType { get; set; }
    public string Department { get; set; }
}

ConversationQueryResponse

public class ConversationQueryResponse
{
    public long Id { get; set; }
    public string Guid { get; set; }
    public string UserKey { get; set; }
    public string ConversationContent { get; set; }
    public string SendMethod { get; set; }
    public string UserLocation { get; set; }
    public string Latitude { get; set; }
    public string Longitude { get; set; }
    public DateTime RecordTime { get; set; }
    public long RecordTimeUTCStamp { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreateTime { get; set; }
    public int MessageType { get; set; }
    public int? SpeakingTime { get; set; }
    public string UserName { get; set; }
    public string WeChatName { get; set; }
    public string PhoneNumber { get; set; }
    public string AvatarUrl { get; set; }
    public string Department { get; set; }
}

UserQueryResponse

public class UserQueryResponse
{
    public long Id { get; set; }
    public string UserName { get; set; }
    public string UserKey { get; set; }
    public string WeChatName { get; set; }
    public string PhoneNumber { get; set; }
    public DateTime FirstLoginTime { get; set; }
    public bool IsDisabled { get; set; }
    public DateTime CreateTime { get; set; }
    public DateTime UpdateTime { get; set; }
    public string AvatarUrl { get; set; }
    public string Department { get; set; }
}

异常处理

  • 所有方法使用 try-catch-finally 模式
  • 异常时返回 HTTP 500 状态码
  • 响应格式:{ success: false, message: "错误描述", error: "异常详情" }
  • finally 块确保数据库连接正确关闭

数据库操作

  • 使用参数化查询防止 SQL 注入
  • 动态构建 SQL 查询条件,仅添加非空参数
  • 使用 LEFT JOIN 关联 xcx_users 表获取用户信息
  • 时间转换:DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()