新增:管理后台前端页面,以及openspec内容。
This commit is contained in:
377
openspec/specs/backend-admin/spec.md
Normal file
377
openspec/specs/backend-admin/spec.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# 后台管理(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** 菜单包含:首页、会话记录管理、用户管理等功能入口
|
||||
|
||||
### 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 提供接口返回可用用户列表,用于管理侧选择/筛选。
|
||||
|
||||
接口:`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** 结果按首次登录时间倒序排列
|
||||
|
||||
## 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()`
|
||||
202
openspec/specs/backend-api/spec.md
Normal file
202
openspec/specs/backend-api/spec.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# 后端 API(WxCheckMvc)规范
|
||||
|
||||
## Purpose
|
||||
本能力描述 `WxCheckMvc` 暴露给微信小程序与管理侧的 HTTP API:登录/注册、会话记录的增删改查、文件上传、地址补全,以及 Redis Stream 的消息读取。
|
||||
|
||||
路由约定:统一为 `/api/{Controller}/{Action}`。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 小程序登录(基于微信 code)
|
||||
系统 SHALL 提供登录接口,用 `wx.login()` 获取的 `code` 向微信换取 `openid`,并以 `openid` 作为系统内部 `UserKey`。
|
||||
|
||||
接口:`POST /api/Login/Login`
|
||||
|
||||
请求:
|
||||
- `Code: string`
|
||||
|
||||
响应(成功):
|
||||
- `success: true`
|
||||
- `data`: 用户对象(包含 `UserKey/openid` 与 `Token`)
|
||||
|
||||
行为:
|
||||
- 若 `xcx_users` 不存在该 `UserKey`,系统 SHALL 自动插入一条用户记录(资料可为空)。
|
||||
- 若用户 `IsDisabled = 1`,系统 SHALL 返回 `success: false`。
|
||||
|
||||
#### Scenario: 首次登录自动建档
|
||||
- **WHEN** 提交的 `code` 能换取有效 `openid`
|
||||
- **AND** 数据库中不存在该 `openid`
|
||||
- **THEN** 系统创建用户记录并返回 `Token`
|
||||
|
||||
#### Scenario: 禁用用户登录
|
||||
- **WHEN** 用户 `IsDisabled = 1`
|
||||
- **THEN** 系统返回 `success: false` 且提示用户已禁用
|
||||
|
||||
### Requirement: 小程序注册/完善资料
|
||||
系统 SHALL 提供接口用于完善用户资料(用户名、微信名、手机号、头像链接)。
|
||||
|
||||
接口:`POST /api/Login/Register`
|
||||
|
||||
请求:
|
||||
- `UserKey: string`(openid)
|
||||
- `UserName: string`
|
||||
- `WeChatName: string`
|
||||
- `PhoneNumber: string`
|
||||
- `AvatarUrl: string`
|
||||
|
||||
校验:
|
||||
- `UserKey` 必填
|
||||
- `PhoneNumber` SHALL 清洗为纯数字后满足 `^1\d{10}$`
|
||||
- `UserName` SHALL 去除标点/符号/空白后仍非空
|
||||
|
||||
响应(成功):
|
||||
- `success: true`
|
||||
- `data`: 更新后的用户对象(包含 `Token`)
|
||||
|
||||
#### Scenario: 正常完善资料
|
||||
- **WHEN** 提交合法的用户名与手机号
|
||||
- **AND** `UserKey` 对应用户存在
|
||||
- **THEN** 系统更新用户资料并返回新 `Token`
|
||||
|
||||
#### Scenario: 用户不存在
|
||||
- **WHEN** `UserKey` 对应用户不存在
|
||||
- **THEN** 系统返回 404
|
||||
|
||||
### Requirement: 上传文件并可更新头像
|
||||
系统 SHALL 提供文件上传接口,保存到后端 `wwwroot/{rootPathType}` 下,并返回可访问 URL。
|
||||
|
||||
接口:`POST /api/Check/UploadFile`(`multipart/form-data`)
|
||||
|
||||
表单字段:
|
||||
- `file`: 上传文件
|
||||
- `rootPathType: string`(可选;默认 `Avatar`,仅允许字母/数字/下划线)
|
||||
- `userKey: string`(可选;若提供则更新 `xcx_users.AvatarUrl`)
|
||||
|
||||
响应:
|
||||
- `success: true/false`
|
||||
- `url`: 公开访问 URL
|
||||
- `path`: 相对路径
|
||||
|
||||
#### Scenario: 上传头像并更新用户表
|
||||
- **WHEN** 上传文件并提供 `userKey`
|
||||
- **THEN** 系统保存文件并更新该用户 `AvatarUrl`
|
||||
|
||||
### Requirement: 新增会话记录
|
||||
系统 SHALL 提供接口新增会话记录(软实时写入数据库),并尝试将消息投递到 Redis Stream。
|
||||
|
||||
接口:`POST /api/Check/AddConversation`
|
||||
|
||||
请求(核心字段):
|
||||
- `UserKey: string`
|
||||
- `ConversationContent: string`
|
||||
- `SendMethod: string`
|
||||
- `UserLocation: string`(当前实现中会尝试解析 `lat,lng`)
|
||||
- `MessageType: int`(默认 1)
|
||||
- `Guid?: string`(可选;缺省则服务端生成)
|
||||
- `SpeakingTime?: int`
|
||||
|
||||
行为:
|
||||
- 系统 SHALL 写入 `xcx_conversation`,并生成/使用 `Guid`。
|
||||
- 系统 MAY 将会话与用户信息写入 Redis Stream(key: `xcx_msg`,group: `xcx_group`)。
|
||||
|
||||
#### Scenario: 新增会话并返回 guid
|
||||
- **WHEN** 提交包含 `UserKey` 与 `ConversationContent` 的请求
|
||||
- **THEN** 系统创建会话记录并返回 `conversationGuid`
|
||||
|
||||
### Requirement: 查询会话记录(按用户)
|
||||
系统 SHALL 提供按 `UserKey` 查询会话记录的接口,默认只返回未删除记录。
|
||||
|
||||
接口:`POST /api/Check/GetConversations`
|
||||
|
||||
请求:
|
||||
- `UserKey: string`
|
||||
- `MessageType: int`(0 不过滤;1 公有;2 私有;当前实现仅在 `MessageType == 1` 时追加过滤)
|
||||
|
||||
#### Scenario: 查询用户所有未删除会话
|
||||
- **WHEN** 提交 `UserKey`
|
||||
- **THEN** 系统返回该用户 `IsDeleted = 0` 的会话记录
|
||||
|
||||
### Requirement: 分页查询会话记录
|
||||
系统 SHALL 提供分页接口。
|
||||
|
||||
接口:`POST /api/Check/GetConversationsByPage`
|
||||
|
||||
请求:
|
||||
- `UserKey: string`
|
||||
- `Page: int`(<1 视为 1)
|
||||
- `PageSize: int`(1..100,否则默认 10)
|
||||
- `MessageType: int`(当前实现仅在 `MessageType == 1` 时追加过滤)
|
||||
|
||||
响应:
|
||||
- `data.conversations`
|
||||
- `data.totalCount / page / pageSize / totalPages`
|
||||
|
||||
#### Scenario: PageSize 超限
|
||||
- **WHEN** `PageSize > 100`
|
||||
- **THEN** 系统按默认值 10 处理
|
||||
|
||||
### Requirement: 更新会话记录
|
||||
系统 SHALL 提供接口按 `Guid + UserKey` 更新会话内容与发送方式。
|
||||
|
||||
接口:`POST /api/Check/UpdateConversation`
|
||||
|
||||
请求:
|
||||
- `Guid: string`
|
||||
- `UserKey: string`
|
||||
- `ConversationContent: string`
|
||||
- `SendMethod: string`
|
||||
- `MessageType: int`
|
||||
|
||||
#### Scenario: 非本人更新
|
||||
- **WHEN** `Guid` 存在但 `UserKey` 不匹配
|
||||
- **THEN** 系统返回 404(记录不存在或无权限修改)
|
||||
|
||||
### Requirement: 删除会话记录(软删除)
|
||||
系统 SHALL 提供接口按 `Guid + UserKey` 软删除会话。
|
||||
|
||||
接口:`POST /api/Check/DeleteConversation`
|
||||
|
||||
请求:
|
||||
- `Guid: string`
|
||||
- `UserKey: string`
|
||||
|
||||
#### Scenario: 删除已删除记录
|
||||
- **WHEN** 记录已被删除
|
||||
- **THEN** 系统返回 404
|
||||
|
||||
### Requirement: 按 Guid 查询会话(不受软删除影响)
|
||||
系统 SHALL 提供接口按 `Guid` 查询单条会话记录,不过滤 `IsDeleted`。
|
||||
|
||||
接口:`POST /api/Check/GetConversationByGuid`
|
||||
|
||||
#### Scenario: 查询已删除会话
|
||||
- **WHEN** `Guid` 对应记录存在但 `IsDeleted = 1`
|
||||
- **THEN** 系统仍返回该记录
|
||||
|
||||
### Requirement: 地址补全(经纬度→地址)
|
||||
系统 SHALL 提供接口按 `Guid` 查找会话记录经纬度,并使用高德逆地理编码生成地址写回 `UserLocation`。
|
||||
|
||||
接口:`POST /api/Check/CheckAddress`
|
||||
|
||||
#### Scenario: 记录不存在
|
||||
- **WHEN** `Guid` 对应记录不存在或已删除
|
||||
- **THEN** 系统返回 404
|
||||
|
||||
### Requirement: Redis Stream 读取消息
|
||||
系统 SHALL 提供接口从 Redis Stream 读取消息。
|
||||
|
||||
接口:`POST /api/Check/ReadMessageFromRedis`
|
||||
|
||||
请求:
|
||||
- `GroupName?: string`(默认 `xcx_group`)
|
||||
- `ConsumerName?: string`(默认 `consumer_{ticks}`)
|
||||
- `Count?: int`(默认 1)
|
||||
|
||||
#### Scenario: 无新消息
|
||||
- **WHEN** Stream 中无可读消息
|
||||
- **THEN** 返回 `success: true` 且数据为空
|
||||
|
||||
## Known Limitations
|
||||
- 当前 JWT 配置存在,但认证中间件与授权标注未启用;接口默认可匿名访问。
|
||||
- `MessageType` 的过滤条件存在实现差异:部分接口仅在 `MessageType == 1` 时追加过滤(不覆盖 2)。
|
||||
- 文件上传未实现内容类型/大小的强制限制。
|
||||
69
openspec/specs/database/spec.md
Normal file
69
openspec/specs/database/spec.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 数据库(wx_xcx_check)规范
|
||||
|
||||
## Purpose
|
||||
本能力描述 MariaDB/MySQL schema `wx_xcx_check` 的表结构与核心业务语义,用于约束后端 API 的持久化行为。
|
||||
|
||||
参考文件:
|
||||
- 根目录 `wx_xcx_check.sql`
|
||||
- `WxCheckMvc/wx_xcx_check.sql`
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 用户表 xcx_users
|
||||
系统 SHALL 存储小程序用户档案,主业务键为 `UserKey`(微信 `openid`)。
|
||||
|
||||
字段语义(核心):
|
||||
- `UserKey`:唯一键(UNIQUE)
|
||||
- `UserName`:用户姓名(可空,注册/完善资料后写入)
|
||||
- `WeChatName`:微信昵称(可空)
|
||||
- `PhoneNumber`:手机号(可空,注册/完善资料后写入)
|
||||
- `AvatarUrl`:头像 URL(可空,上传后写入)
|
||||
- `Department`:部门(可空,用于管理端筛选)
|
||||
- `IsDisabled`:0 启用 / 1 禁用
|
||||
- `FirstLoginTime`:首次登录时间
|
||||
- `CreateTime`/`UpdateTime`
|
||||
|
||||
#### Scenario: 首次登录建档
|
||||
- **WHEN** 后端检测到 `UserKey` 不存在
|
||||
- **THEN** 插入一条用户记录,至少包含 `UserKey/FirstLoginTime/IsDisabled`
|
||||
|
||||
### Requirement: 会话表 xcx_conversation
|
||||
系统 SHALL 存储会话记录,并支持软删除。
|
||||
|
||||
字段语义(核心):
|
||||
- `UserKey`:关联用户(逻辑关联;当前 schema 未声明外键)
|
||||
- `ConversationContent`:会话内容
|
||||
- `SendMethod`:发送方式(文本/语音等,由业务侧约定字符串)
|
||||
- `MessageType`:1 公有 / 2 私有
|
||||
- `Guid`:会话唯一标识(业务约定;schema 未声明 UNIQUE)
|
||||
- `Latitude/Longitude`:经纬度(字符串存储)
|
||||
- `UserLocation`:地址文本(可由高德逆地理编码写入)
|
||||
- `RecordTime`:记录时间
|
||||
- `RecordTimeUTCStamp`:毫秒级 UTC 时间戳(用于排序/分页)
|
||||
- `SpeakingTime`:对话时长(可空)
|
||||
- `IsDeleted`:0 正常 / 1 删除
|
||||
|
||||
索引:
|
||||
- `idx_userkey`、`idx_utcstamp`、`idx_deleted`、`idx_messagetype`、`idx_guid`
|
||||
|
||||
#### Scenario: 软删除
|
||||
- **WHEN** 用户删除会话
|
||||
- **THEN** 将 `IsDeleted` 置为 1
|
||||
- **AND** 默认查询不返回 `IsDeleted = 1` 的记录
|
||||
|
||||
### Requirement: 操作日志表 xcx_log
|
||||
系统 MAY 记录操作日志,用于审计与问题排查。
|
||||
|
||||
字段语义(核心):
|
||||
- `UserKey`:操作者
|
||||
- `OperationContent`:操作内容
|
||||
- `Impact`:影响描述
|
||||
- `RecordTime`:记录时间
|
||||
|
||||
#### Scenario: 记录操作日志
|
||||
- **WHEN** 发生需要审计的关键操作
|
||||
- **THEN** 系统写入 `xcx_log`
|
||||
|
||||
## Known Limitations
|
||||
- `xcx_conversation.UserKey` 与 `xcx_users.UserKey` 当前为“逻辑关联”,未声明外键;数据一致性需由应用层保障。
|
||||
- `Guid` 未声明唯一约束;若业务要求全局唯一,应在后续变更中补齐约束与迁移。
|
||||
106
openspec/specs/wechat-miniapp/spec.md
Normal file
106
openspec/specs/wechat-miniapp/spec.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 微信小程序(CommunicationRecords)规范
|
||||
|
||||
## Purpose
|
||||
本能力描述小程序端的主要用户流程、页面职责以及与后端 API 的交互约定。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 协议勾选门槛
|
||||
系统 SHALL 要求用户在登录前勾选用户协议(隐私合规前置)。
|
||||
|
||||
实现位置:`pages/logs/logs.js`
|
||||
|
||||
#### Scenario: 未勾选协议尝试登录
|
||||
- **WHEN** 用户未勾选协议并触发登录流程
|
||||
- **THEN** 小程序提示“请先勾选用户协议”
|
||||
|
||||
### Requirement: 登录流程(微信 code → 后端 Login)
|
||||
系统 SHALL 使用 `wx.login()` 获取 `code` 并调用后端登录接口。
|
||||
|
||||
接口:`POST /api/Login/Login`
|
||||
|
||||
行为:
|
||||
- 登录成功时,SHALL 将 `openid(UserKey)` 写入本地缓存 `openid`,并同步到 `app.globalData`。
|
||||
- 若用户资料未完善(缺少 `UserName/WeChatName/PhoneNumber`),SHALL 进入注册/完善资料流程。
|
||||
|
||||
#### Scenario: 已注册用户直接进入聊天页
|
||||
- **WHEN** 登录接口返回完整用户信息
|
||||
- **THEN** 小程序跳转到 `pages/chat/chat`
|
||||
|
||||
#### Scenario: 未注册用户提示完善信息
|
||||
- **WHEN** 登录接口成功但返回的用户信息不完整
|
||||
- **THEN** 小程序提示需要完善资料并展示注册表单
|
||||
|
||||
### Requirement: 注册/完善资料流程
|
||||
系统 SHALL 提供表单采集姓名、手机号、昵称与头像,并调用后端注册接口更新用户资料。
|
||||
|
||||
接口:`POST /api/Login/Register`
|
||||
|
||||
实现位置:`pages/logs/logs.js`
|
||||
|
||||
行为:
|
||||
- 注册接口成功后,SHALL 触发头像上传(`/api/Check/UploadFile`)并更新页面头像。
|
||||
|
||||
#### Scenario: 正常注册
|
||||
- **WHEN** 用户提交有效姓名与手机号
|
||||
- **THEN** 小程序完成注册并进入聊天页
|
||||
|
||||
### Requirement: 头像上传
|
||||
系统 SHALL 允许用户选择头像并上传到服务器,后端返回永久 URL 后用于展示。
|
||||
|
||||
接口:`POST /api/Check/UploadFile`
|
||||
|
||||
实现位置:`pages/logs/logs.js` 与 `pages/chat/chat.js`
|
||||
|
||||
#### Scenario: 上传成功
|
||||
- **WHEN** 上传接口返回 `success: true`
|
||||
- **THEN** 小程序使用返回的 `url` 更新头像展示
|
||||
|
||||
### Requirement: 聊天页加载历史会话
|
||||
系统 SHALL 在进入聊天页时请求历史会话并渲染。
|
||||
|
||||
接口:`POST /api/Check/GetConversationsByPage`
|
||||
|
||||
实现位置:`pages/chat/chat.js`(`loadHistory` / `GetConversations`)
|
||||
|
||||
#### Scenario: 首次进入加载第一页
|
||||
- **WHEN** 进入聊天页
|
||||
- **THEN** 请求第一页(Page=1, PageSize=20)并展示
|
||||
|
||||
### Requirement: 发送消息
|
||||
系统 SHALL 通过后端新增会话接口发送消息。
|
||||
|
||||
接口:`POST /api/Check/AddConversation`
|
||||
|
||||
实现位置:`pages/chat/chat.js`
|
||||
|
||||
#### Scenario: 文本发送成功
|
||||
- **WHEN** 用户发送文本消息
|
||||
- **THEN** 小程序调用新增会话接口并更新列表
|
||||
|
||||
### Requirement: 编辑与删除消息
|
||||
系统 SHALL 支持对会话进行更新与软删除。
|
||||
|
||||
接口:
|
||||
- `POST /api/Check/UpdateConversation`
|
||||
- `POST /api/Check/DeleteConversation`
|
||||
|
||||
实现位置:`pages/chat/chat.js`
|
||||
|
||||
#### Scenario: 删除消息
|
||||
- **WHEN** 用户删除某条消息
|
||||
- **THEN** 小程序调用删除接口并从列表移除/刷新
|
||||
|
||||
## Configuration
|
||||
|
||||
### Requirement: API 根地址
|
||||
系统 SHOULD 通过 `utils/config.js` 统一管理 `baseUrl`,并在请求中使用 `${config.baseUrl}` 拼接接口路径。
|
||||
|
||||
#### Scenario: 环境切换
|
||||
- **WHEN** 小程序环境为 develop/trial/release
|
||||
- **THEN** 选择对应 `baseUrl`
|
||||
|
||||
## Known Limitations
|
||||
- `utils/config.js` 当前强制返回 `release`,实际不会随 `envVersion` 切换。
|
||||
- 存在硬编码域名请求(例如注册/登录/上传头像部分),未统一走 `config.baseUrl`。
|
||||
- `app.js` 中的 `globalData.baseUrl` 为占位配置,实际请求主要依赖 `utils/config.js` 或硬编码。
|
||||
Reference in New Issue
Block a user