# 应用管理模块 - 需求与技术设计方案 ## 文档信息 | 项目 | 内容 | |------|------| | 项目名称 | 盘古用户认证中心 - 应用管理模块 | | 文档版本 | 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 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 list(PgStudent student, PageQuery pageQuery) { return openApiStudentService.selectPageList(student, pageQuery); } /** * 学生详情查询(对外开放接口) */ @GetMapping("/{studentId}") public R getInfo(@PathVariable Long studentId) { return R.ok(openApiStudentService.selectById(studentId)); } } ``` #### 9.2.4 开放接口业务层 (OpenApiStudentService) **说明**:开放接口单独创建 Service 层,用于处理脱敏、数据转换、字段过滤等业务逻辑,与内部业务服务解耦。 **服务接口**: ```java /** * 开放接口学生服务接口 */ public interface IOpenApiStudentService { /** * 分页查询学生列表(脱敏版本) */ TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery); /** * 查询学生详情(脱敏版本) */ OpenApiStudentVo selectById(Long studentId); } ``` **服务实现**: ```java @Service @RequiredArgsConstructor public class OpenApiStudentServiceImpl implements IOpenApiStudentService { private final IPgStudentService studentService; // 复用原有业务服务 @Override public TableDataInfo selectPageList(PgStudent student, PageQuery pageQuery) { // 1. 调用原有业务服务获取数据 TableDataInfo result = studentService.selectPageList(student, pageQuery); // 2. 转换为开放接口VO并脱敏 List openApiList = result.getRows().stream() .map(this::convertToOpenApiVo) .collect(Collectors.toList()); // 3. 返回脱敏后的数据 TableDataInfo 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 selectPageList(PgTeacher teacher, PageQuery pageQuery); } ``` **服务实现**: ```java @Service @RequiredArgsConstructor public class OpenApiTeacherServiceImpl implements IOpenApiTeacherService { private final IPgTeacherService teacherService; // 复用原有业务服务 @Override public TableDataInfo selectPageList(PgTeacher teacher, PageQuery pageQuery) { // 1. 调用原有业务服务 TableDataInfo result = teacherService.selectPageList(teacher, pageQuery); // 2. 转换并脱敏 List openApiList = result.getRows().stream() .map(this::convertToOpenApiVo) .collect(Collectors.toList()); // 3. 返回 TableDataInfo 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 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` 或 `TableDataInfo` 返回格式 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 params) throws Exception { // 1. 添加时间戳 String timestamp = String.valueOf(System.currentTimeMillis()); // 2. 计算签名 TreeMap 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 | 初稿 | --- *文档结束*