# 后台管理（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 提供管理员登录功能，使用前端验证方式。

实现补充（效率优化）：
- 系统 SHOULD 提供“记住密码”机制：当管理员成功登录过一次后，将密码保存到浏览器 `localStorage` 并在下次进入登录页时自动回填，从而无需重复输入。

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

#### Scenario: 记住密码并自动回填
- **WHEN** 管理员成功登录一次
- **THEN** 系统将本次输入的密码写入 `localStorage`
- **AND** 下次进入登录页时自动回填密码输入框

#### 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-menu`（router 模式）
- 抽屉的打开 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-私有）
- `StartTime`、`EndTime`（按 `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** 管理端提交包含 `StartTime` 与 `EndTime` 的查询请求
- **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` 有更新，且 `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
- 当前接口未提供分页与导出能力（若管理端数据量很大，需在后续能力中补齐）。
- 当前未强制鉴权（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.js`（Pinia）
- **样式目录**: `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（认证状态）
```javascript
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（主题状态）
```javascript
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
```csharp
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
```csharp
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
```csharp
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()`