1227 lines
54 KiB
Markdown
1227 lines
54 KiB
Markdown
|
|
# 应用管理模块 - 需求与技术设计方案
|
|||
|
|
|
|||
|
|
## 文档信息
|
|||
|
|
|
|||
|
|
| 项目 | 内容 |
|
|||
|
|
|------|------|
|
|||
|
|
| 项目名称 | 盘古用户认证中心 - 应用管理模块 |
|
|||
|
|
| 文档版本 | V1.0 |
|
|||
|
|
| 编写日期 | 2026-02-04 |
|
|||
|
|
| 作者 | pangu |
|
|||
|
|
| 状态 | 已完成 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 第一部分:需求设计方案
|
|||
|
|
|
|||
|
|
## 1. 业务背景
|
|||
|
|
|
|||
|
|
### 1.1 项目背景
|
|||
|
|
|
|||
|
|
盘古用户认证中心作为统一身份认证平台,需要对外提供开放API接口,供第三方应用系统(如AI智慧教育平台、数字图书馆系统、校园OA办公系统、家校通小程序等)调用,以获取学生、教师、学校等基础数据信息。
|
|||
|
|
|
|||
|
|
为保障数据安全和接口调用的可控性,需要建立一套完整的**应用管理机制**,实现:
|
|||
|
|
- 第三方应用的注册与管理
|
|||
|
|
- 应用凭证(AppCode + AppSecret)的生成与维护
|
|||
|
|
- 开放接口的精细化授权控制
|
|||
|
|
- 接口调用的安全鉴权
|
|||
|
|
|
|||
|
|
### 1.2 业务目标
|
|||
|
|
|
|||
|
|
1. **安全可控**:通过应用编码和密钥机制,确保只有授权应用才能调用开放接口
|
|||
|
|
2. **权限精细**:支持按应用维度配置可调用的接口列表,实现最小权限原则
|
|||
|
|
3. **易于扩展**:提供标准化的开放接口规范和示例代码,便于甲方二次开发
|
|||
|
|
4. **运维便捷**:支持应用状态管理、密钥重置、调用日志等运维功能
|
|||
|
|
|
|||
|
|
### 1.3 适用范围
|
|||
|
|
|
|||
|
|
本方案适用于以下场景:
|
|||
|
|
- 第三方业务系统需要获取学生、教师、学校等基础数据
|
|||
|
|
- 移动端小程序需要调用认证中心接口
|
|||
|
|
- 内部子系统之间的数据同步
|
|||
|
|
- 数据中台、大数据平台的数据采集
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 需求分析
|
|||
|
|
|
|||
|
|
### 2.1 用户角色
|
|||
|
|
|
|||
|
|
| 角色 | 描述 | 主要操作 |
|
|||
|
|
|------|------|----------|
|
|||
|
|
| 系统管理员 | 认证中心的管理员 | 管理第三方应用、配置接口授权、查看调用日志 |
|
|||
|
|
| 第三方开发者 | 对接方的技术人员 | 获取应用凭证、按规范调用API、查看接口文档 |
|
|||
|
|
|
|||
|
|
### 2.2 功能需求
|
|||
|
|
|
|||
|
|
#### 2.2.1 应用管理(管理端)
|
|||
|
|
|
|||
|
|
| 功能点 | 描述 | 优先级 |
|
|||
|
|
|--------|------|--------|
|
|||
|
|
| 应用列表 | 展示所有已注册的第三方应用,支持搜索、分页 | P0 |
|
|||
|
|
| 新增应用 | 录入应用基本信息,系统自动生成AppCode和AppSecret | P0 |
|
|||
|
|
| 编辑应用 | 修改应用名称、联系人、状态等信息 | P0 |
|
|||
|
|
| 删除应用 | 删除应用(逻辑删除),删除后凭证失效 | P0 |
|
|||
|
|
| 重置密钥 | 重新生成AppSecret,原密钥立即失效 | P0 |
|
|||
|
|
| 接口授权 | 为应用配置可调用的接口列表 | P0 |
|
|||
|
|
| 查看凭证 | 查看应用的AppCode和AppSecret | P1 |
|
|||
|
|
| 调用统计 | 查看应用的接口调用次数、成功率等 | P2 |
|
|||
|
|
|
|||
|
|
#### 2.2.2 开放接口(对接端)
|
|||
|
|
|
|||
|
|
| 功能点 | 描述 | 优先级 |
|
|||
|
|
|--------|------|--------|
|
|||
|
|
| 接口鉴权 | 验证请求的AppCode、时间戳、签名 | P0 |
|
|||
|
|
| 权限校验 | 检查应用是否有权调用该接口 | P0 |
|
|||
|
|
| 学生列表 | 分页查询学生信息(示例接口) | P0 |
|
|||
|
|
| 接口文档 | 提供接口说明、参数定义、示例代码 | P1 |
|
|||
|
|
|
|||
|
|
#### 2.2.3 接口字典管理
|
|||
|
|
|
|||
|
|
| 功能点 | 描述 | 优先级 |
|
|||
|
|
|--------|------|--------|
|
|||
|
|
| 接口字典 | 维护可授权的开放接口列表 | P0 |
|
|||
|
|
| 新增接口 | 添加新的开放接口定义(二次开发) | P1 |
|
|||
|
|
|
|||
|
|
### 2.3 非功能需求
|
|||
|
|
|
|||
|
|
| 类型 | 需求描述 |
|
|||
|
|
|------|----------|
|
|||
|
|
| 安全性 | 签名有效期5分钟,防重放攻击;密钥使用MD5加密传输 |
|
|||
|
|
| 性能 | 接口鉴权响应时间 < 50ms,不影响业务接口性能 |
|
|||
|
|
| 可用性 | 支持应用状态控制,停用后立即生效 |
|
|||
|
|
| 可扩展性 | 支持甲方二次开发,新增开放接口无需修改鉴权逻辑 |
|
|||
|
|
| 兼容性 | 支持多种调用方式:HTTP GET/POST,支持常见编程语言 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 业务流程
|
|||
|
|
|
|||
|
|
### 3.1 应用注册与授权流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 应用注册与授权流程 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ 收集需求 │───>│ 创建应用 │───>│ 生成凭证 │───>│ 配置授权 │───>│ 交付凭证 │
|
|||
|
|
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|||
|
|
│ │ │ │ │
|
|||
|
|
v v v v v
|
|||
|
|
收集第三方 填写应用名称 自动生成 勾选该应用 将AppCode
|
|||
|
|
系统信息 联系人等 AppCode 可调用的 AppSecret
|
|||
|
|
确认对接需求 基本信息 AppSecret 接口列表 发送给对接方
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 第三方调用接口流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 第三方调用接口流程 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
|||
|
|
│ 构造请求 │───>│ 计算签名 │───>│ 发送请求 │───>│ 服务端校验│───>│ 返回数据 │
|
|||
|
|
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
|
|||
|
|
│ │ │ │ │
|
|||
|
|
v v v v v
|
|||
|
|
准备请求参数 MD5(params 添加请求头 1.验证签名 校验通过
|
|||
|
|
pageNum=1 +appSecret) X-App-Id 2.验证时间戳 返回业务数据
|
|||
|
|
pageSize=10 得到签名值 X-Timestamp 3.验证应用状态 校验失败
|
|||
|
|
X-Sign 4.验证接口权限 返回错误信息
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 签名算法流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 签名计算流程 │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
1. 收集所有请求参数(不含签名本身)
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ pageNum=1, pageSize=10, status=0 │
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
v
|
|||
|
|
2. 按参数名 ASCII 码升序排序
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ pageNum=1 & pageSize=10 & status=0 │
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
v
|
|||
|
|
3. 拼接键值对,末尾追加 appSecret
|
|||
|
|
┌─────────────────────────────────────────────────────────┐
|
|||
|
|
│ pageNum=1&pageSize=10&status=0&appSecret=xxxxxxxxxx │
|
|||
|
|
└─────────────────────────────────────────────────────────┘
|
|||
|
|
│
|
|||
|
|
v
|
|||
|
|
4. 对整个字符串进行 MD5 加密,转大写
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6 │ <-- X-Sign
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 界面原型说明
|
|||
|
|
|
|||
|
|
### 4.1 应用列表页面
|
|||
|
|
|
|||
|
|
**页面路径**:`http://localhost/application`
|
|||
|
|
|
|||
|
|
**页面布局**:
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 搜索区域 │
|
|||
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
|
|||
|
|
│ │ 应用名称 │ │ 应用编码 │ │ 状态 ▼ │ │ 🔍搜索 │ │ ↻重置 │ │
|
|||
|
|
│ └──────────────┘ └──────────────┘ └────────┘ └────────┘ └────────┘ │
|
|||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ ┌─────────┐ │
|
|||
|
|
│ │ + 新增 │ │
|
|||
|
|
│ └─────────┘ │
|
|||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ │ 应用名称 │ 应用编码 │ 联系人 │ 联系电话 │ 状态 │ 创建时间 │ 操作 │
|
|||
|
|
│ ├────────────────┼──────────┼────────┼─────────────┼──────┼────────────┼──────────┤
|
|||
|
|
│ │ AI智慧教育平台 │ YY000001 │ 张经理 │ 138****3001 │ 正常 │ 2026-02-02 │ 编辑 设置 │
|
|||
|
|
│ │ 数字图书馆系统 │ YY000002 │ 李经理 │ 138****3002 │ 正常 │ 2026-02-02 │ 编辑 设置 │
|
|||
|
|
│ │ ... │ ... │ ... │ ... │ ... │ ... │ ... │
|
|||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ 共 6 条 10条/页 < 1 > │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**操作按钮说明**:
|
|||
|
|
| 按钮 | 功能 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 新增 | 打开新增应用弹窗 | 录入应用基本信息 |
|
|||
|
|
| 编辑 | 打开编辑应用弹窗 | 修改应用信息 |
|
|||
|
|
| 设置 | 打开接口授权弹窗 | 配置应用可调用的接口 |
|
|||
|
|
| 重置密钥 | 重新生成密钥 | 需二次确认 |
|
|||
|
|
| 删除 | 删除应用 | 需二次确认 |
|
|||
|
|
|
|||
|
|
### 4.2 新增/编辑应用弹窗
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────┐
|
|||
|
|
│ 新增应用 [×] │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ 应用名称 * ┌────────────────────────────┐ │
|
|||
|
|
│ │ 请输入应用名称 │ │
|
|||
|
|
│ └────────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 应用编码 ┌────────────────────────────┐ │
|
|||
|
|
│ │ 保存后自动生成(不可编辑) │ │
|
|||
|
|
│ └────────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 应用描述 ┌────────────────────────────┐ │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ 请输入应用描述 │ │
|
|||
|
|
│ └────────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 联系人 ┌────────────────────────────┐ │
|
|||
|
|
│ │ 请输入联系人 │ │
|
|||
|
|
│ └────────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 联系电话 ┌────────────────────────────┐ │
|
|||
|
|
│ │ 请输入联系电话 │ │
|
|||
|
|
│ └────────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 状态 ○ 正常 ○ 停用 │
|
|||
|
|
│ │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ [取消] [确定] │
|
|||
|
|
└─────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 接口授权设置弹窗
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────┐
|
|||
|
|
│ 接口授权设置 - AI智慧教育平台 [×] │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ 应用编码:YY000001 │
|
|||
|
|
│ 应用密钥:a1b2c3d4...(点击复制) │
|
|||
|
|
│ │
|
|||
|
|
│ ───────────────────────────────────────── │
|
|||
|
|
│ │
|
|||
|
|
│ 可授权接口: │
|
|||
|
|
│ │
|
|||
|
|
│ ☑ 学生列表查询 /open/api/student/list │
|
|||
|
|
│ ☐ 教师列表查询 /open/api/teacher/list │
|
|||
|
|
│ ☐ 学校信息查询 /open/api/school/info │
|
|||
|
|
│ ☐ 班级列表查询 /open/api/class/list │
|
|||
|
|
│ ... │
|
|||
|
|
│ │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ [取消] [保存] │
|
|||
|
|
└─────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 数据字典
|
|||
|
|
|
|||
|
|
### 5.1 应用状态
|
|||
|
|
|
|||
|
|
| 状态值 | 状态名称 | 说明 |
|
|||
|
|
|--------|----------|------|
|
|||
|
|
| 0 | 正常 | 应用可正常调用接口 |
|
|||
|
|
| 1 | 停用 | 应用已停用,所有接口调用将被拒绝 |
|
|||
|
|
|
|||
|
|
### 5.2 API 状态
|
|||
|
|
|
|||
|
|
| 状态值 | 状态名称 | 说明 |
|
|||
|
|
|--------|----------|------|
|
|||
|
|
| 0 | 正常 | 接口可正常被授权和调用 |
|
|||
|
|
| 1 | 停用 | 接口已停用,即使授权也无法调用 |
|
|||
|
|
|
|||
|
|
### 5.3 请求方法
|
|||
|
|
|
|||
|
|
| 方法 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| GET | 查询类接口 |
|
|||
|
|
| POST | 新增/复杂查询类接口 |
|
|||
|
|
| PUT | 修改类接口 |
|
|||
|
|
| DELETE | 删除类接口 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 第二部分:技术设计方案
|
|||
|
|
|
|||
|
|
## 6. 系统架构
|
|||
|
|
|
|||
|
|
### 6.1 整体架构图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 调用方 │
|
|||
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|||
|
|
│ │ AI教育平台 │ │ 图书馆系统 │ │ OA办公系统 │ │ 家校通小程序 │ │
|
|||
|
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
|||
|
|
└──────────┼─────────────────┼─────────────────┼─────────────────┼───────────┘
|
|||
|
|
│ │ │ │
|
|||
|
|
└─────────────────┴────────┬────────┴─────────────────┘
|
|||
|
|
│
|
|||
|
|
v
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 盘古用户认证中心 │
|
|||
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ API 网关层(可选) │ │
|
|||
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ v │
|
|||
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 开放接口认证拦截器 │ │
|
|||
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|||
|
|
│ │ │ 参数校验 │─>│ 签名验证 │─>│ 应用状态 │─>│ 接口权限 │ │ │
|
|||
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|||
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ v │
|
|||
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 开放接口层 │ │
|
|||
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|||
|
|
│ │ │ 学生接口 │ │ 教师接口 │ │ 学校接口 │ │ 更多接口... │ │ │
|
|||
|
|
│ │ │ /open/api/ │ │ /open/api/ │ │ /open/api/ │ │ (二次开发) │ │ │
|
|||
|
|
│ │ │ student/* │ │ teacher/* │ │ school/* │ │ │ │ │
|
|||
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|||
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ v │
|
|||
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 业务服务层 │ │
|
|||
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|||
|
|
│ │ │ 学生服务 │ │ 教师服务 │ │ 学校服务 │ │ 应用管理 │ │ │
|
|||
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|||
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
│ │ │
|
|||
|
|
│ v │
|
|||
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ 数据持久层 │ │
|
|||
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|||
|
|
│ │ │ pg_student │ │ pg_teacher │ │ pg_school │ │ pg_app* │ │ │
|
|||
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|||
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 技术选型
|
|||
|
|
|
|||
|
|
| 层次 | 技术栈 | 说明 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| 前端框架 | Vue 3 + Element Plus | 管理后台界面 |
|
|||
|
|
| 后端框架 | Spring Boot 3.x | 基于若依Plus框架 |
|
|||
|
|
| ORM框架 | MyBatis Plus | 数据库访问 |
|
|||
|
|
| 安全框架 | Sa-Token | 内部系统鉴权(管理端) |
|
|||
|
|
| 数据库 | MySQL 8.0 | 数据存储 |
|
|||
|
|
| 缓存 | Redis | 可选,用于接口限流、凭证缓存 |
|
|||
|
|
| 加密算法 | MD5 | 接口签名(可升级为HMAC-SHA256) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 数据库设计
|
|||
|
|
|
|||
|
|
### 7.1 ER 关系图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|||
|
|
│ pg_application │ │ pg_app_api │ │ pg_api_dict │
|
|||
|
|
│ (第三方应用表) │ 1───n │ (应用API授权表) │ n───1 │ (API接口字典) │
|
|||
|
|
├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤
|
|||
|
|
│ PK app_id │────────>│ FK app_id │ │ PK api_id │
|
|||
|
|
│ app_code │ │ FK api_id │<────────│ api_code │
|
|||
|
|
│ app_name │ │ create_time │ │ api_name │
|
|||
|
|
│ app_secret │ └─────────────────────┘ │ api_path │
|
|||
|
|
│ contact_person │ │ api_method │
|
|||
|
|
│ contact_phone │ │ api_desc │
|
|||
|
|
│ status │ │ status │
|
|||
|
|
│ ... │ │ order_num │
|
|||
|
|
└─────────────────────┘ └─────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 表结构详细设计
|
|||
|
|
|
|||
|
|
#### 7.2.1 第三方应用表 (pg_application)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `pg_application` (
|
|||
|
|
`app_id` bigint NOT NULL COMMENT '应用ID(雪花算法)',
|
|||
|
|
`app_code` varchar(32) NOT NULL COMMENT '应用编码(格式:YY000001)',
|
|||
|
|
`app_name` varchar(100) NOT NULL COMMENT '应用名称',
|
|||
|
|
`app_secret` varchar(100) NOT NULL COMMENT '应用密钥(32位UUID)',
|
|||
|
|
`contact_person` varchar(50) DEFAULT NULL COMMENT '联系人',
|
|||
|
|
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
|
|||
|
|
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
|||
|
|
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
|
|||
|
|
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
|||
|
|
`create_by` bigint DEFAULT NULL COMMENT '创建者',
|
|||
|
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
|||
|
|
`update_by` bigint DEFAULT NULL COMMENT '更新者',
|
|||
|
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
|||
|
|
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0存在 1删除)',
|
|||
|
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注/描述',
|
|||
|
|
PRIMARY KEY (`app_id`),
|
|||
|
|
UNIQUE KEY `uk_app_code` (`app_code`)
|
|||
|
|
) ENGINE=InnoDB COMMENT='第三方应用表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**字段说明**:
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| app_id | bigint | 是 | 主键,雪花算法生成 |
|
|||
|
|
| app_code | varchar(32) | 是 | 应用编码,格式 YY+6位序号,系统自动生成 |
|
|||
|
|
| app_name | varchar(100) | 是 | 应用名称,用于显示 |
|
|||
|
|
| app_secret | varchar(100) | 是 | 应用密钥,32位UUID,用于签名计算 |
|
|||
|
|
| contact_person | varchar(50) | 否 | 对接联系人 |
|
|||
|
|
| contact_phone | varchar(20) | 否 | 联系电话 |
|
|||
|
|
| status | char(1) | 是 | 0=正常,1=停用 |
|
|||
|
|
| del_flag | char(1) | 是 | 逻辑删除标志,0=存在,1=已删除 |
|
|||
|
|
|
|||
|
|
#### 7.2.2 API接口字典表 (pg_api_dict)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `pg_api_dict` (
|
|||
|
|
`api_id` bigint NOT NULL COMMENT 'API ID',
|
|||
|
|
`api_code` varchar(50) NOT NULL COMMENT 'API编码(唯一标识)',
|
|||
|
|
`api_name` varchar(100) NOT NULL COMMENT 'API名称(显示用)',
|
|||
|
|
`api_path` varchar(200) NOT NULL COMMENT 'API路径(如 /open/api/student/list)',
|
|||
|
|
`api_method` varchar(10) DEFAULT 'GET' COMMENT '请求方法(GET/POST/PUT/DELETE)',
|
|||
|
|
`api_desc` varchar(500) DEFAULT NULL COMMENT 'API描述',
|
|||
|
|
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
|||
|
|
`order_num` int DEFAULT 0 COMMENT '排序号',
|
|||
|
|
PRIMARY KEY (`api_id`),
|
|||
|
|
UNIQUE KEY `uk_api_code` (`api_code`)
|
|||
|
|
) ENGINE=InnoDB COMMENT='API接口字典表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**初始数据**(示例接口):
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO pg_api_dict (api_id, api_code, api_name, api_path, api_method, api_desc, status, order_num) VALUES
|
|||
|
|
(1700000000000000001, 'OPEN_STUDENT_LIST', '学生列表查询', '/open/api/student/list', 'GET', '分页查询学生信息,支持按姓名、学号、班级等条件筛选', '0', 10);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.2.3 应用API授权表 (pg_app_api)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `pg_app_api` (
|
|||
|
|
`id` bigint NOT NULL COMMENT '主键',
|
|||
|
|
`app_id` bigint NOT NULL COMMENT '应用ID',
|
|||
|
|
`api_id` bigint NOT NULL COMMENT 'API ID',
|
|||
|
|
`create_time` datetime DEFAULT NULL COMMENT '授权时间',
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
UNIQUE KEY `uk_app_api` (`app_id`, `api_id`),
|
|||
|
|
KEY `idx_app_id` (`app_id`),
|
|||
|
|
KEY `idx_api_id` (`api_id`)
|
|||
|
|
) ENGINE=InnoDB COMMENT='应用API授权表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.3 菜单数据
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 应用管理菜单(一级菜单)
|
|||
|
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, perms, icon)
|
|||
|
|
VALUES (2300, '应用管理', 0, 4, 'application', 'business/application/index', 'C', 'business:application:list', 'component');
|
|||
|
|
|
|||
|
|
-- 按钮权限
|
|||
|
|
INSERT INTO sys_menu VALUES (2301, '应用查询', 2300, 1, '', '', 'F', 'business:application:query', '#');
|
|||
|
|
INSERT INTO sys_menu VALUES (2302, '应用新增', 2300, 2, '', '', 'F', 'business:application:add', '#');
|
|||
|
|
INSERT INTO sys_menu VALUES (2303, '应用修改', 2300, 3, '', '', 'F', 'business:application:edit', '#');
|
|||
|
|
INSERT INTO sys_menu VALUES (2304, '应用删除', 2300, 4, '', '', 'F', 'business:application:remove', '#');
|
|||
|
|
INSERT INTO sys_menu VALUES (2305, '重置密钥', 2300, 5, '', '', 'F', 'business:application:resetSecret', '#');
|
|||
|
|
INSERT INTO sys_menu VALUES (2306, '接口授权', 2300, 6, '', '', 'F', 'business:application:api', '#');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 接口设计
|
|||
|
|
|
|||
|
|
### 8.1 管理端接口
|
|||
|
|
|
|||
|
|
#### 8.1.1 应用管理接口
|
|||
|
|
|
|||
|
|
| 接口名称 | 方法 | URL | 权限 | 说明 |
|
|||
|
|
|----------|------|-----|------|------|
|
|||
|
|
| 应用列表 | GET | /business/application/list | business:application:list | 分页查询 |
|
|||
|
|
| 应用详情 | GET | /business/application/{appId} | business:application:query | 获取详情 |
|
|||
|
|
| 新增应用 | POST | /business/application | business:application:add | 新增 |
|
|||
|
|
| 修改应用 | PUT | /business/application | business:application:edit | 修改 |
|
|||
|
|
| 删除应用 | DELETE | /business/application/{appIds} | business:application:remove | 删除 |
|
|||
|
|
| 重置密钥 | PUT | /business/application/resetSecret/{appId} | business:application:edit | 重置 |
|
|||
|
|
| 接口列表 | GET | /business/application/apiList | business:application:list | 获取可授权接口 |
|
|||
|
|
|
|||
|
|
#### 8.1.2 接口详细定义
|
|||
|
|
|
|||
|
|
**新增应用**
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
POST /business/application
|
|||
|
|
Content-Type: application/json
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"appName": "AI智慧教育平台",
|
|||
|
|
"contactPerson": "张经理",
|
|||
|
|
"contactPhone": "13800138001",
|
|||
|
|
"status": "0",
|
|||
|
|
"remark": "新华AI智慧教育平台,提供智能题库、AI批改等功能",
|
|||
|
|
"apiCodes": ["OPEN_STUDENT_LIST"]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**响应**:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "操作成功",
|
|||
|
|
"data": {
|
|||
|
|
"appId": 1890000000000000001,
|
|||
|
|
"appCode": "YY000001",
|
|||
|
|
"appSecret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 开放接口
|
|||
|
|
|
|||
|
|
#### 8.2.1 请求头规范
|
|||
|
|
|
|||
|
|
| Header | 必填 | 说明 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| X-App-Id | 是 | 应用编码(AppCode) |
|
|||
|
|
| X-Timestamp | 是 | 当前时间戳(毫秒) |
|
|||
|
|
| X-Sign | 是 | 请求签名(MD5大写) |
|
|||
|
|
|
|||
|
|
#### 8.2.2 签名算法
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 将所有请求参数(不含签名)按参数名 ASCII 码升序排序
|
|||
|
|
2. 按照 key1=value1&key2=value2&... 格式拼接
|
|||
|
|
3. 末尾追加 &appSecret={密钥}
|
|||
|
|
4. 对整个字符串进行 MD5 加密,转大写
|
|||
|
|
|
|||
|
|
示例:
|
|||
|
|
请求参数:pageNum=1, pageSize=10, status=0
|
|||
|
|
密钥:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
|||
|
|
|
|||
|
|
签名字符串:pageNum=1&pageSize=10&status=0&appSecret=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
|||
|
|
签名结果:MD5(签名字符串).toUpperCase()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 8.2.3 学生列表接口(示例)
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
GET /open/api/student/list?pageNum=1&pageSize=10
|
|||
|
|
X-App-Id: YY000001
|
|||
|
|
X-Timestamp: 1738656000000
|
|||
|
|
X-Sign: A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**响应**:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "操作成功",
|
|||
|
|
"rows": [
|
|||
|
|
{
|
|||
|
|
"studentId": 1,
|
|||
|
|
"studentName": "张三",
|
|||
|
|
"studentCode": "S2024001",
|
|||
|
|
"schoolName": "第一小学",
|
|||
|
|
"gradeName": "三年级",
|
|||
|
|
"className": "1班"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"total": 100
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 8.2.4 错误码定义
|
|||
|
|
|
|||
|
|
| 错误码 | 错误信息 | 说明 |
|
|||
|
|
|--------|----------|------|
|
|||
|
|
| 401 | 缺少认证参数 | X-App-Id/X-Timestamp/X-Sign 缺失 |
|
|||
|
|
| 401 | 时间戳格式错误 | X-Timestamp 非数字 |
|
|||
|
|
| 401 | 请求已过期 | 时间戳超过5分钟有效期 |
|
|||
|
|
| 401 | 应用不存在 | AppCode 无效 |
|
|||
|
|
| 401 | 应用已停用 | 应用状态为停用 |
|
|||
|
|
| 401 | 签名验证失败 | 签名计算错误 |
|
|||
|
|
| 403 | 无权访问该接口 | 应用未被授权访问该接口 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 核心代码设计
|
|||
|
|
|
|||
|
|
### 9.1 后端代码结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
pangu-modules/pangu-business/src/main/java/org/dromara/pangu/
|
|||
|
|
├── application/ # 应用管理模块
|
|||
|
|
│ ├── controller/
|
|||
|
|
│ │ └── PgApplicationController.java # 应用管理控制器
|
|||
|
|
│ ├── domain/
|
|||
|
|
│ │ ├── PgApplication.java # 第三方应用实体
|
|||
|
|
│ │ ├── PgApiDict.java # API字典实体
|
|||
|
|
│ │ └── PgAppApi.java # 应用API授权实体
|
|||
|
|
│ ├── mapper/
|
|||
|
|
│ │ ├── PgApplicationMapper.java
|
|||
|
|
│ │ ├── PgApiDictMapper.java
|
|||
|
|
│ │ └── PgAppApiMapper.java
|
|||
|
|
│ └── service/
|
|||
|
|
│ ├── IPgApplicationService.java
|
|||
|
|
│ ├── IPgApiDictService.java
|
|||
|
|
│ └── impl/
|
|||
|
|
│ ├── PgApplicationServiceImpl.java
|
|||
|
|
│ └── PgApiDictServiceImpl.java
|
|||
|
|
│
|
|||
|
|
└── openapi/ # 开放接口模块
|
|||
|
|
├── config/
|
|||
|
|
│ ├── ApiAuthInterceptor.java # 开放API鉴权拦截器
|
|||
|
|
│ └── OpenApiWebMvcConfig.java # 拦截器注册配置
|
|||
|
|
├── controller/
|
|||
|
|
│ └── OpenApiStudentController.java # 学生列表开放接口(示例)
|
|||
|
|
├── service/ # 开放接口业务层
|
|||
|
|
│ ├── IOpenApiStudentService.java # 开放接口学生服务接口
|
|||
|
|
│ └── impl/
|
|||
|
|
│ └── OpenApiStudentServiceImpl.java # 脱敏、数据转换等业务逻辑
|
|||
|
|
├── domain/
|
|||
|
|
│ └── vo/
|
|||
|
|
│ └── OpenApiStudentVo.java # 开放接口专用VO(脱敏后的数据结构)
|
|||
|
|
└── utils/
|
|||
|
|
└── DataMaskUtil.java # 数据脱敏工具类
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.2 核心类设计
|
|||
|
|
|
|||
|
|
#### 9.2.1 鉴权拦截器 (ApiAuthInterceptor.java)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Slf4j
|
|||
|
|
@Component
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class ApiAuthInterceptor implements HandlerInterceptor {
|
|||
|
|
|
|||
|
|
private static final String HEADER_APP_ID = "X-App-Id";
|
|||
|
|
private static final String HEADER_TIMESTAMP = "X-Timestamp";
|
|||
|
|
private static final String HEADER_SIGN = "X-Sign";
|
|||
|
|
private static final long TIMESTAMP_EXPIRE_MS = 5 * 60 * 1000L; // 5分钟
|
|||
|
|
|
|||
|
|
private final IPgApplicationService applicationService;
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
|||
|
|
// 1. 参数校验
|
|||
|
|
String appId = request.getHeader(HEADER_APP_ID);
|
|||
|
|
String timestamp = request.getHeader(HEADER_TIMESTAMP);
|
|||
|
|
String sign = request.getHeader(HEADER_SIGN);
|
|||
|
|
if (StrUtil.isBlank(appId) || StrUtil.isBlank(timestamp) || StrUtil.isBlank(sign)) {
|
|||
|
|
throw new ServiceException("缺少认证参数");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 时间戳校验(防重放攻击)
|
|||
|
|
long reqTime = Long.parseLong(timestamp.trim());
|
|||
|
|
if (Math.abs(System.currentTimeMillis() - reqTime) > TIMESTAMP_EXPIRE_MS) {
|
|||
|
|
throw new ServiceException("请求已过期");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 应用状态校验
|
|||
|
|
PgApplication app = applicationService.selectByAppCode(appId.trim());
|
|||
|
|
if (app == null) {
|
|||
|
|
throw new ServiceException("应用不存在");
|
|||
|
|
}
|
|||
|
|
if ("1".equals(app.getStatus())) {
|
|||
|
|
throw new ServiceException("应用已停用");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 签名校验
|
|||
|
|
String expectedSign = buildSign(request, app.getAppSecret());
|
|||
|
|
if (!expectedSign.equalsIgnoreCase(sign.trim())) {
|
|||
|
|
throw new ServiceException("签名验证失败");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 接口权限校验
|
|||
|
|
String apiPath = request.getRequestURI();
|
|||
|
|
if (!applicationService.checkApiPermission(appId.trim(), apiPath)) {
|
|||
|
|
throw new ServiceException("无权访问该接口");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private String buildSign(HttpServletRequest request, String appSecret) {
|
|||
|
|
// 参数按 key ASCII 升序排序,拼接后追加 appSecret,MD5 大写
|
|||
|
|
Map<String, String> params = new TreeMap<>();
|
|||
|
|
request.getParameterMap().forEach((key, values) -> {
|
|||
|
|
if (values != null && values.length > 0 && StrUtil.isNotBlank(values[0])) {
|
|||
|
|
params.put(key, values[0].trim());
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
StringBuilder sb = new StringBuilder();
|
|||
|
|
params.forEach((k, v) -> sb.append(sb.length() > 0 ? "&" : "").append(k).append("=").append(v));
|
|||
|
|
sb.append("&appSecret=").append(appSecret);
|
|||
|
|
return DigestUtil.md5Hex(sb.toString()).toUpperCase();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 9.2.2 拦截器注册 (OpenApiWebMvcConfig.java)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Configuration
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class OpenApiWebMvcConfig implements WebMvcConfigurer {
|
|||
|
|
|
|||
|
|
private final ApiAuthInterceptor apiAuthInterceptor;
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void addInterceptors(InterceptorRegistry registry) {
|
|||
|
|
registry.addInterceptor(apiAuthInterceptor)
|
|||
|
|
.addPathPatterns("/open/api/**"); // 仅对开放接口生效
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 9.2.3 开放接口示例 (OpenApiStudentController.java)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Validated
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
@RestController
|
|||
|
|
@RequestMapping("/open/api/student")
|
|||
|
|
public class OpenApiStudentController extends BaseController {
|
|||
|
|
|
|||
|
|
private final IOpenApiStudentService openApiStudentService; // 调用开放接口专用服务
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 学生列表分页查询(对外开放接口)
|
|||
|
|
*
|
|||
|
|
* 请求示例:
|
|||
|
|
* GET /open/api/student/list?pageNum=1&pageSize=10&studentName=张
|
|||
|
|
* Headers:
|
|||
|
|
* X-App-Id: YY000001
|
|||
|
|
* X-Timestamp: 1738656000000
|
|||
|
|
* X-Sign: A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6
|
|||
|
|
*/
|
|||
|
|
@GetMapping("/list")
|
|||
|
|
public TableDataInfo<OpenApiStudentVo> list(PgStudent student, PageQuery pageQuery) {
|
|||
|
|
return openApiStudentService.selectPageList(student, pageQuery);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 学生详情查询(对外开放接口)
|
|||
|
|
*/
|
|||
|
|
@GetMapping("/{studentId}")
|
|||
|
|
public R<OpenApiStudentVo> getInfo(@PathVariable Long studentId) {
|
|||
|
|
return R.ok(openApiStudentService.selectById(studentId));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 9.2.4 开放接口业务层 (OpenApiStudentService)
|
|||
|
|
|
|||
|
|
**说明**:开放接口单独创建 Service 层,用于处理脱敏、数据转换、字段过滤等业务逻辑,与内部业务服务解耦。
|
|||
|
|
|
|||
|
|
**服务接口**:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
/**
|
|||
|
|
* 开放接口学生服务接口
|
|||
|
|
*/
|
|||
|
|
public interface IOpenApiStudentService {
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 分页查询学生列表(脱敏版本)
|
|||
|
|
*/
|
|||
|
|
TableDataInfo<OpenApiStudentVo> selectPageList(PgStudent student, PageQuery pageQuery);
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 查询学生详情(脱敏版本)
|
|||
|
|
*/
|
|||
|
|
OpenApiStudentVo selectById(Long studentId);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**服务实现**:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class OpenApiStudentServiceImpl implements IOpenApiStudentService {
|
|||
|
|
|
|||
|
|
private final IPgStudentService studentService; // 复用原有业务服务
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public TableDataInfo<OpenApiStudentVo> selectPageList(PgStudent student, PageQuery pageQuery) {
|
|||
|
|
// 1. 调用原有业务服务获取数据
|
|||
|
|
TableDataInfo<StudentVo> result = studentService.selectPageList(student, pageQuery);
|
|||
|
|
|
|||
|
|
// 2. 转换为开放接口VO并脱敏
|
|||
|
|
List<OpenApiStudentVo> openApiList = result.getRows().stream()
|
|||
|
|
.map(this::convertToOpenApiVo)
|
|||
|
|
.collect(Collectors.toList());
|
|||
|
|
|
|||
|
|
// 3. 返回脱敏后的数据
|
|||
|
|
TableDataInfo<OpenApiStudentVo> openApiResult = new TableDataInfo<>();
|
|||
|
|
openApiResult.setRows(openApiList);
|
|||
|
|
openApiResult.setTotal(result.getTotal());
|
|||
|
|
return openApiResult;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public OpenApiStudentVo selectById(Long studentId) {
|
|||
|
|
StudentVo student = studentService.selectById(studentId);
|
|||
|
|
return convertToOpenApiVo(student);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 内部转换方法:原始VO -> 开放接口VO(脱敏处理)
|
|||
|
|
*/
|
|||
|
|
private OpenApiStudentVo convertToOpenApiVo(StudentVo source) {
|
|||
|
|
if (source == null) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
OpenApiStudentVo vo = new OpenApiStudentVo();
|
|||
|
|
vo.setStudentId(source.getStudentId());
|
|||
|
|
vo.setStudentCode(source.getStudentCode());
|
|||
|
|
|
|||
|
|
// 敏感字段脱敏
|
|||
|
|
vo.setStudentName(DataMaskUtil.maskName(source.getStudentName()));
|
|||
|
|
|
|||
|
|
// 非敏感字段直接复制
|
|||
|
|
vo.setSchoolName(source.getSchoolName());
|
|||
|
|
vo.setGradeName(source.getGradeName());
|
|||
|
|
vo.setClassName(source.getClassName());
|
|||
|
|
|
|||
|
|
// 敏感字段不对外暴露(身份证号、手机号等)
|
|||
|
|
// 不设置 idCard、phone 等字段
|
|||
|
|
|
|||
|
|
return vo;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 9.2.5 开放接口专用 VO (OpenApiStudentVo.java)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
@Schema(description = "开放接口学生信息")
|
|||
|
|
public class OpenApiStudentVo implements Serializable {
|
|||
|
|
|
|||
|
|
@Schema(description = "学生ID")
|
|||
|
|
private Long studentId;
|
|||
|
|
|
|||
|
|
@Schema(description = "学生姓名(脱敏)")
|
|||
|
|
private String studentName;
|
|||
|
|
|
|||
|
|
@Schema(description = "学号")
|
|||
|
|
private String studentCode;
|
|||
|
|
|
|||
|
|
@Schema(description = "学校名称")
|
|||
|
|
private String schoolName;
|
|||
|
|
|
|||
|
|
@Schema(description = "年级名称")
|
|||
|
|
private String gradeName;
|
|||
|
|
|
|||
|
|
@Schema(description = "班级名称")
|
|||
|
|
private String className;
|
|||
|
|
|
|||
|
|
// 注意:敏感字段如身份证号、手机号等不对外暴露
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 9.2.6 数据脱敏工具类 (DataMaskUtil.java)
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
/**
|
|||
|
|
* 数据脱敏工具类
|
|||
|
|
*/
|
|||
|
|
public class DataMaskUtil {
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 姓名脱敏:张三 -> 张*
|
|||
|
|
*/
|
|||
|
|
public static String maskName(String name) {
|
|||
|
|
if (StrUtil.isBlank(name) || name.length() == 1) {
|
|||
|
|
return name;
|
|||
|
|
}
|
|||
|
|
return name.charAt(0) + "*".repeat(name.length() - 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 手机号脱敏:13812345678 -> 138****5678
|
|||
|
|
*/
|
|||
|
|
public static String maskPhone(String phone) {
|
|||
|
|
if (StrUtil.isBlank(phone) || phone.length() != 11) {
|
|||
|
|
return phone;
|
|||
|
|
}
|
|||
|
|
return phone.substring(0, 3) + "****" + phone.substring(7);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 身份证脱敏:110101199001011234 -> 110101********1234
|
|||
|
|
*/
|
|||
|
|
public static String maskIdCard(String idCard) {
|
|||
|
|
if (StrUtil.isBlank(idCard) || idCard.length() < 8) {
|
|||
|
|
return idCard;
|
|||
|
|
}
|
|||
|
|
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.3 前端代码结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
frontend/src/
|
|||
|
|
├── api/pangu/
|
|||
|
|
│ └── application.js # 应用管理API
|
|||
|
|
├── views/application/
|
|||
|
|
│ ├── index.vue # 应用列表页面
|
|||
|
|
│ └── components/
|
|||
|
|
│ ├── AppDialog.vue # 新增/编辑弹窗
|
|||
|
|
│ ├── SecretDialog.vue # 密钥展示弹窗
|
|||
|
|
│ └── AuthDialog.vue # 接口授权弹窗(新增)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. 二次开发指南
|
|||
|
|
|
|||
|
|
### 10.1 新增开放接口步骤
|
|||
|
|
|
|||
|
|
#### 步骤一:定义接口字典
|
|||
|
|
|
|||
|
|
在数据库 `pg_api_dict` 表中新增一条记录:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO pg_api_dict (api_id, api_code, api_name, api_path, api_method, api_desc, status, order_num)
|
|||
|
|
VALUES (1700000000000000002, 'OPEN_TEACHER_LIST', '教师列表查询', '/open/api/teacher/list', 'GET', '分页查询教师信息', '0', 20);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤二:创建开放接口专用 VO
|
|||
|
|
|
|||
|
|
在 `openapi/domain/vo` 目录下新建 VO:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
@Schema(description = "开放接口教师信息")
|
|||
|
|
public class OpenApiTeacherVo implements Serializable {
|
|||
|
|
|
|||
|
|
@Schema(description = "教师ID")
|
|||
|
|
private Long teacherId;
|
|||
|
|
|
|||
|
|
@Schema(description = "教师姓名(脱敏)")
|
|||
|
|
private String teacherName;
|
|||
|
|
|
|||
|
|
@Schema(description = "教师工号")
|
|||
|
|
private String teacherCode;
|
|||
|
|
|
|||
|
|
@Schema(description = "学校名称")
|
|||
|
|
private String schoolName;
|
|||
|
|
|
|||
|
|
// 敏感字段不对外暴露
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤三:创建开放接口服务层
|
|||
|
|
|
|||
|
|
在 `openapi/service` 目录下新建服务接口和实现类:
|
|||
|
|
|
|||
|
|
**服务接口**:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public interface IOpenApiTeacherService {
|
|||
|
|
TableDataInfo<OpenApiTeacherVo> selectPageList(PgTeacher teacher, PageQuery pageQuery);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**服务实现**:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class OpenApiTeacherServiceImpl implements IOpenApiTeacherService {
|
|||
|
|
|
|||
|
|
private final IPgTeacherService teacherService; // 复用原有业务服务
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public TableDataInfo<OpenApiTeacherVo> selectPageList(PgTeacher teacher, PageQuery pageQuery) {
|
|||
|
|
// 1. 调用原有业务服务
|
|||
|
|
TableDataInfo<TeacherVo> result = teacherService.selectPageList(teacher, pageQuery);
|
|||
|
|
|
|||
|
|
// 2. 转换并脱敏
|
|||
|
|
List<OpenApiTeacherVo> openApiList = result.getRows().stream()
|
|||
|
|
.map(this::convertToOpenApiVo)
|
|||
|
|
.collect(Collectors.toList());
|
|||
|
|
|
|||
|
|
// 3. 返回
|
|||
|
|
TableDataInfo<OpenApiTeacherVo> openApiResult = new TableDataInfo<>();
|
|||
|
|
openApiResult.setRows(openApiList);
|
|||
|
|
openApiResult.setTotal(result.getTotal());
|
|||
|
|
return openApiResult;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private OpenApiTeacherVo convertToOpenApiVo(TeacherVo source) {
|
|||
|
|
if (source == null) return null;
|
|||
|
|
|
|||
|
|
OpenApiTeacherVo vo = new OpenApiTeacherVo();
|
|||
|
|
vo.setTeacherId(source.getTeacherId());
|
|||
|
|
vo.setTeacherCode(source.getTeacherCode());
|
|||
|
|
vo.setTeacherName(DataMaskUtil.maskName(source.getTeacherName())); // 脱敏
|
|||
|
|
vo.setSchoolName(source.getSchoolName());
|
|||
|
|
return vo;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤四:创建接口控制器
|
|||
|
|
|
|||
|
|
在 `openapi/controller` 目录下新建控制器:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Validated
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
@RestController
|
|||
|
|
@RequestMapping("/open/api/teacher")
|
|||
|
|
public class OpenApiTeacherController extends BaseController {
|
|||
|
|
|
|||
|
|
private final IOpenApiTeacherService openApiTeacherService; // 调用开放接口专用服务
|
|||
|
|
|
|||
|
|
@GetMapping("/list")
|
|||
|
|
public TableDataInfo<OpenApiTeacherVo> list(PgTeacher teacher, PageQuery pageQuery) {
|
|||
|
|
return openApiTeacherService.selectPageList(teacher, pageQuery);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤五:为应用授权
|
|||
|
|
|
|||
|
|
在应用管理界面,勾选新增的接口授权。
|
|||
|
|
|
|||
|
|
### 10.2 接口开发规范
|
|||
|
|
|
|||
|
|
1. **路径规范**:所有开放接口必须以 `/open/api/` 开头
|
|||
|
|
2. **分层规范**:
|
|||
|
|
- Controller 层仅负责接收请求和返回响应
|
|||
|
|
- Service 层处理业务逻辑(脱敏、转换、字段过滤等)
|
|||
|
|
- 复用原有业务服务,避免重复代码
|
|||
|
|
3. **VO 设计规范**:
|
|||
|
|
- 为开放接口单独创建 VO,避免直接使用内部实体
|
|||
|
|
- VO 中只包含需要对外暴露的字段
|
|||
|
|
- 敏感字段在 Service 层进行脱敏处理
|
|||
|
|
4. **返回格式**:使用统一的 `R<T>` 或 `TableDataInfo<T>` 返回格式
|
|||
|
|
5. **参数校验**:使用 `@Validated` 注解进行参数校验
|
|||
|
|
6. **日志记录**:重要操作需记录日志
|
|||
|
|
7. **异常处理**:业务异常使用 `ServiceException`
|
|||
|
|
|
|||
|
|
### 10.3 调用示例代码
|
|||
|
|
|
|||
|
|
#### Java 调用示例
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public class OpenApiClient {
|
|||
|
|
private static final String BASE_URL = "http://your-domain.com";
|
|||
|
|
private static final String APP_CODE = "YY000001";
|
|||
|
|
private static final String APP_SECRET = "your-app-secret";
|
|||
|
|
|
|||
|
|
public String callApi(String path, Map<String, String> params) throws Exception {
|
|||
|
|
// 1. 添加时间戳
|
|||
|
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
|||
|
|
|
|||
|
|
// 2. 计算签名
|
|||
|
|
TreeMap<String, String> sortedParams = new TreeMap<>(params);
|
|||
|
|
StringBuilder sb = new StringBuilder();
|
|||
|
|
sortedParams.forEach((k, v) -> sb.append(sb.length() > 0 ? "&" : "").append(k).append("=").append(v));
|
|||
|
|
sb.append("&appSecret=").append(APP_SECRET);
|
|||
|
|
String sign = DigestUtils.md5Hex(sb.toString()).toUpperCase();
|
|||
|
|
|
|||
|
|
// 3. 构造请求
|
|||
|
|
HttpRequest request = HttpRequest.get(BASE_URL + path + "?" + buildQueryString(params))
|
|||
|
|
.header("X-App-Id", APP_CODE)
|
|||
|
|
.header("X-Timestamp", timestamp)
|
|||
|
|
.header("X-Sign", sign);
|
|||
|
|
|
|||
|
|
return request.execute().body();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Python 调用示例
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import hashlib
|
|||
|
|
import time
|
|||
|
|
import requests
|
|||
|
|
|
|||
|
|
APP_CODE = "YY000001"
|
|||
|
|
APP_SECRET = "your-app-secret"
|
|||
|
|
BASE_URL = "http://your-domain.com"
|
|||
|
|
|
|||
|
|
def call_api(path, params):
|
|||
|
|
# 1. 添加时间戳
|
|||
|
|
timestamp = str(int(time.time() * 1000))
|
|||
|
|
|
|||
|
|
# 2. 计算签名
|
|||
|
|
sorted_params = sorted(params.items())
|
|||
|
|
sign_str = "&".join([f"{k}={v}" for k, v in sorted_params])
|
|||
|
|
sign_str += f"&appSecret={APP_SECRET}"
|
|||
|
|
sign = hashlib.md5(sign_str.encode()).hexdigest().upper()
|
|||
|
|
|
|||
|
|
# 3. 发送请求
|
|||
|
|
headers = {
|
|||
|
|
"X-App-Id": APP_CODE,
|
|||
|
|
"X-Timestamp": timestamp,
|
|||
|
|
"X-Sign": sign
|
|||
|
|
}
|
|||
|
|
response = requests.get(f"{BASE_URL}{path}", params=params, headers=headers)
|
|||
|
|
return response.json()
|
|||
|
|
|
|||
|
|
# 调用示例
|
|||
|
|
result = call_api("/open/api/student/list", {"pageNum": "1", "pageSize": "10"})
|
|||
|
|
print(result)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. 部署与运维
|
|||
|
|
|
|||
|
|
### 11.1 SQL 脚本执行顺序
|
|||
|
|
|
|||
|
|
1. `pangu_tables.sql` - 创建表结构
|
|||
|
|
2. `pangu_menu.sql` - 插入菜单数据
|
|||
|
|
3. `open_api_dict_data.sql` - 插入API字典初始数据
|
|||
|
|
|
|||
|
|
### 11.2 配置项
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# application.yml
|
|||
|
|
pangu:
|
|||
|
|
openapi:
|
|||
|
|
# 签名有效期(毫秒)
|
|||
|
|
timestamp-expire: 300000
|
|||
|
|
# 是否开启调用日志
|
|||
|
|
log-enabled: true
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.3 监控指标
|
|||
|
|
|
|||
|
|
| 指标 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| 接口调用次数 | 按应用、接口维度统计 |
|
|||
|
|
| 调用成功率 | 成功/总调用 |
|
|||
|
|
| 平均响应时间 | 接口性能监控 |
|
|||
|
|
| 异常次数 | 签名失败、权限不足等 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. 文件清单
|
|||
|
|
|
|||
|
|
### 12.1 后端文件
|
|||
|
|
|
|||
|
|
| 文件 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| `application/controller/PgApplicationController.java` | 应用管理控制器 |
|
|||
|
|
| `application/domain/PgApplication.java` | 第三方应用实体 |
|
|||
|
|
| `application/domain/PgApiDict.java` | API字典实体 |
|
|||
|
|
| `application/domain/PgAppApi.java` | 应用API授权实体 |
|
|||
|
|
| `application/mapper/PgApplicationMapper.java` | 应用Mapper |
|
|||
|
|
| `application/mapper/PgApiDictMapper.java` | API字典Mapper |
|
|||
|
|
| `application/mapper/PgAppApiMapper.java` | 应用API授权Mapper |
|
|||
|
|
| `application/service/IPgApplicationService.java` | 应用服务接口 |
|
|||
|
|
| `application/service/impl/PgApplicationServiceImpl.java` | 应用服务实现 |
|
|||
|
|
| `openapi/config/ApiAuthInterceptor.java` | 开放API鉴权拦截器 |
|
|||
|
|
| `openapi/config/OpenApiWebMvcConfig.java` | 拦截器配置 |
|
|||
|
|
| `openapi/controller/OpenApiStudentController.java` | 学生列表开放接口 |
|
|||
|
|
| `openapi/service/IOpenApiStudentService.java` | 开放接口学生服务接口 |
|
|||
|
|
| `openapi/service/impl/OpenApiStudentServiceImpl.java` | 开放接口学生服务实现(脱敏、转换) |
|
|||
|
|
| `openapi/domain/vo/OpenApiStudentVo.java` | 开放接口专用VO |
|
|||
|
|
| `openapi/utils/DataMaskUtil.java` | 数据脱敏工具类 |
|
|||
|
|
|
|||
|
|
### 12.2 前端文件
|
|||
|
|
|
|||
|
|
| 文件 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| `api/pangu/application.js` | 应用管理API |
|
|||
|
|
| `views/application/index.vue` | 应用列表页面 |
|
|||
|
|
| `views/application/components/AppDialog.vue` | 新增/编辑弹窗 |
|
|||
|
|
| `views/application/components/SecretDialog.vue` | 密钥展示弹窗 |
|
|||
|
|
|
|||
|
|
### 12.3 SQL 文件
|
|||
|
|
|
|||
|
|
| 文件 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| `sql/pangu_tables.sql` | 表结构(pg_application、pg_api_dict、pg_app_api) |
|
|||
|
|
| `sql/pangu_menu.sql` | 菜单数据 |
|
|||
|
|
| `sql/open_api_dict_data.sql` | API字典初始数据 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录
|
|||
|
|
|
|||
|
|
### A. 术语表
|
|||
|
|
|
|||
|
|
| 术语 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| AppCode | 应用编码,唯一标识一个第三方应用 |
|
|||
|
|
| AppSecret | 应用密钥,用于签名计算,需保密 |
|
|||
|
|
| 开放接口 | 对外提供的数据查询接口,需鉴权后访问 |
|
|||
|
|
| 接口授权 | 为应用配置可调用的接口列表 |
|
|||
|
|
|
|||
|
|
### B. 修订历史
|
|||
|
|
|
|||
|
|
| 版本 | 日期 | 修订人 | 说明 |
|
|||
|
|
|------|------|--------|------|
|
|||
|
|
| V1.0 | 2026-02-04 | pangu | 初稿 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*文档结束*
|