1525 lines
42 KiB
Markdown
1525 lines
42 KiB
Markdown
# 盘古用户平台 - 应用管理模块后端技术方案
|
||
|
||
---
|
||
|
||
| 文档信息 | 内容 |
|
||
|---------|------|
|
||
| **文档版本** | V1.0 |
|
||
| **模块名称** | 应用管理模块 - 后端 |
|
||
| **编写团队 | pangu |
|
||
| **创建日期** | 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;
|
||
|
||
/**
|
||
* 应用实体
|
||
* @author pangu
|
||
*/
|
||
@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;
|
||
|
||
/**
|
||
* 应用接口授权实体
|
||
* @author pangu
|
||
*/
|
||
@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接口字典实体
|
||
* @author pangu
|
||
*/
|
||
@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;
|
||
|
||
/**
|
||
* 应用传输对象
|
||
* @author pangu
|
||
*/
|
||
@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;
|
||
|
||
/**
|
||
* 应用视图对象
|
||
* @author pangu
|
||
*/
|
||
@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接口
|
||
* @author pangu
|
||
*/
|
||
@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接口
|
||
* @author pangu
|
||
*/
|
||
@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接口
|
||
* @author pangu
|
||
*/
|
||
@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;
|
||
|
||
/**
|
||
* 应用管理服务接口
|
||
* @author pangu
|
||
*/
|
||
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;
|
||
|
||
/**
|
||
* 应用管理服务实现
|
||
* @author pangu
|
||
*/
|
||
@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;
|
||
|
||
/**
|
||
* 应用管理控制器
|
||
* @author pangu
|
||
*/
|
||
@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认证拦截器
|
||
* 用于验证第三方应用的签名和权限
|
||
* @author pangu
|
||
*/
|
||
@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配置
|
||
* @author pangu
|
||
*/
|
||
@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.*;
|
||
|
||
/**
|
||
* 应用管理服务单元测试
|
||
* @author pangu
|
||
*/
|
||
@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接口测试通过
|
||
|
||
---
|
||
|
||
*文档结束*
|