54 KiB
54 KiB
应用管理模块 - 需求与技术设计方案
文档信息
| 项目 | 内容 |
|---|---|
| 项目名称 | 盘古用户认证中心 - 应用管理模块 |
| 文档版本 | V1.0 |
| 编写日期 | 2026-02-04 |
| 作者 | pangu |
| 状态 | 已完成 |
第一部分:需求设计方案
1. 业务背景
1.1 项目背景
盘古用户认证中心作为统一身份认证平台,需要对外提供开放API接口,供第三方应用系统(如AI智慧教育平台、数字图书馆系统、校园OA办公系统、家校通小程序等)调用,以获取学生、教师、学校等基础数据信息。
为保障数据安全和接口调用的可控性,需要建立一套完整的应用管理机制,实现:
- 第三方应用的注册与管理
- 应用凭证(AppCode + AppSecret)的生成与维护
- 开放接口的精细化授权控制
- 接口调用的安全鉴权
1.2 业务目标
- 安全可控:通过应用编码和密钥机制,确保只有授权应用才能调用开放接口
- 权限精细:支持按应用维度配置可调用的接口列表,实现最小权限原则
- 易于扩展:提供标准化的开放接口规范和示例代码,便于甲方二次开发
- 运维便捷:支持应用状态管理、密钥重置、调用日志等运维功能
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)
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)
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接口字典表';
初始数据(示例接口):
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)
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 菜单数据
-- 应用管理菜单(一级菜单)
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 接口详细定义
新增应用
POST /business/application
Content-Type: application/json
{
"appName": "AI智慧教育平台",
"contactPerson": "张经理",
"contactPhone": "13800138001",
"status": "0",
"remark": "新华AI智慧教育平台,提供智能题库、AI批改等功能",
"apiCodes": ["OPEN_STUDENT_LIST"]
}
响应:
{
"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 学生列表接口(示例)
GET /open/api/student/list?pageNum=1&pageSize=10
X-App-Id: YY000001
X-Timestamp: 1738656000000
X-Sign: A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6
响应:
{
"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)
@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)
@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)
@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 层,用于处理脱敏、数据转换、字段过滤等业务逻辑,与内部业务服务解耦。
服务接口:
/**
* 开放接口学生服务接口
*/
public interface IOpenApiStudentService {
/**
* 分页查询学生列表(脱敏版本)
*/
TableDataInfo<OpenApiStudentVo> selectPageList(PgStudent student, PageQuery pageQuery);
/**
* 查询学生详情(脱敏版本)
*/
OpenApiStudentVo selectById(Long studentId);
}
服务实现:
@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)
@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)
/**
* 数据脱敏工具类
*/
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 表中新增一条记录:
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:
@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 目录下新建服务接口和实现类:
服务接口:
public interface IOpenApiTeacherService {
TableDataInfo<OpenApiTeacherVo> selectPageList(PgTeacher teacher, PageQuery pageQuery);
}
服务实现:
@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 目录下新建控制器:
@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 接口开发规范
- 路径规范:所有开放接口必须以
/open/api/开头 - 分层规范:
- Controller 层仅负责接收请求和返回响应
- Service 层处理业务逻辑(脱敏、转换、字段过滤等)
- 复用原有业务服务,避免重复代码
- VO 设计规范:
- 为开放接口单独创建 VO,避免直接使用内部实体
- VO 中只包含需要对外暴露的字段
- 敏感字段在 Service 层进行脱敏处理
- 返回格式:使用统一的
R<T>或TableDataInfo<T>返回格式 - 参数校验:使用
@Validated注解进行参数校验 - 日志记录:重要操作需记录日志
- 异常处理:业务异常使用
ServiceException
10.3 调用示例代码
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 调用示例
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 脚本执行顺序
pangu_tables.sql- 创建表结构pangu_menu.sql- 插入菜单数据open_api_dict_data.sql- 插入API字典初始数据
11.2 配置项
# 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 | 初稿 |
文档结束