2026-01-31 17:55:58 +08:00
|
|
|
|
# 盘古用户平台 - 应用管理模块后端技术方案
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
| 文档信息 | 内容 |
|
|
|
|
|
|
|---------|------|
|
|
|
|
|
|
| **文档版本** | V1.0 |
|
|
|
|
|
|
| **模块名称** | 应用管理模块 - 后端 |
|
2026-01-31 23:14:11 +08:00
|
|
|
|
| **编写团队 | pangu |
|
2026-01-31 17:55:58 +08:00
|
|
|
|
| **创建日期** | 2026-01-31 |
|
|
|
|
|
|
| **审核状态** | 待评审 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 技术栈
|
|
|
|
|
|
|
|
|
|
|
|
| 技术 | 版本 | 用途 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| JDK | 17+ | 运行环境 |
|
|
|
|
|
|
| Spring Boot | 3.3.x | 应用框架 |
|
|
|
|
|
|
| Spring Security | 6.x | 安全框架 |
|
|
|
|
|
|
| MyBatis Plus | 3.5.x | ORM框架 |
|
|
|
|
|
|
| MySQL | 8.0 | 数据库 |
|
|
|
|
|
|
| Redis | 7.x | 缓存 |
|
|
|
|
|
|
| Hutool | 5.x | 工具库 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 模块结构
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 包结构
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
com.pangu.admin/
|
|
|
|
|
|
├── controller/
|
|
|
|
|
|
│ └── ApplicationController.java # 应用管理控制器
|
|
|
|
|
|
├── service/
|
|
|
|
|
|
│ ├── IApplicationService.java # 应用服务接口
|
|
|
|
|
|
│ └── impl/
|
|
|
|
|
|
│ └── ApplicationServiceImpl.java # 应用服务实现
|
|
|
|
|
|
├── mapper/
|
|
|
|
|
|
│ ├── ApplicationMapper.java # 应用Mapper
|
|
|
|
|
|
│ ├── AppApiMapper.java # 应用接口授权Mapper
|
|
|
|
|
|
│ └── ApiDictMapper.java # 接口字典Mapper
|
|
|
|
|
|
└── domain/
|
|
|
|
|
|
├── entity/
|
|
|
|
|
|
│ ├── Application.java # 应用实体
|
|
|
|
|
|
│ ├── AppApi.java # 应用接口授权实体
|
|
|
|
|
|
│ └── ApiDict.java # 接口字典实体
|
|
|
|
|
|
├── vo/
|
|
|
|
|
|
│ └── ApplicationVO.java # 应用视图对象
|
|
|
|
|
|
└── dto/
|
|
|
|
|
|
└── ApplicationDTO.java # 应用传输对象
|
|
|
|
|
|
|
|
|
|
|
|
com.pangu.open/
|
|
|
|
|
|
├── controller/
|
|
|
|
|
|
│ └── OpenApiController.java # 开放API控制器
|
|
|
|
|
|
├── config/
|
|
|
|
|
|
│ └── OpenApiConfig.java # 开放API配置
|
|
|
|
|
|
├── interceptor/
|
|
|
|
|
|
│ └── ApiAuthInterceptor.java # API认证拦截器
|
|
|
|
|
|
└── service/
|
|
|
|
|
|
├── IApiAuthService.java # API认证服务接口
|
|
|
|
|
|
└── impl/
|
|
|
|
|
|
└── ApiAuthServiceImpl.java # API认证服务实现
|
|
|
|
|
|
|
|
|
|
|
|
com.pangu.common/
|
|
|
|
|
|
└── util/
|
|
|
|
|
|
├── SecretGenerator.java # 密钥生成工具
|
|
|
|
|
|
└── SignUtils.java # 签名工具
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2.2 MyBatis Mapper XML
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
resources/mapper/
|
|
|
|
|
|
├── ApplicationMapper.xml
|
|
|
|
|
|
├── AppApiMapper.xml
|
|
|
|
|
|
└── ApiDictMapper.xml
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 数据库设计
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 表结构
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.1.1 应用表 (pg_application)
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE `pg_application` (
|
|
|
|
|
|
`app_id` bigint NOT NULL AUTO_INCREMENT COMMENT '应用ID',
|
|
|
|
|
|
`app_code` varchar(32) NOT NULL COMMENT '应用编码',
|
|
|
|
|
|
`app_name` varchar(100) NOT NULL COMMENT '应用名称',
|
|
|
|
|
|
`app_secret` varchar(64) NOT NULL COMMENT '应用密钥',
|
|
|
|
|
|
`app_desc` varchar(500) DEFAULT NULL COMMENT '应用描述',
|
|
|
|
|
|
`contact_person` varchar(50) DEFAULT NULL COMMENT '联系人',
|
|
|
|
|
|
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
|
|
|
|
|
|
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
|
|
|
|
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
|
|
|
|
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
|
|
|
|
|
`update_by` varchar(64) DEFAULT '' 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`),
|
|
|
|
|
|
UNIQUE KEY `uk_app_name` (`app_name`, `del_flag`)
|
|
|
|
|
|
) ENGINE=InnoDB COMMENT='应用表';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.1.2 应用接口授权表 (pg_app_api)
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE `pg_app_api` (
|
|
|
|
|
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
|
|
|
|
|
`app_id` bigint NOT NULL COMMENT '应用ID',
|
|
|
|
|
|
`api_code` varchar(100) NOT NULL COMMENT '接口编码',
|
|
|
|
|
|
`api_name` varchar(100) DEFAULT NULL COMMENT '接口名称',
|
|
|
|
|
|
`api_path` varchar(200) NOT NULL COMMENT '接口路径',
|
|
|
|
|
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
|
|
|
|
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
|
|
|
|
|
PRIMARY KEY (`id`),
|
|
|
|
|
|
UNIQUE KEY `uk_app_api` (`app_id`, `api_code`),
|
|
|
|
|
|
KEY `idx_app_id` (`app_id`)
|
|
|
|
|
|
) ENGINE=InnoDB COMMENT='应用接口授权表';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 3.1.3 API接口字典表 (pg_api_dict)
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE `pg_api_dict` (
|
|
|
|
|
|
`api_id` bigint NOT NULL AUTO_INCREMENT COMMENT '接口ID',
|
|
|
|
|
|
`api_code` varchar(100) NOT NULL COMMENT '接口编码',
|
|
|
|
|
|
`api_name` varchar(100) NOT NULL COMMENT '接口名称',
|
|
|
|
|
|
`api_path` varchar(200) NOT NULL COMMENT '接口路径',
|
|
|
|
|
|
`api_method` varchar(10) DEFAULT 'GET' COMMENT '请求方法',
|
|
|
|
|
|
`api_desc` varchar(500) DEFAULT NULL COMMENT '接口描述',
|
|
|
|
|
|
`order_num` int DEFAULT 0 COMMENT '显示顺序',
|
|
|
|
|
|
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
|
|
|
|
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
|
|
|
|
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
|
|
|
|
|
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
|
|
|
|
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
|
|
|
|
|
PRIMARY KEY (`api_id`),
|
|
|
|
|
|
UNIQUE KEY `uk_api_code` (`api_code`)
|
|
|
|
|
|
) ENGINE=InnoDB COMMENT='API接口字典表';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 初始化数据SQL
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
-- API接口字典数据
|
|
|
|
|
|
INSERT INTO pg_api_dict (api_id, api_code, api_name, api_path, api_method, api_desc, order_num) VALUES
|
|
|
|
|
|
(1, 'STUDENT_LIST', '查询学生信息', '/open/student/list', 'GET', '获取学生列表', 1),
|
|
|
|
|
|
(2, 'SCHOOL_LIST', '查询学校信息', '/open/school/list', 'GET', '获取学校列表', 2),
|
|
|
|
|
|
(3, 'GRADE_LIST', '查询年级信息', '/open/grade/list', 'GET', '获取年级列表', 3),
|
|
|
|
|
|
(4, 'CLASS_LIST', '查询班级信息', '/open/class/list', 'GET', '获取班级列表', 4),
|
|
|
|
|
|
(5, 'MEMBER_LIST', '查询会员信息', '/open/member/list', 'GET', '获取会员列表', 5),
|
|
|
|
|
|
(6, 'REGION_TREE', '查询区域树', '/open/region/tree', 'GET', '获取区域树形结构', 6);
|
|
|
|
|
|
|
|
|
|
|
|
-- 菜单数据
|
|
|
|
|
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, visible, perms, icon) VALUES
|
|
|
|
|
|
(2100, '应用管理', 0, 10, 'application', 'application/index', 'C', '0', 'system:application:list', 'app');
|
|
|
|
|
|
|
|
|
|
|
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, perms, menu_type) VALUES
|
|
|
|
|
|
(2101, '应用查询', 2100, 1, 'system:application:query', 'F'),
|
|
|
|
|
|
(2102, '应用新增', 2100, 2, 'system:application:add', 'F'),
|
|
|
|
|
|
(2103, '应用修改', 2100, 3, 'system:application:edit', 'F'),
|
|
|
|
|
|
(2104, '应用删除', 2100, 4, 'system:application:remove', 'F'),
|
|
|
|
|
|
(2105, '密钥重置', 2100, 5, 'system:application:resetSecret', 'F');
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 实体类设计
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 Application.java(应用实体)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.domain.entity;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.annotation.*;
|
|
|
|
|
|
import com.pangu.common.core.domain.BaseEntity;
|
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
import lombok.EqualsAndHashCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用实体
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Data
|
|
|
|
|
|
@EqualsAndHashCode(callSuper = true)
|
|
|
|
|
|
@TableName("pg_application")
|
|
|
|
|
|
public class Application extends BaseEntity {
|
|
|
|
|
|
|
|
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
@TableId(type = IdType.AUTO)
|
|
|
|
|
|
private Long appId;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用编码(格式:YY + 6位序号)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用名称
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appName;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用密钥(32位随机字符串)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appSecret;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用描述
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appDesc;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 联系人
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String contactPerson;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 联系电话
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String contactPhone;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 状态(0正常 1停用)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String status;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除标志(0存在 1删除)
|
|
|
|
|
|
*/
|
|
|
|
|
|
@TableLogic
|
|
|
|
|
|
private String delFlag;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 AppApi.java(应用接口授权实体)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.domain.entity;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.annotation.*;
|
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
|
|
|
|
|
|
import java.io.Serializable;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用接口授权实体
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Data
|
|
|
|
|
|
@TableName("pg_app_api")
|
|
|
|
|
|
public class AppApi implements Serializable {
|
|
|
|
|
|
|
|
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 主键ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
@TableId(type = IdType.AUTO)
|
|
|
|
|
|
private Long id;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Long appId;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口编码
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口名称
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiName;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiPath;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建者
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String createBy;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建时间
|
|
|
|
|
|
*/
|
|
|
|
|
|
private LocalDateTime createTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.3 ApiDict.java(接口字典实体)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.domain.entity;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.annotation.*;
|
|
|
|
|
|
import com.pangu.common.core.domain.BaseEntity;
|
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
import lombok.EqualsAndHashCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* API接口字典实体
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Data
|
|
|
|
|
|
@EqualsAndHashCode(callSuper = true)
|
|
|
|
|
|
@TableName("pg_api_dict")
|
|
|
|
|
|
public class ApiDict extends BaseEntity {
|
|
|
|
|
|
|
|
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
@TableId(type = IdType.AUTO)
|
|
|
|
|
|
private Long apiId;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口编码
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口名称
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiName;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口路径
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiPath;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 请求方法
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiMethod;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口描述
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String apiDesc;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 显示顺序
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Integer orderNum;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 状态(0正常 1停用)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String status;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.4 ApplicationDTO.java(传输对象)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.domain.dto;
|
|
|
|
|
|
|
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
|
|
|
|
|
|
import jakarta.validation.constraints.NotBlank;
|
|
|
|
|
|
import jakarta.validation.constraints.Size;
|
|
|
|
|
|
import java.io.Serializable;
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用传输对象
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Data
|
|
|
|
|
|
public class ApplicationDTO implements Serializable {
|
|
|
|
|
|
|
|
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用ID(编辑时必填)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Long appId;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用名称
|
|
|
|
|
|
*/
|
|
|
|
|
|
@NotBlank(message = "应用名称不能为空")
|
|
|
|
|
|
@Size(max = 100, message = "应用名称不能超过100个字符")
|
|
|
|
|
|
private String appName;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用编码(查询条件)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用描述
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Size(max = 500, message = "应用描述不能超过500个字符")
|
|
|
|
|
|
private String appDesc;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 联系人
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Size(max = 50, message = "联系人不能超过50个字符")
|
|
|
|
|
|
private String contactPerson;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 联系电话
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Size(max = 20, message = "联系电话不能超过20个字符")
|
|
|
|
|
|
private String contactPhone;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 状态(0正常 1停用)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String status;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 授权的接口编码列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
private List<String> apiCodes;
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 查询条件 ==========
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分页页码
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Integer pageNum;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分页大小
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Integer pageSize;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.5 ApplicationVO.java(视图对象)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.domain.vo;
|
|
|
|
|
|
|
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
|
|
|
|
|
|
import java.io.Serializable;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用视图对象
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Data
|
|
|
|
|
|
public class ApplicationVO implements Serializable {
|
|
|
|
|
|
|
|
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
private Long appId;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用编码
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appCode;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用名称
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appName;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用描述
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String appDesc;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 联系人
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String contactPerson;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 联系电话
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String contactPhone;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 状态(0正常 1停用)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String status;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建者
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String createBy;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建时间
|
|
|
|
|
|
*/
|
|
|
|
|
|
private LocalDateTime createTime;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 授权的接口列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
private List<ApiInfo> apis;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 接口信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Data
|
|
|
|
|
|
public static class ApiInfo implements Serializable {
|
|
|
|
|
|
private String apiCode;
|
|
|
|
|
|
private String apiName;
|
|
|
|
|
|
private String apiPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Mapper层设计
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 ApplicationMapper.java
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.mapper;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.Application;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Mapper;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Param;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Select;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用Mapper接口
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Mapper
|
|
|
|
|
|
public interface ApplicationMapper extends BaseMapper<Application> {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询最大应用编码
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT MAX(app_code) FROM pg_application WHERE del_flag = '0'")
|
|
|
|
|
|
String selectMaxAppCode();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据应用编码查询应用
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT * FROM pg_application WHERE app_code = #{appCode} AND del_flag = '0'")
|
|
|
|
|
|
Application selectByAppCode(@Param("appCode") String appCode);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查应用名称是否存在
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT COUNT(*) FROM pg_application WHERE app_name = #{appName} AND del_flag = '0' AND app_id != #{excludeId}")
|
|
|
|
|
|
int checkAppNameExists(@Param("appName") String appName, @Param("excludeId") Long excludeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 AppApiMapper.java
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.mapper;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.AppApi;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Delete;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Mapper;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Param;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Select;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用接口授权Mapper接口
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Mapper
|
|
|
|
|
|
public interface AppApiMapper extends BaseMapper<AppApi> {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据应用ID查询授权接口
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT * FROM pg_app_api WHERE app_id = #{appId}")
|
|
|
|
|
|
List<AppApi> selectByAppId(@Param("appId") Long appId);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据应用ID删除授权接口
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Delete("DELETE FROM pg_app_api WHERE app_id = #{appId}")
|
|
|
|
|
|
int deleteByAppId(@Param("appId") Long appId);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查应用是否有某接口权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT COUNT(*) FROM pg_app_api WHERE app_id = #{appId} AND api_path = #{apiPath}")
|
|
|
|
|
|
int checkPermission(@Param("appId") Long appId, @Param("apiPath") String apiPath);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据应用编码查询授权的接口路径列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT aa.api_path FROM pg_app_api aa " +
|
|
|
|
|
|
"INNER JOIN pg_application a ON aa.app_id = a.app_id " +
|
|
|
|
|
|
"WHERE a.app_code = #{appCode} AND a.del_flag = '0'")
|
|
|
|
|
|
List<String> selectApiPathsByAppCode(@Param("appCode") String appCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 ApiDictMapper.java
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.mapper;
|
|
|
|
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.ApiDict;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Mapper;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Param;
|
|
|
|
|
|
import org.apache.ibatis.annotations.Select;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* API接口字典Mapper接口
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Mapper
|
|
|
|
|
|
public interface ApiDictMapper extends BaseMapper<ApiDict> {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询启用的接口列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Select("SELECT * FROM pg_api_dict WHERE status = '0' ORDER BY order_num")
|
|
|
|
|
|
List<ApiDict> selectEnabledList();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据接口编码列表查询
|
|
|
|
|
|
*/
|
|
|
|
|
|
List<ApiDict> selectByCodes(@Param("codes") List<String> codes);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Service层设计
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 IApplicationService.java(接口)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.service;
|
|
|
|
|
|
|
|
|
|
|
|
import com.pangu.admin.domain.dto.ApplicationDTO;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.ApiDict;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.Application;
|
|
|
|
|
|
import com.pangu.admin.domain.vo.ApplicationVO;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用管理服务接口
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
public interface IApplicationService {
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询应用列表
|
|
|
|
|
|
* @param dto 查询条件
|
|
|
|
|
|
* @return 应用列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
List<ApplicationVO> selectApplicationList(ApplicationDTO dto);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询应用详情
|
|
|
|
|
|
* @param appId 应用ID
|
|
|
|
|
|
* @return 应用信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
ApplicationVO selectApplicationById(Long appId);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 新增应用
|
|
|
|
|
|
* @param dto 应用数据
|
|
|
|
|
|
* @return 包含appCode和appSecret的Map
|
|
|
|
|
|
*/
|
|
|
|
|
|
Map<String, String> insertApplication(ApplicationDTO dto);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改应用
|
|
|
|
|
|
* @param dto 应用数据
|
|
|
|
|
|
* @return 影响行数
|
|
|
|
|
|
*/
|
|
|
|
|
|
int updateApplication(ApplicationDTO dto);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除应用
|
|
|
|
|
|
* @param appId 应用ID
|
|
|
|
|
|
* @return 影响行数
|
|
|
|
|
|
*/
|
|
|
|
|
|
int deleteApplicationById(Long appId);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置应用密钥
|
|
|
|
|
|
* @param appId 应用ID
|
|
|
|
|
|
* @return 新密钥
|
|
|
|
|
|
*/
|
|
|
|
|
|
String resetAppSecret(Long appId);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取API接口字典列表
|
|
|
|
|
|
* @return 接口列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
List<ApiDict> selectApiDictList();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 根据应用编码查询应用(用于API认证)
|
|
|
|
|
|
* @param appCode 应用编码
|
|
|
|
|
|
* @return 应用信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
Application selectByAppCode(String appCode);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查应用是否有接口权限
|
|
|
|
|
|
* @param appCode 应用编码
|
|
|
|
|
|
* @param apiPath 接口路径
|
|
|
|
|
|
* @return 是否有权限
|
|
|
|
|
|
*/
|
|
|
|
|
|
boolean checkApiPermission(String appCode, String apiPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 6.2 ApplicationServiceImpl.java(实现)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.service.impl;
|
|
|
|
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
|
|
|
|
import cn.hutool.core.util.RandomUtil;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
|
import com.pangu.admin.domain.dto.ApplicationDTO;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.ApiDict;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.AppApi;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.Application;
|
|
|
|
|
|
import com.pangu.admin.domain.vo.ApplicationVO;
|
|
|
|
|
|
import com.pangu.admin.mapper.ApiDictMapper;
|
|
|
|
|
|
import com.pangu.admin.mapper.AppApiMapper;
|
|
|
|
|
|
import com.pangu.admin.mapper.ApplicationMapper;
|
|
|
|
|
|
import com.pangu.admin.service.IApplicationService;
|
|
|
|
|
|
import com.pangu.common.core.exception.ServiceException;
|
|
|
|
|
|
import com.pangu.common.redis.service.RedisService;
|
|
|
|
|
|
import com.pangu.common.security.utils.SecurityUtils;
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
|
import java.util.*;
|
|
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用管理服务实现
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Slf4j
|
|
|
|
|
|
@Service
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class ApplicationServiceImpl implements IApplicationService {
|
|
|
|
|
|
|
|
|
|
|
|
private final ApplicationMapper applicationMapper;
|
|
|
|
|
|
private final AppApiMapper appApiMapper;
|
|
|
|
|
|
private final ApiDictMapper apiDictMapper;
|
|
|
|
|
|
private final RedisService redisService;
|
|
|
|
|
|
|
|
|
|
|
|
/** 应用信息缓存前缀 */
|
|
|
|
|
|
private static final String CACHE_APP_INFO = "app:info:";
|
|
|
|
|
|
/** 应用授权接口缓存前缀 */
|
|
|
|
|
|
private static final String CACHE_APP_APIS = "app:apis:";
|
|
|
|
|
|
/** 缓存过期时间(分钟) */
|
|
|
|
|
|
private static final long CACHE_EXPIRE_MINUTES = 30;
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public List<ApplicationVO> selectApplicationList(ApplicationDTO dto) {
|
|
|
|
|
|
LambdaQueryWrapper<Application> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
|
|
|
|
|
|
|
// 查询条件
|
|
|
|
|
|
if (dto.getAppName() != null && !dto.getAppName().isEmpty()) {
|
|
|
|
|
|
wrapper.like(Application::getAppName, dto.getAppName());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (dto.getAppCode() != null && !dto.getAppCode().isEmpty()) {
|
|
|
|
|
|
wrapper.like(Application::getAppCode, dto.getAppCode());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (dto.getStatus() != null && !dto.getStatus().isEmpty()) {
|
|
|
|
|
|
wrapper.eq(Application::getStatus, dto.getStatus());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wrapper.orderByDesc(Application::getCreateTime);
|
|
|
|
|
|
|
|
|
|
|
|
List<Application> list = applicationMapper.selectList(wrapper);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为VO并填充授权接口信息
|
|
|
|
|
|
return list.stream().map(this::toVO).collect(Collectors.toList());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public ApplicationVO selectApplicationById(Long appId) {
|
|
|
|
|
|
Application app = applicationMapper.selectById(appId);
|
|
|
|
|
|
if (app == null) {
|
|
|
|
|
|
throw new ServiceException("应用不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
return toVO(app);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public Map<String, String> insertApplication(ApplicationDTO dto) {
|
|
|
|
|
|
// 检查应用名称是否重复
|
|
|
|
|
|
if (applicationMapper.checkAppNameExists(dto.getAppName(), 0L) > 0) {
|
|
|
|
|
|
throw new ServiceException("应用名称已存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成应用编码和密钥
|
|
|
|
|
|
String appCode = generateAppCode();
|
|
|
|
|
|
String appSecret = generateAppSecret();
|
|
|
|
|
|
|
|
|
|
|
|
// 构建实体
|
|
|
|
|
|
Application app = new Application();
|
|
|
|
|
|
app.setAppCode(appCode);
|
|
|
|
|
|
app.setAppName(dto.getAppName());
|
|
|
|
|
|
app.setAppSecret(appSecret);
|
|
|
|
|
|
app.setAppDesc(dto.getAppDesc());
|
|
|
|
|
|
app.setContactPerson(dto.getContactPerson());
|
|
|
|
|
|
app.setContactPhone(dto.getContactPhone());
|
|
|
|
|
|
app.setStatus(dto.getStatus() != null ? dto.getStatus() : "0");
|
|
|
|
|
|
app.setCreateBy(SecurityUtils.getUsername());
|
|
|
|
|
|
app.setCreateTime(LocalDateTime.now());
|
|
|
|
|
|
|
|
|
|
|
|
applicationMapper.insert(app);
|
|
|
|
|
|
|
|
|
|
|
|
// 保存接口授权
|
|
|
|
|
|
saveAppApis(app.getAppId(), dto.getApiCodes());
|
|
|
|
|
|
|
|
|
|
|
|
log.info("新增应用成功,appCode={}, appName={}", appCode, dto.getAppName());
|
|
|
|
|
|
|
|
|
|
|
|
// 返回编码和密钥
|
|
|
|
|
|
Map<String, String> result = new HashMap<>();
|
|
|
|
|
|
result.put("appCode", appCode);
|
|
|
|
|
|
result.put("appSecret", appSecret);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int updateApplication(ApplicationDTO dto) {
|
|
|
|
|
|
if (dto.getAppId() == null) {
|
|
|
|
|
|
throw new ServiceException("应用ID不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Application existApp = applicationMapper.selectById(dto.getAppId());
|
|
|
|
|
|
if (existApp == null) {
|
|
|
|
|
|
throw new ServiceException("应用不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查应用名称是否重复(排除自身)
|
|
|
|
|
|
if (applicationMapper.checkAppNameExists(dto.getAppName(), dto.getAppId()) > 0) {
|
|
|
|
|
|
throw new ServiceException("应用名称已存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新应用信息
|
|
|
|
|
|
Application app = new Application();
|
|
|
|
|
|
app.setAppId(dto.getAppId());
|
|
|
|
|
|
app.setAppName(dto.getAppName());
|
|
|
|
|
|
app.setAppDesc(dto.getAppDesc());
|
|
|
|
|
|
app.setContactPerson(dto.getContactPerson());
|
|
|
|
|
|
app.setContactPhone(dto.getContactPhone());
|
|
|
|
|
|
app.setStatus(dto.getStatus());
|
|
|
|
|
|
app.setUpdateBy(SecurityUtils.getUsername());
|
|
|
|
|
|
app.setUpdateTime(LocalDateTime.now());
|
|
|
|
|
|
|
|
|
|
|
|
int rows = applicationMapper.updateById(app);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新接口授权(先删后增)
|
|
|
|
|
|
appApiMapper.deleteByAppId(dto.getAppId());
|
|
|
|
|
|
saveAppApis(dto.getAppId(), dto.getApiCodes());
|
|
|
|
|
|
|
|
|
|
|
|
// 清除缓存
|
|
|
|
|
|
clearAppCache(existApp.getAppCode());
|
|
|
|
|
|
|
|
|
|
|
|
log.info("修改应用成功,appId={}, appName={}", dto.getAppId(), dto.getAppName());
|
|
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public int deleteApplicationById(Long appId) {
|
|
|
|
|
|
Application app = applicationMapper.selectById(appId);
|
|
|
|
|
|
if (app == null) {
|
|
|
|
|
|
throw new ServiceException("应用不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除应用(逻辑删除)
|
|
|
|
|
|
int rows = applicationMapper.deleteById(appId);
|
|
|
|
|
|
|
|
|
|
|
|
// 删除接口授权
|
|
|
|
|
|
appApiMapper.deleteByAppId(appId);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除缓存
|
|
|
|
|
|
clearAppCache(app.getAppCode());
|
|
|
|
|
|
|
|
|
|
|
|
log.info("删除应用成功,appId={}, appCode={}", appId, app.getAppCode());
|
|
|
|
|
|
|
|
|
|
|
|
return rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
|
public String resetAppSecret(Long appId) {
|
|
|
|
|
|
Application app = applicationMapper.selectById(appId);
|
|
|
|
|
|
if (app == null) {
|
|
|
|
|
|
throw new ServiceException("应用不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成新密钥
|
|
|
|
|
|
String newSecret = generateAppSecret();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新密钥
|
|
|
|
|
|
Application updateApp = new Application();
|
|
|
|
|
|
updateApp.setAppId(appId);
|
|
|
|
|
|
updateApp.setAppSecret(newSecret);
|
|
|
|
|
|
updateApp.setUpdateBy(SecurityUtils.getUsername());
|
|
|
|
|
|
updateApp.setUpdateTime(LocalDateTime.now());
|
|
|
|
|
|
|
|
|
|
|
|
applicationMapper.updateById(updateApp);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除缓存
|
|
|
|
|
|
clearAppCache(app.getAppCode());
|
|
|
|
|
|
|
|
|
|
|
|
log.info("重置应用密钥成功,appId={}, appCode={}", appId, app.getAppCode());
|
|
|
|
|
|
|
|
|
|
|
|
return newSecret;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public List<ApiDict> selectApiDictList() {
|
|
|
|
|
|
return apiDictMapper.selectEnabledList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public Application selectByAppCode(String appCode) {
|
|
|
|
|
|
// 先从缓存获取
|
|
|
|
|
|
String cacheKey = CACHE_APP_INFO + appCode;
|
|
|
|
|
|
Application app = redisService.getCacheObject(cacheKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (app == null) {
|
|
|
|
|
|
// 从数据库查询
|
|
|
|
|
|
app = applicationMapper.selectByAppCode(appCode);
|
|
|
|
|
|
if (app != null) {
|
|
|
|
|
|
// 放入缓存
|
|
|
|
|
|
redisService.setCacheObject(cacheKey, app, CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return app;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean checkApiPermission(String appCode, String apiPath) {
|
|
|
|
|
|
// 先从缓存获取授权接口列表
|
|
|
|
|
|
String cacheKey = CACHE_APP_APIS + appCode;
|
|
|
|
|
|
Set<String> apiPaths = redisService.getCacheObject(cacheKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (apiPaths == null) {
|
|
|
|
|
|
// 从数据库查询
|
|
|
|
|
|
List<String> paths = appApiMapper.selectApiPathsByAppCode(appCode);
|
|
|
|
|
|
apiPaths = new HashSet<>(paths);
|
|
|
|
|
|
// 放入缓存
|
|
|
|
|
|
redisService.setCacheObject(cacheKey, apiPaths, CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return apiPaths.contains(apiPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 私有方法 ==========
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成应用编码
|
|
|
|
|
|
* 格式:YY + 6位序号(从000001开始)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String generateAppCode() {
|
|
|
|
|
|
String maxCode = applicationMapper.selectMaxAppCode();
|
|
|
|
|
|
int nextSeq = 1;
|
|
|
|
|
|
if (maxCode != null && maxCode.length() > 2) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
nextSeq = Integer.parseInt(maxCode.substring(2)) + 1;
|
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
|
log.warn("解析应用编码失败,使用默认序号1");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return String.format("YY%06d", nextSeq);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成32位随机密钥
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String generateAppSecret() {
|
|
|
|
|
|
return RandomUtil.randomString(32);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 保存应用接口授权
|
|
|
|
|
|
*/
|
|
|
|
|
|
private void saveAppApis(Long appId, List<String> apiCodes) {
|
|
|
|
|
|
if (CollUtil.isEmpty(apiCodes)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询接口字典
|
|
|
|
|
|
List<ApiDict> apiDicts = apiDictMapper.selectByCodes(apiCodes);
|
|
|
|
|
|
Map<String, ApiDict> dictMap = apiDicts.stream()
|
|
|
|
|
|
.collect(Collectors.toMap(ApiDict::getApiCode, d -> d));
|
|
|
|
|
|
|
|
|
|
|
|
// 批量插入授权记录
|
|
|
|
|
|
String username = SecurityUtils.getUsername();
|
|
|
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
|
|
|
|
|
|
|
|
|
for (String apiCode : apiCodes) {
|
|
|
|
|
|
ApiDict dict = dictMap.get(apiCode);
|
|
|
|
|
|
if (dict != null) {
|
|
|
|
|
|
AppApi appApi = new AppApi();
|
|
|
|
|
|
appApi.setAppId(appId);
|
|
|
|
|
|
appApi.setApiCode(apiCode);
|
|
|
|
|
|
appApi.setApiName(dict.getApiName());
|
|
|
|
|
|
appApi.setApiPath(dict.getApiPath());
|
|
|
|
|
|
appApi.setCreateBy(username);
|
|
|
|
|
|
appApi.setCreateTime(now);
|
|
|
|
|
|
appApiMapper.insert(appApi);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 转换为VO
|
|
|
|
|
|
*/
|
|
|
|
|
|
private ApplicationVO toVO(Application app) {
|
|
|
|
|
|
ApplicationVO vo = new ApplicationVO();
|
|
|
|
|
|
vo.setAppId(app.getAppId());
|
|
|
|
|
|
vo.setAppCode(app.getAppCode());
|
|
|
|
|
|
vo.setAppName(app.getAppName());
|
|
|
|
|
|
vo.setAppDesc(app.getAppDesc());
|
|
|
|
|
|
vo.setContactPerson(app.getContactPerson());
|
|
|
|
|
|
vo.setContactPhone(app.getContactPhone());
|
|
|
|
|
|
vo.setStatus(app.getStatus());
|
|
|
|
|
|
vo.setCreateBy(app.getCreateBy());
|
|
|
|
|
|
vo.setCreateTime(app.getCreateTime());
|
|
|
|
|
|
|
|
|
|
|
|
// 查询授权接口
|
|
|
|
|
|
List<AppApi> appApis = appApiMapper.selectByAppId(app.getAppId());
|
|
|
|
|
|
if (CollUtil.isNotEmpty(appApis)) {
|
|
|
|
|
|
List<ApplicationVO.ApiInfo> apis = appApis.stream().map(api -> {
|
|
|
|
|
|
ApplicationVO.ApiInfo info = new ApplicationVO.ApiInfo();
|
|
|
|
|
|
info.setApiCode(api.getApiCode());
|
|
|
|
|
|
info.setApiName(api.getApiName());
|
|
|
|
|
|
info.setApiPath(api.getApiPath());
|
|
|
|
|
|
return info;
|
|
|
|
|
|
}).collect(Collectors.toList());
|
|
|
|
|
|
vo.setApis(apis);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return vo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清除应用缓存
|
|
|
|
|
|
*/
|
|
|
|
|
|
private void clearAppCache(String appCode) {
|
|
|
|
|
|
redisService.deleteObject(CACHE_APP_INFO + appCode);
|
|
|
|
|
|
redisService.deleteObject(CACHE_APP_APIS + appCode);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. Controller层设计
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 ApplicationController.java
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.controller;
|
|
|
|
|
|
|
|
|
|
|
|
import com.pangu.admin.domain.dto.ApplicationDTO;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.ApiDict;
|
|
|
|
|
|
import com.pangu.admin.domain.vo.ApplicationVO;
|
|
|
|
|
|
import com.pangu.admin.service.IApplicationService;
|
|
|
|
|
|
import com.pangu.common.core.controller.BaseController;
|
|
|
|
|
|
import com.pangu.common.core.domain.AjaxResult;
|
|
|
|
|
|
import com.pangu.common.core.page.TableDataInfo;
|
|
|
|
|
|
import com.pangu.common.log.annotation.Log;
|
|
|
|
|
|
import com.pangu.common.log.enums.BusinessType;
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用管理控制器
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@RestController
|
|
|
|
|
|
@RequestMapping("/api/application")
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class ApplicationController extends BaseController {
|
|
|
|
|
|
|
|
|
|
|
|
private final IApplicationService applicationService;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询应用列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:list')")
|
|
|
|
|
|
@GetMapping("/list")
|
|
|
|
|
|
public TableDataInfo list(ApplicationDTO dto) {
|
|
|
|
|
|
startPage();
|
|
|
|
|
|
List<ApplicationVO> list = applicationService.selectApplicationList(dto);
|
|
|
|
|
|
return getDataTable(list);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取应用详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:query')")
|
|
|
|
|
|
@GetMapping("/{appId}")
|
|
|
|
|
|
public AjaxResult getInfo(@PathVariable Long appId) {
|
|
|
|
|
|
return success(applicationService.selectApplicationById(appId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 新增应用
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:add')")
|
|
|
|
|
|
@Log(title = "应用管理", businessType = BusinessType.INSERT)
|
|
|
|
|
|
@PostMapping
|
|
|
|
|
|
public AjaxResult add(@Validated @RequestBody ApplicationDTO dto) {
|
|
|
|
|
|
Map<String, String> result = applicationService.insertApplication(dto);
|
|
|
|
|
|
return success(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改应用
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:edit')")
|
|
|
|
|
|
@Log(title = "应用管理", businessType = BusinessType.UPDATE)
|
|
|
|
|
|
@PutMapping
|
|
|
|
|
|
public AjaxResult edit(@Validated @RequestBody ApplicationDTO dto) {
|
|
|
|
|
|
return toAjax(applicationService.updateApplication(dto));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除应用
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:remove')")
|
|
|
|
|
|
@Log(title = "应用管理", businessType = BusinessType.DELETE)
|
|
|
|
|
|
@DeleteMapping("/{appId}")
|
|
|
|
|
|
public AjaxResult remove(@PathVariable Long appId) {
|
|
|
|
|
|
return toAjax(applicationService.deleteApplicationById(appId));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置应用密钥
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:resetSecret')")
|
|
|
|
|
|
@Log(title = "应用管理-重置密钥", businessType = BusinessType.UPDATE)
|
|
|
|
|
|
@PutMapping("/resetSecret/{appId}")
|
|
|
|
|
|
public AjaxResult resetSecret(@PathVariable Long appId) {
|
|
|
|
|
|
String newSecret = applicationService.resetAppSecret(appId);
|
|
|
|
|
|
return success("重置成功", Map.of("appSecret", newSecret));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取API接口列表(用于授权选择)
|
|
|
|
|
|
*/
|
|
|
|
|
|
@PreAuthorize("hasPermission('system:application:list')")
|
|
|
|
|
|
@GetMapping("/apiList")
|
|
|
|
|
|
public AjaxResult apiList() {
|
|
|
|
|
|
List<ApiDict> list = applicationService.selectApiDictList();
|
|
|
|
|
|
return success(list);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 开放API认证拦截器
|
|
|
|
|
|
|
|
|
|
|
|
### 8.1 ApiAuthInterceptor.java
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.open.interceptor;
|
|
|
|
|
|
|
|
|
|
|
|
import cn.hutool.crypto.digest.DigestUtil;
|
|
|
|
|
|
import com.pangu.admin.domain.entity.Application;
|
|
|
|
|
|
import com.pangu.admin.service.IApplicationService;
|
|
|
|
|
|
import com.pangu.common.core.exception.ServiceException;
|
|
|
|
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
|
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
import org.springframework.web.servlet.HandlerInterceptor;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
import java.util.TreeMap;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 开放API认证拦截器
|
|
|
|
|
|
* 用于验证第三方应用的签名和权限
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Slf4j
|
|
|
|
|
|
@Component
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class ApiAuthInterceptor implements HandlerInterceptor {
|
|
|
|
|
|
|
|
|
|
|
|
private final IApplicationService applicationService;
|
|
|
|
|
|
|
|
|
|
|
|
/** 时间戳有效期(5分钟) */
|
|
|
|
|
|
private static final long TIMESTAMP_EXPIRE_MS = 5 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
|
|
|
|
|
// 获取请求头
|
|
|
|
|
|
String appId = request.getHeader("X-App-Id");
|
|
|
|
|
|
String timestamp = request.getHeader("X-Timestamp");
|
|
|
|
|
|
String sign = request.getHeader("X-Sign");
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 验证必要参数
|
|
|
|
|
|
if (appId == null || timestamp == null || sign == null) {
|
|
|
|
|
|
throw new ServiceException("缺少认证参数");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 验证时间戳(防重放攻击)
|
|
|
|
|
|
long reqTime;
|
|
|
|
|
|
try {
|
|
|
|
|
|
reqTime = Long.parseLong(timestamp);
|
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
|
throw new ServiceException("时间戳格式错误");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Math.abs(System.currentTimeMillis() - reqTime) > TIMESTAMP_EXPIRE_MS) {
|
|
|
|
|
|
throw new ServiceException("请求已过期");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 查询应用信息
|
|
|
|
|
|
Application app = applicationService.selectByAppCode(appId);
|
|
|
|
|
|
if (app == null) {
|
|
|
|
|
|
throw new ServiceException("应用不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ("1".equals(app.getStatus())) {
|
|
|
|
|
|
throw new ServiceException("应用已停用");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 验证签名
|
|
|
|
|
|
String expectedSign = generateSign(request, app.getAppSecret());
|
|
|
|
|
|
if (!expectedSign.equalsIgnoreCase(sign)) {
|
|
|
|
|
|
log.warn("签名验证失败,appId={}, expectedSign={}, actualSign={}", appId, expectedSign, sign);
|
|
|
|
|
|
throw new ServiceException("签名验证失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 检查接口权限
|
|
|
|
|
|
String apiPath = request.getRequestURI();
|
|
|
|
|
|
if (!applicationService.checkApiPermission(appId, apiPath)) {
|
|
|
|
|
|
throw new ServiceException("无权访问该接口");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.info("API认证通过,appId={}, apiPath={}", appId, apiPath);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成签名
|
|
|
|
|
|
* 规则:将请求参数按ASCII排序后拼接,末尾追加appSecret,MD5加密
|
|
|
|
|
|
*/
|
|
|
|
|
|
private String generateSign(HttpServletRequest request, String appSecret) {
|
|
|
|
|
|
// 按参数名ASCII排序
|
|
|
|
|
|
Map<String, String> params = new TreeMap<>();
|
|
|
|
|
|
request.getParameterMap().forEach((key, values) -> {
|
|
|
|
|
|
if (values != null && values.length > 0) {
|
|
|
|
|
|
params.put(key, values[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 拼接字符串
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
|
params.forEach((key, value) -> {
|
|
|
|
|
|
if (sb.length() > 0) {
|
|
|
|
|
|
sb.append("&");
|
|
|
|
|
|
}
|
|
|
|
|
|
sb.append(key).append("=").append(value);
|
|
|
|
|
|
});
|
|
|
|
|
|
sb.append("&appSecret=").append(appSecret);
|
|
|
|
|
|
|
|
|
|
|
|
// MD5加密(大写)
|
|
|
|
|
|
return DigestUtil.md5Hex(sb.toString()).toUpperCase();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8.2 OpenApiConfig.java(配置类)
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.open.config;
|
|
|
|
|
|
|
|
|
|
|
|
import com.pangu.open.interceptor.ApiAuthInterceptor;
|
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
import org.springframework.context.annotation.Configuration;
|
|
|
|
|
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|
|
|
|
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 开放API配置
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Configuration
|
|
|
|
|
|
@RequiredArgsConstructor
|
|
|
|
|
|
public class OpenApiConfig implements WebMvcConfigurer {
|
|
|
|
|
|
|
|
|
|
|
|
private final ApiAuthInterceptor apiAuthInterceptor;
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void addInterceptors(InterceptorRegistry registry) {
|
|
|
|
|
|
// 对/open/**路径的请求进行认证
|
|
|
|
|
|
registry.addInterceptor(apiAuthInterceptor)
|
|
|
|
|
|
.addPathPatterns("/open/**");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 缓存设计
|
|
|
|
|
|
|
|
|
|
|
|
### 9.1 缓存策略
|
|
|
|
|
|
|
|
|
|
|
|
| 缓存KEY | 数据类型 | 过期时间 | 说明 |
|
|
|
|
|
|
|---------|---------|---------|------|
|
|
|
|
|
|
| `app:info:{appCode}` | Application | 30分钟 | 应用基本信息 |
|
|
|
|
|
|
| `app:apis:{appCode}` | Set<String> | 30分钟 | 应用授权的接口路径 |
|
|
|
|
|
|
|
|
|
|
|
|
### 9.2 缓存操作
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
// 写入缓存
|
|
|
|
|
|
redisService.setCacheObject(CACHE_APP_INFO + appCode, app, 30, TimeUnit.MINUTES);
|
|
|
|
|
|
|
|
|
|
|
|
// 读取缓存
|
|
|
|
|
|
Application app = redisService.getCacheObject(CACHE_APP_INFO + appCode);
|
|
|
|
|
|
|
|
|
|
|
|
// 删除缓存
|
|
|
|
|
|
redisService.deleteObject(CACHE_APP_INFO + appCode);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 9.3 缓存更新时机
|
|
|
|
|
|
|
|
|
|
|
|
| 操作 | 缓存处理 |
|
|
|
|
|
|
|------|---------|
|
|
|
|
|
|
| 新增应用 | 无需处理 |
|
|
|
|
|
|
| 修改应用 | 删除该应用的缓存 |
|
|
|
|
|
|
| 删除应用 | 删除该应用的缓存 |
|
|
|
|
|
|
| 重置密钥 | 删除该应用的缓存 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 单元测试
|
|
|
|
|
|
|
|
|
|
|
|
### 10.1 ApplicationServiceTest.java
|
|
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
|
package com.pangu.admin.service;
|
|
|
|
|
|
|
|
|
|
|
|
import com.pangu.admin.domain.dto.ApplicationDTO;
|
|
|
|
|
|
import com.pangu.admin.domain.vo.ApplicationVO;
|
|
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
|
import org.springframework.boot.test.context.SpringBootTest;
|
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用管理服务单元测试
|
2026-01-31 23:14:11 +08:00
|
|
|
|
* @author pangu
|
2026-01-31 17:55:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@SpringBootTest
|
|
|
|
|
|
@Transactional
|
|
|
|
|
|
class ApplicationServiceTest {
|
|
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
|
private IApplicationService applicationService;
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testInsertApplication() {
|
|
|
|
|
|
ApplicationDTO dto = new ApplicationDTO();
|
|
|
|
|
|
dto.setAppName("测试应用");
|
|
|
|
|
|
dto.setAppDesc("测试描述");
|
|
|
|
|
|
dto.setStatus("0");
|
|
|
|
|
|
dto.setApiCodes(Arrays.asList("SCHOOL_LIST", "GRADE_LIST"));
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, String> result = applicationService.insertApplication(dto);
|
|
|
|
|
|
|
|
|
|
|
|
assertNotNull(result.get("appCode"));
|
|
|
|
|
|
assertNotNull(result.get("appSecret"));
|
|
|
|
|
|
assertTrue(result.get("appCode").startsWith("YY"));
|
|
|
|
|
|
assertEquals(32, result.get("appSecret").length());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testSelectApplicationList() {
|
|
|
|
|
|
ApplicationDTO dto = new ApplicationDTO();
|
|
|
|
|
|
dto.setStatus("0");
|
|
|
|
|
|
|
|
|
|
|
|
List<ApplicationVO> list = applicationService.selectApplicationList(dto);
|
|
|
|
|
|
|
|
|
|
|
|
assertNotNull(list);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testResetAppSecret() {
|
|
|
|
|
|
// 先创建应用
|
|
|
|
|
|
ApplicationDTO dto = new ApplicationDTO();
|
|
|
|
|
|
dto.setAppName("重置密钥测试");
|
|
|
|
|
|
dto.setStatus("0");
|
|
|
|
|
|
Map<String, String> result = applicationService.insertApplication(dto);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取应用ID
|
|
|
|
|
|
ApplicationDTO query = new ApplicationDTO();
|
|
|
|
|
|
query.setAppCode(result.get("appCode"));
|
|
|
|
|
|
List<ApplicationVO> list = applicationService.selectApplicationList(query);
|
|
|
|
|
|
Long appId = list.get(0).getAppId();
|
|
|
|
|
|
|
|
|
|
|
|
// 重置密钥
|
|
|
|
|
|
String oldSecret = result.get("appSecret");
|
|
|
|
|
|
String newSecret = applicationService.resetAppSecret(appId);
|
|
|
|
|
|
|
|
|
|
|
|
assertNotNull(newSecret);
|
|
|
|
|
|
assertEquals(32, newSecret.length());
|
|
|
|
|
|
assertNotEquals(oldSecret, newSecret);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
void testCheckApiPermission() {
|
|
|
|
|
|
// 先创建应用并授权
|
|
|
|
|
|
ApplicationDTO dto = new ApplicationDTO();
|
|
|
|
|
|
dto.setAppName("权限测试应用");
|
|
|
|
|
|
dto.setStatus("0");
|
|
|
|
|
|
dto.setApiCodes(Arrays.asList("SCHOOL_LIST"));
|
|
|
|
|
|
Map<String, String> result = applicationService.insertApplication(dto);
|
|
|
|
|
|
|
|
|
|
|
|
String appCode = result.get("appCode");
|
|
|
|
|
|
|
|
|
|
|
|
// 检查有权限的接口
|
|
|
|
|
|
assertTrue(applicationService.checkApiPermission(appCode, "/open/school/list"));
|
|
|
|
|
|
|
|
|
|
|
|
// 检查无权限的接口
|
|
|
|
|
|
assertFalse(applicationService.checkApiPermission(appCode, "/open/student/list"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 开发检查清单
|
|
|
|
|
|
|
|
|
|
|
|
### 11.1 开发前检查
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 阅读需求文档和技术方案
|
|
|
|
|
|
- [ ] 确认数据库表结构
|
|
|
|
|
|
- [ ] 确认接口设计
|
|
|
|
|
|
|
|
|
|
|
|
### 11.2 开发中检查
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 数据库表创建完成
|
|
|
|
|
|
- [ ] 初始化数据插入完成
|
|
|
|
|
|
- [ ] 实体类开发完成
|
|
|
|
|
|
- [ ] Mapper层开发完成
|
|
|
|
|
|
- [ ] Service层开发完成
|
|
|
|
|
|
- [ ] Controller层开发完成
|
|
|
|
|
|
- [ ] 开放API拦截器开发完成
|
|
|
|
|
|
- [ ] 缓存处理完成
|
|
|
|
|
|
- [ ] 单元测试编写完成
|
|
|
|
|
|
|
|
|
|
|
|
### 11.3 开发后检查
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 代码格式化
|
|
|
|
|
|
- [ ] 删除无用代码
|
|
|
|
|
|
- [ ] 添加必要注释
|
|
|
|
|
|
- [ ] 单元测试通过
|
|
|
|
|
|
- [ ] 接口文档更新
|
|
|
|
|
|
- [ ] Postman接口测试通过
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*文档结束*
|