首次提交后端接口

This commit is contained in:
super
2026-01-27 20:48:47 +08:00
commit 40db55e85d
177 changed files with 18905 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
*.class
*.log
*.ctxt
.mtj.tmp/
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
.classpath
.project
.settings
.target
.idea
*.iml

156
README.md Normal file
View File

@@ -0,0 +1,156 @@
# Nanxiislet Admin Backend
南溪小岛管理后台 Spring Boot 后端服务
## 技术栈
- **框架**: Spring Boot 3.3.4
- **Java 版本**: JDK 21
- **数据库**: MySQL 8.0+
- **缓存**: Redis
- **ORM**: MyBatis-Plus 3.5.7
- **认证**: Sa-Token 1.39.0
- **API 文档**: Knife4j 4.5.0 (OpenAPI 3)
- **工具库**: Hutool、Lombok、EasyExcel
## 项目结构
```
src/main/java/com/nanxiislet/admin/
├── common/ # 公共模块
│ ├── base/ # 基础类
│ ├── exception/ # 异常处理
│ └── result/ # 响应结果
├── config/ # 配置类
├── controller/ # 控制器
├── dto/ # 数据传输对象
│ ├── auth/ # 认证相关
│ └── query/ # 查询条件
├── entity/ # 实体类
├── mapper/ # MyBatis Mapper
└── service/ # 服务层
└── impl/ # 服务实现
```
## 功能模块
### 认证管理
- 用户登录/登出
- 验证码
- 获取当前用户信息
### 用户管理
- 用户 CRUD
- 密码重置/修改
- 用户状态管理
### 平台管理
- 项目管理
- 服务器管理
- 域名管理
### 财务管理
- 收入管理
- 支出管理
- 预算管理
- 报销管理
- 发票管理
- 结算管理
### 仪表盘
- 统计数据
- 快速概览
## 快速开始
### 1. 环境要求
- JDK 21+
- Maven 3.8+
- MySQL 8.0+
- Redis 6.0+
### 2. 数据库初始化
执行 `src/main/resources/db/init.sql` 脚本创建数据库和表。
```bash
mysql -h 192.168.9.100 -u root -p < src/main/resources/db/init.sql
```
### 3. 配置修改
编辑 `src/main/resources/application.yml`,修改数据库和 Redis 连接信息。
### 4. 启动项目
```bash
mvn spring-boot:run
```
或打包后运行:
```bash
mvn package -DskipTests
java -jar target/nanxiislet-admin-1.0.0-SNAPSHOT.jar
```
### 5. 访问 API 文档
启动后访问: http://localhost:8080/api/doc.html
## 默认账户
- 用户名: `admin`
- 密码: `admin123`
## API 说明
### 认证接口
| 方法 | 路径 | 说明 |
| ---- | --------------- | ---------------- |
| GET | /auth/captcha | 获取验证码 |
| POST | /auth/login | 用户登录 |
| POST | /auth/logout | 用户登出 |
| GET | /auth/user-info | 获取当前用户信息 |
### 用户管理
| 方法 | 路径 | 说明 |
| ------ | -------------------------------- | -------- |
| GET | /system/user/list | 用户列表 |
| GET | /system/user/{id} | 用户详情 |
| POST | /system/user | 新增用户 |
| PUT | /system/user/{id} | 更新用户 |
| DELETE | /system/user/{id} | 删除用户 |
| POST | /system/user/{id}/reset-password | 重置密码 |
| POST | /system/user/change-password | 修改密码 |
### 请求头
需要登录的接口请在请求头中携带 Token
```
Authorization: Bearer {token}
```
## 响应格式
```json
{
"code": 200,
"message": "操作成功",
"data": {},
"timestamp": 1704540000000
}
```
## License
MIT License

View File

@@ -0,0 +1,142 @@
# 域名管理集成指南
## 1Panel 接口文档
1Panel API 文档地址http://47.109.57.58:42588/1panel/swagger/index.html
## 架构设计
### 数据流
```
前端(域名管理页面)
后端(PlatformDomainController)
后端(PlatformDomainService)
↓ 存储/读取
数据库(platform_domain表)
↓ 部署时调用
1Panel API(创建网站/申请证书/配置HTTPS)
```
### 核心流程
1. **域名创建**:用户在前端新建域名 → 后端存储到数据库(状态为 `pending`,部署状态为 `not_deployed`
2. **项目关联**:用户在项目管理中绑定域名 → 自动生成项目地址和部署路径
3. **项目部署**(点击"部署"按钮时触发):
- 检查/创建 1Panel 网站
- 如果启用 HTTPS检查/申请 SSL 证书
- 配置 HTTPS强制跳转
- 更新域名和项目状态
## 后端接口
### 域名管理 API
| 方法 | 路径 | 描述 |
| ------ | ------------------------------- | ---------------------- |
| GET | /platform/domain/list | 分页获取域名列表 |
| GET | /platform/domain/all | 获取所有域名 |
| GET | /platform/domain/{id} | 获取域名详情 |
| POST | /platform/domain | 新增域名 |
| PUT | /platform/domain/{id} | 更新域名 |
| DELETE | /platform/domain/{id} | 删除域名 |
| GET | /platform/domain/stats | 域名统计信息 |
| POST | /platform/domain/deploy | 部署域名到 1Panel |
| POST | /platform/domain/{id}/check-dns | 检查 DNS 解析状态 |
| POST | /platform/domain/{id}/sync | 从 1Panel 同步域名信息 |
### 部署请求参数
```json
{
"domainId": 1,
"enableHttps": true,
"acmeAccountId": 1,
"dnsAccountId": 1,
"createIfNotExist": true
}
```
### 部署结果
```json
{
"success": true,
"message": "部署流程完成",
"websiteId": 123,
"sslCertificateId": 456,
"steps": [
{
"step": "检查网站",
"status": "success",
"message": "网站已存在ID: 123"
},
{
"step": "检查SSL证书",
"status": "success",
"message": "证书已存在ID: 456"
},
{ "step": "配置HTTPS", "status": "success", "message": "HTTPS配置成功" }
]
}
```
## 数据库表结构
### platform_domain 表
新增字段V6\_\_domain_enhance.sql
- `panel_website_id` - 1Panel 网站 ID
- `panel_ssl_id` - 1Panel 证书 ID
- `site_path` - 网站目录路径
- `alias` - 网站别名
- `deploy_status` - 部署状态not_deployed/deploying/deployed/failed
- `last_deploy_time` - 最后部署时间
- `last_deploy_message` - 最后部署消息
## 前端集成
### API 文件
`src/api/domain.ts` - 域名管理 API 封装
### 使用示例
```typescript
import {
getDomainList,
createDomain,
deployDomain,
checkDomainDns,
} from "@/api/domain";
// 获取域名列表
const { records, total } = await getDomainList({ page: 1, pageSize: 10 });
// 创建域名
await createDomain({
domain: "example.com",
serverId: 1,
port: 80,
});
// 部署域名
const result = await deployDomain({
domainId: 1,
enableHttps: true,
acmeAccountId: 1,
dnsAccountId: 1,
});
```
## 注意事项
1. 部署操作是异步的,可能需要等待证书申请完成(最长 120 秒)
2. 首次部署会自动创建网站目录,路径为 `/opt/1panel/www/sites/{域名}/index`
3. SSL 证书申请需要配置 Acme 账户和 DNS 账户(在 1Panel 中配置)
4. 部署完成后,项目的"部署"按钮会自动同步状态

49
init-database.bat Normal file
View File

@@ -0,0 +1,49 @@
@echo off
REM 数据库初始化脚本
REM 请先配置好MySQL可执行文件路径
set MYSQL_PATH=mysql
set HOST=192.168.9.100
set PORT=3306
set USER=root
set PASSWORD=mysql_23fw7s
set DATABASE=nanxiislet
echo ================================
echo 南溪小岛管理后台 - 数据库初始化
echo ================================
echo.
echo 数据库地址: %HOST%:%PORT%
echo 数据库名称: %DATABASE%
echo.
REM 先创建数据库
echo 正在创建数据库...
%MYSQL_PATH% -h%HOST% -P%PORT% -u%USER% -p%PASSWORD% -e "CREATE DATABASE IF NOT EXISTS %DATABASE% DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
if %ERRORLEVEL% NEQ 0 (
echo 创建数据库失败请检查MySQL连接配置。
pause
exit /b 1
)
REM 导入初始化脚本
echo 正在导入数据表...
%MYSQL_PATH% -h%HOST% -P%PORT% -u%USER% -p%PASSWORD% %DATABASE% < src\main\resources\db\init.sql
if %ERRORLEVEL% NEQ 0 (
echo 导入数据失败!
pause
exit /b 1
)
echo.
echo ================================
echo 数据库初始化成功!
echo ================================
echo.
echo 默认管理员账号:
echo 用户名: admin
echo 密码: admin123
echo.
pause

Binary file not shown.

223
pom.xml Normal file
View File

@@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nanxiislet</groupId>
<artifactId>nanxiislet-admin</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>NanxiIslet Admin Backend</name>
<description>南溪小岛管理后台 - Spring Boot后端服务</description>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.3.4</spring-boot.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<jwt.version>0.12.6</jwt.version>
<hutool.version>5.8.31</hutool.version>
<knife4j.version>4.5.0</knife4j.version>
<sa-token.version>1.39.0</sa-token.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.23</version>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Knife4j OpenAPI Documentation -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- Hutool 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Security Crypto (for BCryptPasswordEncoder) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- File Upload -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.17.0</version>
</dependency>
<!-- Excel 处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>4.0.3</version>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.3</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>aliyun</id>
<name>Aliyun Maven Repository</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,30 @@
package com.nanxiislet.admin;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 南溪小岛管理后台 - 启动类
*
* @author NanxiIslet
* @since 2024-01-06
*/
@SpringBootApplication
@MapperScan("com.nanxiislet.admin.mapper")
@EnableTransactionManagement
@EnableAsync
@EnableScheduling
public class NanxiisletAdminApplication {
public static void main(String[] args) {
SpringApplication.run(NanxiisletAdminApplication.class, args);
System.out.println("====================================================");
System.out.println(" 南溪小岛管理后台服务启动成功!");
System.out.println(" 接口文档地址: http://localhost:8080/api/doc.html");
System.out.println("====================================================");
}
}

View File

@@ -0,0 +1,47 @@
package com.nanxiislet.admin.common.base;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 实体基类
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
public class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@Schema(description = "创建人ID")
@TableField(fill = FieldFill.INSERT)
private Long createdBy;
@Schema(description = "更新人ID")
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updatedBy;
@Schema(description = "删除标记 0-未删除 1-已删除")
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
}

View File

@@ -0,0 +1,42 @@
package com.nanxiislet.admin.common.base;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 分页查询基类
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
public class BasePageQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "当前页码", example = "1")
private Integer page = 1;
@Schema(description = "每页条数", example = "10")
private Integer pageSize = 10;
@Schema(description = "搜索关键词")
private String keyword;
@Schema(description = "排序字段")
private String orderBy;
@Schema(description = "排序方式 asc/desc")
private String order = "desc";
/**
* 获取偏移量
*/
public int getOffset() {
return (page - 1) * pageSize;
}
}

View File

@@ -0,0 +1,46 @@
package com.nanxiislet.admin.common.exception;
import com.nanxiislet.admin.common.result.ResultCode;
import lombok.Getter;
import java.io.Serial;
/**
* 业务异常
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Getter
public class BusinessException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private final int code;
private final String message;
public BusinessException(String message) {
super(message);
this.code = ResultCode.FAILED.getCode();
this.message = message;
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
this.message = resultCode.getMessage();
}
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.code = resultCode.getCode();
this.message = message;
}
}

View File

@@ -0,0 +1,148 @@
package com.nanxiislet.admin.common.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.stream.Collectors;
/**
* 全局异常处理器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK)
public R<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.warn("业务异常: {} - {}", request.getRequestURI(), e.getMessage());
return R.fail(e.getCode(), e.getMessage());
}
/**
* 未登录异常
*/
@ExceptionHandler(NotLoginException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
log.warn("未登录访问: {} - {}", request.getRequestURI(), e.getMessage());
return R.fail(ResultCode.UNAUTHORIZED);
}
/**
* 无权限异常
*/
@ExceptionHandler(NotPermissionException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
log.warn("无权限访问: {} - {}", request.getRequestURI(), e.getMessage());
return R.fail(ResultCode.FORBIDDEN);
}
/**
* 无角色异常
*/
@ExceptionHandler(NotRoleException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R<Void> handleNotRoleException(NotRoleException e, HttpServletRequest request) {
log.warn("无角色访问: {} - {}", request.getRequestURI(), e.getMessage());
return R.fail(ResultCode.FORBIDDEN);
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R<Void> handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.warn("参数校验失败: {}", message);
return R.fail(ResultCode.VALIDATE_FAILED, message);
}
/**
* 绑定异常
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R<Void> handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.warn("参数绑定失败: {}", message);
return R.fail(ResultCode.VALIDATE_FAILED, message);
}
/**
* 缺少请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R<Void> handleMissingParameterException(MissingServletRequestParameterException e) {
log.warn("缺少请求参数: {}", e.getParameterName());
return R.fail(ResultCode.BAD_REQUEST, "缺少请求参数: " + e.getParameterName());
}
/**
* 请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public R<Void> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.warn("不支持的请求方法: {}", e.getMethod());
return R.fail(ResultCode.METHOD_NOT_ALLOWED);
}
/**
* 404异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public R<Void> handleNoHandlerFoundException(NoHandlerFoundException e) {
log.warn("资源不存在: {}", e.getRequestURL());
return R.fail(ResultCode.NOT_FOUND);
}
/**
* 数据库唯一键冲突异常
*/
@ExceptionHandler(org.springframework.dao.DuplicateKeyException.class)
@ResponseStatus(HttpStatus.OK)
public R<Void> handleDuplicateKeyException(org.springframework.dao.DuplicateKeyException e) {
log.warn("数据重复: {}", e.getMessage());
return R.fail(ResultCode.VALIDATE_FAILED, "数据已存在(编码或名称重复),即使是已删除的历史数据也不能重复,请更换后重试");
}
/**
* 其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<Void> handleException(Exception e, HttpServletRequest request) {
log.error("系统异常: {} - {}", request.getRequestURI(), e.getMessage(), e);
return R.fail(ResultCode.INTERNAL_ERROR, "系统繁忙,请稍后重试");
}
}

View File

@@ -0,0 +1,63 @@
package com.nanxiislet.admin.common.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@Schema(description = "分页结果")
public class PageResult<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "数据列表")
private List<T> records;
@Schema(description = "总条数")
private long total;
@Schema(description = "当前页码")
private long page;
@Schema(description = "每页条数")
private long pageSize;
@Schema(description = "总页数")
private long totalPages;
public PageResult() {
}
public PageResult(List<T> records, long total, long page, long pageSize) {
this.records = records;
this.total = total;
this.page = page;
this.pageSize = pageSize;
this.totalPages = (total + pageSize - 1) / pageSize;
}
/**
* 创建分页结果
*/
public static <T> PageResult<T> of(List<T> records, long total, long page, long pageSize) {
return new PageResult<>(records, total, page, pageSize);
}
/**
* 空分页结果
*/
public static <T> PageResult<T> empty() {
return new PageResult<>(List.of(), 0, 1, 10);
}
}

View File

@@ -0,0 +1,107 @@
package com.nanxiislet.admin.common.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 统一响应结果
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@Schema(description = "统一响应结果")
public class R<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "响应码", example = "200")
private int code;
@Schema(description = "响应消息", example = "操作成功")
private String message;
@Schema(description = "响应数据")
private T data;
@Schema(description = "时间戳")
private long timestamp;
public R() {
this.timestamp = System.currentTimeMillis();
}
public R(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
/**
* 成功
*/
public static <T> R<T> ok() {
return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
}
/**
* 成功
*/
public static <T> R<T> ok(T data) {
return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功
*/
public static <T> R<T> ok(String message, T data) {
return new R<>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败
*/
public static <T> R<T> fail() {
return new R<>(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), null);
}
/**
* 失败
*/
public static <T> R<T> fail(String message) {
return new R<>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败
*/
public static <T> R<T> fail(int code, String message) {
return new R<>(code, message, null);
}
/**
* 失败
*/
public static <T> R<T> fail(ResultCode resultCode) {
return new R<>(resultCode.getCode(), resultCode.getMessage(), null);
}
/**
* 失败
*/
public static <T> R<T> fail(ResultCode resultCode, String message) {
return new R<>(resultCode.getCode(), message, null);
}
/**
* 判断是否成功
*/
public boolean isSuccess() {
return this.code == ResultCode.SUCCESS.getCode();
}
}

View File

@@ -0,0 +1,68 @@
package com.nanxiislet.admin.common.result;
import lombok.Getter;
/**
* 响应码枚举
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Getter
public enum ResultCode {
// 成功
SUCCESS(200, "操作成功"),
// 客户端错误 4xx
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未登录或登录已过期"),
FORBIDDEN(403, "没有操作权限"),
NOT_FOUND(404, "资源不存在"),
METHOD_NOT_ALLOWED(405, "请求方法不支持"),
CONFLICT(409, "资源冲突"),
VALIDATE_FAILED(422, "参数校验失败"),
TOO_MANY_REQUESTS(429, "请求过于频繁"),
// 服务端错误 5xx
FAILED(500, "操作失败"),
INTERNAL_ERROR(500, "服务器内部错误"),
SERVICE_UNAVAILABLE(503, "服务暂不可用"),
// 业务错误 1xxx
USER_NOT_EXIST(1001, "用户不存在"),
USER_PASSWORD_ERROR(1002, "用户名或密码错误"),
USER_DISABLED(1003, "用户已被禁用"),
USER_EXISTS(1004, "用户已存在"),
CAPTCHA_ERROR(1005, "验证码错误"),
CAPTCHA_EXPIRED(1006, "验证码已过期"),
TOKEN_INVALID(1007, "Token无效"),
TOKEN_EXPIRED(1008, "Token已过期"),
// 数据相关 2xxx
DATA_NOT_EXIST(2001, "数据不存在"),
DATA_ALREADY_EXIST(2002, "数据已存在"),
DATA_SAVE_ERROR(2003, "数据保存失败"),
DATA_UPDATE_ERROR(2004, "数据更新失败"),
DATA_DELETE_ERROR(2005, "数据删除失败"),
DATA_IMPORT_ERROR(2006, "数据导入失败"),
// 文件相关 3xxx
FILE_NOT_FOUND(3001, "文件不存在"),
FILE_UPLOAD_ERROR(3002, "文件上传失败"),
FILE_TYPE_NOT_ALLOWED(3003, "不支持的文件类型"),
FILE_SIZE_EXCEEDED(3004, "文件大小超限"),
// 审批相关 4xxx
APPROVAL_NOT_FOUND(4001, "审批流程不存在"),
APPROVAL_ALREADY_PROCESSED(4002, "审批已处理"),
APPROVAL_NO_PERMISSION(4003, "无审批权限");
private final int code;
private final String message;
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
}

View File

@@ -0,0 +1,55 @@
package com.nanxiislet.admin.config;
import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* 验证码配置
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Configuration
public class CaptchaConfig {
@Bean
public Producer captchaProducer() {
DefaultKaptcha kaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 验证码宽度
properties.setProperty("kaptcha.image.width", "150");
// 验证码高度
properties.setProperty("kaptcha.image.height", "50");
// 验证码字符长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
// 验证码字符集
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
// 验证码字体大小
properties.setProperty("kaptcha.textproducer.font.size", "38");
// 验证码字体
properties.setProperty("kaptcha.textproducer.font.names", "Arial,Courier");
// 验证码字体颜色
properties.setProperty("kaptcha.textproducer.font.color", "black");
// 验证码背景颜色渐变开始
properties.setProperty("kaptcha.background.clear.from", "lightGray");
// 验证码背景颜色渐变结束
properties.setProperty("kaptcha.background.clear.to", "white");
// 验证码干扰
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
// 验证码干扰颜色
properties.setProperty("kaptcha.noise.color", "gray");
// 边框
properties.setProperty("kaptcha.border", "yes");
properties.setProperty("kaptcha.border.color", "105,179,90");
properties.setProperty("kaptcha.border.thickness", "1");
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}

View File

@@ -0,0 +1,77 @@
package com.nanxiislet.admin.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import jakarta.annotation.Resource;
import java.nio.charset.StandardCharsets;
/**
* 数据库初始化配置
* 在应用启动时检查并初始化数据库
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Slf4j
@Component
public class DatabaseInitializer implements CommandLineRunner {
@Resource
private JdbcTemplate jdbcTemplate;
@Value("${nanxiislet.db.init:false}")
private boolean initEnabled;
@Override
public void run(String... args) throws Exception {
if (!initEnabled) {
log.info("数据库初始化已禁用,跳过初始化");
return;
}
try {
// 检查表是否存在
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'sys_user'",
Integer.class
);
if (count != null && count > 0) {
log.info("数据库表已存在,跳过初始化");
return;
}
log.info("开始初始化数据库...");
// 读取SQL文件
ClassPathResource resource = new ClassPathResource("db/init.sql");
String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
// 分割并执行SQL语句
String[] statements = sql.split(";");
int executed = 0;
for (String statement : statements) {
String trimmed = statement.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("--")) {
try {
jdbcTemplate.execute(trimmed);
executed++;
} catch (Exception e) {
log.warn("执行SQL失败: {}", e.getMessage());
}
}
}
log.info("数据库初始化完成,执行了 {} 条SQL语句", executed);
} catch (Exception e) {
log.error("数据库初始化失败: {}", e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,88 @@
package com.nanxiislet.admin.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis-Plus 配置
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus 插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(500L);
paginationInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInterceptor);
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 防止全表更新删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
/**
* 自动填充处理器
*/
@Component
public static class AutoFillMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, now);
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, now);
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
// 获取当前登录用户ID
Long userId = getCurrentUserId();
if (userId != null) {
this.strictInsertFill(metaObject, "createdBy", Long.class, userId);
this.strictInsertFill(metaObject, "updatedBy", Long.class, userId);
}
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
Long userId = getCurrentUserId();
if (userId != null) {
this.strictUpdateFill(metaObject, "updatedBy", Long.class, userId);
}
}
/**
* 获取当前登录用户ID
*/
private Long getCurrentUserId() {
try {
Object loginId = cn.dev33.satoken.stp.StpUtil.getLoginIdDefaultNull();
if (loginId != null) {
return Long.parseLong(loginId.toString());
}
} catch (Exception ignored) {
}
return null;
}
}
}

View File

@@ -0,0 +1,53 @@
package com.nanxiislet.admin.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis 配置
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// JSON序列化器
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key使用String序列化
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// value使用JSON序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,87 @@
package com.nanxiislet.admin.config;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import com.nanxiislet.admin.entity.SysMenu;
import com.nanxiislet.admin.entity.SysUser;
import com.nanxiislet.admin.service.AuthService;
import com.nanxiislet.admin.service.SysMenuService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Sa-Token 权限认证接口实现
* 用于获取当前用户的角色和权限列表
*
* @author NanxiIslet
* @since 2024-01-08
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private AuthService authService;
@Resource
private SysMenuService menuService;
/**
* 获取用户权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 获取用户信息
Long userId = Long.parseLong(loginId.toString());
SysUser user = authService.getById(userId);
if (user == null) {
return new ArrayList<>();
}
String roleCode = user.getRole();
// 超级管理员拥有所有权限
if ("super_admin".equals(roleCode)) {
return List.of("*");
}
// 根据角色获取菜单权限
List<SysMenu> menus = menuService.getMenusByRoleCode(roleCode);
if (menus == null || menus.isEmpty()) {
return new ArrayList<>();
}
return menus.stream()
.map(SysMenu::getCode)
.distinct()
.toList();
}
/**
* 获取用户角色列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
Long userId = Long.parseLong(loginId.toString());
SysUser user = authService.getById(userId);
if (user == null) {
return new ArrayList<>();
}
List<String> roles = new ArrayList<>();
String roleCode = user.getRole();
if (roleCode != null && !roleCode.isEmpty()) {
roles.add(roleCode);
// 超级管理员同时拥有 admin 角色
if ("super_admin".equals(roleCode)) {
roles.add("admin");
}
}
return roles;
}
}

View File

@@ -0,0 +1,46 @@
package com.nanxiislet.admin.config;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Swagger/OpenAPI 配置
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("南溪小岛管理后台 API")
.description("南溪小岛管理后台 RESTful API 文档")
.version("1.0.0")
.contact(new Contact()
.name("NanxiIslet")
.email("admin@nanxiislet.com"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT")))
.externalDocs(new ExternalDocumentation()
.description("项目文档")
.url("https://doc.nanxiislet.com"))
.addSecurityItem(new SecurityRequirement().addList("Authorization"))
.schemaRequirement("Authorization", new SecurityScheme()
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER));
}
}

View File

@@ -0,0 +1,84 @@
package com.nanxiislet.admin.config;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 跨域配置
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
/**
* 拦截器配置
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// Sa-Token 鉴权拦截器
registry.addInterceptor(new SaInterceptor(handle -> {
// OPTIONS 预检请求不检查登录
if ("OPTIONS".equalsIgnoreCase(SaHolder.getRequest().getMethod())) {
return;
}
StpUtil.checkLogin();
}))
.addPathPatterns("/**")
.excludePathPatterns(
// 登录认证相关
"/auth/login",
"/auth/captcha",
"/auth/logout",
// Swagger文档
"/doc.html",
"/swagger-ui.html",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/webjars/**",
// 静态资源
"/static/**",
"/favicon.ico",
// 健康检查
"/actuator/**",
"/health",
"/" // 根路径
);
}
/**
* 静态资源配置
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 上传文件访问
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:./uploads/");
// Swagger UI
registry.addResourceHandler("doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}

View File

@@ -0,0 +1,52 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.dto.auth.*;
import com.nanxiislet.admin.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 认证控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Slf4j
@RestController
@RequestMapping("/auth")
@Tag(name = "认证管理", description = "登录、登出、验证码等")
public class AuthController {
@Resource
private AuthService authService;
@GetMapping("/captcha")
@Operation(summary = "获取验证码")
public R<CaptchaResponse> getCaptcha() {
return R.ok(authService.generateCaptcha());
}
@PostMapping("/login")
@Operation(summary = "用户登录")
public R<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
return R.ok(authService.login(request));
}
@PostMapping("/logout")
@Operation(summary = "用户登出")
public R<Void> logout() {
authService.logout();
return R.ok();
}
@GetMapping("/user-info")
@Operation(summary = "获取当前用户信息")
public R<UserInfoVO> getCurrentUser() {
return R.ok(authService.getCurrentUser());
}
}

View File

@@ -0,0 +1,127 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.Data;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 仪表盘控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@RequestMapping("/dashboard")
@Tag(name = "仪表盘", description = "首页统计数据")
public class DashboardController {
@Resource
private FinanceIncomeService incomeService;
@Resource
private FinanceExpenseService expenseService;
@Resource
private FinanceReimbursementService reimbursementService;
@Resource
private PlatformProjectService projectService;
@Resource
private PlatformServerService serverService;
@Resource
private PlatformDomainService domainService;
@GetMapping("/stats")
@Operation(summary = "获取统计数据", description = "获取首页仪表盘统计数据")
public R<DashboardStats> getStats() {
DashboardStats stats = new DashboardStats();
// 项目统计
stats.setProjectCount(projectService.count());
// 服务器统计
stats.setServerCount(serverService.count());
// 域名统计
stats.setDomainCount(domainService.count());
// 财务统计(收入总额、支出总额、待报销金额)
// 这里简化处理,实际应该用聚合查询
stats.setIncomeCount(incomeService.count());
stats.setExpenseCount(expenseService.count());
stats.setReimbursementCount(reimbursementService.count());
return R.ok(stats);
}
@GetMapping("/overview")
@Operation(summary = "获取概览数据", description = "获取财务概览数据")
public R<Map<String, Object>> getOverview() {
Map<String, Object> overview = new HashMap<>();
// 统计数量
overview.put("totalProjects", projectService.count());
overview.put("totalServers", serverService.count());
overview.put("totalDomains", domainService.count());
overview.put("totalIncomes", incomeService.count());
overview.put("totalExpenses", expenseService.count());
overview.put("pendingReimbursements", reimbursementService.count());
return R.ok(overview);
}
@GetMapping("/quick-stats")
@Operation(summary = "快速统计", description = "获取快速统计卡片数据")
public R<List<QuickStat>> getQuickStats() {
List<QuickStat> stats = new ArrayList<>();
stats.add(new QuickStat("项目总数", projectService.count(), "project", "#1890ff"));
stats.add(new QuickStat("服务器", serverService.count(), "server", "#52c41a"));
stats.add(new QuickStat("域名", domainService.count(), "domain", "#722ed1"));
stats.add(new QuickStat("收入记录", incomeService.count(), "income", "#fa8c16"));
return R.ok(stats);
}
@Data
public static class DashboardStats {
private long projectCount;
private long serverCount;
private long domainCount;
private long incomeCount;
private long expenseCount;
private long reimbursementCount;
private BigDecimal totalIncome;
private BigDecimal totalExpense;
private BigDecimal pendingReimbursement;
}
@Data
public static class QuickStat {
private String title;
private long value;
private String icon;
private String color;
public QuickStat(String title, long value, String icon, String color) {
this.title = title;
this.value = value;
this.icon = icon;
this.color = color;
}
}
}

View File

@@ -0,0 +1,284 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.service.OnePanelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 数据库管理控制器
* 通过1Panel API管理数据库
*/
@RestController
@RequestMapping("/platform/database")
@Tag(name = "数据库管理", description = "通过1Panel管理数据库")
public class DatabaseController {
@Resource
private OnePanelService onePanelService;
/**
* 检查应用安装状态MySQL/PostgreSQL/Redis等
*/
@PostMapping("/app/check")
@Operation(summary = "检查应用安装状态")
public R<Map<String, Object>> checkAppInstalled(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
String key = params.get("key").toString();
String name = params.get("name").toString();
Map<String, Object> result = onePanelService.checkAppInstalled(serverId, key, name);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 查询数据库列表
*/
@PostMapping("/search")
@Operation(summary = "查询数据库列表")
public R<Map<String, Object>> searchDatabases(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
String database = params.get("database").toString();
int page = params.containsKey("page") ? Integer.parseInt(params.get("page").toString()) : 1;
int pageSize = params.containsKey("pageSize") ? Integer.parseInt(params.get("pageSize").toString()) : 20;
Map<String, Object> result = onePanelService.searchDatabases(serverId, database, page, pageSize);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 创建数据库
*/
@PostMapping("/create")
@Operation(summary = "创建数据库")
public R<Map<String, Object>> createDatabase(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
// 移除serverId只保留1Panel需要的参数
params.remove("serverId");
Map<String, Object> result = onePanelService.createDatabase(serverId, params);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 删除数据库
*/
@PostMapping("/delete")
@Operation(summary = "删除数据库")
public R<Void> deleteDatabase(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
// 移除serverId只保留1Panel需要的参数
params.remove("serverId");
boolean success = onePanelService.deleteDatabase(serverId, params);
if (success) {
return R.ok();
} else {
return R.fail("删除数据库失败");
}
}
/**
* 更新数据库描述
*/
@PostMapping("/description/update")
@Operation(summary = "更新数据库描述")
public R<Void> updateDescription(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
// 移除serverId只保留1Panel需要的参数
params.remove("serverId");
boolean success = onePanelService.updateDatabaseDescription(serverId, params);
if (success) {
return R.ok();
} else {
return R.fail("更新数据库描述失败");
}
}
/**
* 修改数据库密码
*/
@PostMapping("/password/change")
@Operation(summary = "修改数据库密码")
public R<Void> changePassword(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
// 移除serverId只保留1Panel需要的参数
params.remove("serverId");
boolean success = onePanelService.changeDatabasePassword(serverId, params);
if (success) {
return R.ok();
} else {
return R.fail("修改数据库密码失败");
}
}
/**
* 操作应用(启动/停止/重启)
*/
@PostMapping("/app/operate")
@Operation(summary = "操作应用")
public R<Void> operateApp(@RequestBody Map<String, Object> params) {
Long serverId = Long.parseLong(params.get("serverId").toString());
// 移除serverId只保留1Panel需要的参数 (installId, operate)
params.remove("serverId");
boolean success = onePanelService.operateApp(serverId, params);
if (success) {
return R.ok();
} else {
return R.fail("操作失败");
}
}
/**
* 获取数据库字符集排序规则选项
*/
@PostMapping("/format/options")
@Operation(summary = "获取数据库排序规则选项")
public R<java.util.List<String>> getFormatOptions(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
Object typeObj = params.get("type");
String type = typeObj != null ? typeObj.toString() : "mysql";
Object dbObj = params.get("database");
String database = dbObj != null ? dbObj.toString() : type;
Object formatObj = params.get("format");
String format = formatObj != null ? formatObj.toString() : null;
java.util.List<String> options = onePanelService.getDatabaseFormatOptions(serverId, type, database, format);
return R.ok(options);
}
/**
* 获取应用信息如Redis
*/
@PostMapping("/app/info")
@Operation(summary = "获取应用信息")
public R<Map<String, Object>> getAppInfo(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String appKey = params.get("appKey") != null ? params.get("appKey").toString() : "redis";
Map<String, Object> result = onePanelService.getAppInfo(serverId, appKey);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 获取应用版本详情
*/
@PostMapping("/app/detail")
@Operation(summary = "获取应用版本详情")
public R<Map<String, Object>> getAppDetail(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
Long appId = params.get("appId") != null ? Long.parseLong(params.get("appId").toString()) : null;
String version = params.get("version") != null ? params.get("version").toString() : null;
if (appId == null || version == null) {
return R.fail("appId和version不能为空");
}
Map<String, Object> result = onePanelService.getAppDetail(serverId, appId, version);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 安装应用
*/
@PostMapping("/app/install")
@Operation(summary = "安装应用")
public R<Map<String, Object>> installApp(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
// 移除serverId只保留1Panel需要的参数
params.remove("serverId");
Map<String, Object> result = onePanelService.installApp(serverId, params);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 读取任务日志
*/
@PostMapping("/task/log")
@Operation(summary = "读取任务日志")
public R<Map<String, Object>> readTaskLog(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String taskId = params.get("taskId") != null ? params.get("taskId").toString() : "";
int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
int pageSize = params.get("pageSize") != null ? Integer.parseInt(params.get("pageSize").toString()) : 500;
if (taskId.isEmpty()) {
return R.fail("taskId不能为空");
}
Map<String, Object> result = onePanelService.readTaskLog(serverId, taskId, page, pageSize);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
}

View File

@@ -0,0 +1,233 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.service.OnePanelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 文件管理控制器
*/
@RestController
@RequestMapping("/platform/files")
@Tag(name = "文件管理")
public class FileController {
@Resource
private OnePanelService onePanelService;
/**
* 获取文件/目录列表
*/
@PostMapping("/list")
@Operation(summary = "获取文件列表")
public R<Map<String, Object>> listFiles(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String path = params.get("path") != null ? params.get("path").toString() : "/";
Map<String, Object> result = onePanelService.listFiles(serverId, path);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 创建目录
*/
@PostMapping("/mkdir")
@Operation(summary = "创建目录")
public R<Void> mkdir(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("path") == null) {
return R.fail("path不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String path = params.get("path").toString();
Map<String, Object> result = onePanelService.mkdir(serverId, path);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 删除文件/目录
*/
@PostMapping("/delete")
@Operation(summary = "删除文件")
public R<Void> deleteFile(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("path") == null) {
return R.fail("path不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String path = params.get("path").toString();
Boolean isDir = params.get("isDir") != null && Boolean.parseBoolean(params.get("isDir").toString());
Map<String, Object> result = onePanelService.deleteFile(serverId, path, isDir);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 创建文件
*/
@PostMapping("/create")
@Operation(summary = "创建文件")
public R<Void> createFile(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("path") == null) {
return R.fail("path不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String path = params.get("path").toString();
Map<String, Object> result = onePanelService.createFile(serverId, path);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 上传文件
*/
@PostMapping(value = "/upload", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "上传文件")
public R<Void> uploadFile(
@RequestParam("serverId") Long serverId,
@RequestParam("path") String path,
@RequestPart("file") org.springframework.web.multipart.MultipartFile file) {
if (serverId == null) {
return R.fail("serverId不能为空");
}
if (path == null) {
return R.fail("path不能为空");
}
if (file == null || file.isEmpty()) {
return R.fail("文件不能为空");
}
Map<String, Object> result = onePanelService.uploadFile(serverId, path, file);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 检查文件是否存在
*/
@PostMapping("/check")
@Operation(summary = "检查文件是否存在")
public R<java.util.List<String>> checkUploadFiles(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
java.util.List<String> paths = (java.util.List<String>) params.get("paths");
java.util.List<String> result = onePanelService.checkFileBatch(serverId, paths);
return R.ok(result);
}
/**
* 分片上传文件(旧版,兼容)
*/
@PostMapping(value = "/upload/chunk", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "分片上传文件(旧版)")
public R<String> uploadChunk(
@RequestParam("serverId") Long serverId,
@RequestParam("filename") String filename,
@RequestParam("path") String path,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("chunkCount") int chunkCount,
@RequestPart("chunk") org.springframework.web.multipart.MultipartFile chunk) {
return R.ok(onePanelService.uploadFileChunk(serverId, filename, path, chunkIndex, chunkCount, chunk));
}
/**
* 分片上传文件(新版,支持分片合并)
*/
@PostMapping(value = "/upload/chunk/v2", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "分片上传文件v2")
public R<Map<String, Object>> uploadChunkV2(
@RequestParam("serverId") Long serverId,
@RequestParam("path") String path,
@RequestParam("filename") String filename,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("chunkCount") int chunkCount,
@RequestParam("fileSize") long fileSize,
@RequestParam("uploadId") String uploadId,
@RequestPart("chunk") org.springframework.web.multipart.MultipartFile chunk) {
Map<String, Object> result = onePanelService.uploadChunk(serverId, path, filename,
chunkIndex, chunkCount, fileSize, uploadId, chunk);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 合并已上传的分片
*/
@PostMapping("/upload/merge")
@Operation(summary = "合并分片")
public R<Map<String, Object>> mergeChunks(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String path = params.get("path").toString();
String filename = params.get("filename").toString();
int chunkCount = Integer.parseInt(params.get("chunkCount").toString());
long fileSize = Long.parseLong(params.get("fileSize").toString());
String uploadId = params.get("uploadId").toString();
Map<String, Object> result = onePanelService.mergeChunks(serverId, path, filename,
chunkCount, fileSize, uploadId);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
}

View File

@@ -0,0 +1,70 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceAccount;
import com.nanxiislet.admin.service.FinanceAccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 财务账户控制器
*/
@RestController
@RequestMapping("/finance/account")
@Tag(name = "账户管理", description = "财务账户的增删改查")
public class FinanceAccountController {
@Resource
private FinanceAccountService accountService;
@GetMapping("/list")
@Operation(summary = "账户列表")
public R<List<FinanceAccount>> list() {
return R.ok(accountService.list());
}
@GetMapping("/active")
@Operation(summary = "活跃账户列表")
public R<List<FinanceAccount>> listActive() {
return R.ok(accountService.listActive());
}
@GetMapping("/{id}")
@Operation(summary = "账户详情")
public R<FinanceAccount> getById(@PathVariable Long id) {
FinanceAccount account = accountService.getById(id);
if (account == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(account);
}
@PostMapping
@Operation(summary = "新增账户")
public R<FinanceAccount> create(@Valid @RequestBody FinanceAccount account) {
accountService.save(account);
return R.ok(account);
}
@PutMapping("/{id}")
@Operation(summary = "更新账户")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceAccount account) {
account.setId(id);
accountService.updateById(account);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除账户")
public R<Void> delete(@PathVariable Long id) {
accountService.removeById(id);
return R.ok();
}
}

View File

@@ -0,0 +1,99 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceBudget;
import com.nanxiislet.admin.service.FinanceBudgetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
/**
* 预算管理控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@RequestMapping("/finance/budget")
@Tag(name = "预算管理", description = "预算记录的增删改查")
public class FinanceBudgetController {
@Resource
private FinanceBudgetService budgetService;
@GetMapping("/list")
@Operation(summary = "预算列表", description = "分页查询预算记录")
public R<PageResult<FinanceBudget>> list(BasePageQuery query) {
Page<FinanceBudget> page = budgetService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "预算详情", description = "根据ID获取预算记录详情")
public R<FinanceBudget> getById(@PathVariable Long id) {
FinanceBudget budget = budgetService.getById(id);
if (budget == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(budget);
}
@PostMapping
@Operation(summary = "新增预算", description = "创建新的预算记录")
public R<FinanceBudget> create(@Valid @RequestBody FinanceBudget budget) {
// 初始化金额
if (budget.getUsedAmount() == null) {
budget.setUsedAmount(BigDecimal.ZERO);
}
if (budget.getRemainingAmount() == null) {
budget.setRemainingAmount(budget.getTotalBudget());
}
if (budget.getUsageRate() == null) {
budget.setUsageRate(BigDecimal.ZERO);
}
if (budget.getStatus() == null) {
budget.setStatus("active");
}
budgetService.save(budget);
return R.ok(budget);
}
@PutMapping("/{id}")
@Operation(summary = "更新预算", description = "更新预算记录")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceBudget budget) {
budget.setId(id);
budgetService.updateById(budget);
budgetService.calculateUsageRate(id);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除预算", description = "删除预算记录")
public R<Void> delete(@PathVariable Long id) {
budgetService.removeById(id);
return R.ok();
}
@GetMapping("/find")
@Operation(summary = "查找预算", description = "按条件查找预算")
public R<FinanceBudget> findBudget(
@RequestParam Integer year,
@RequestParam String period,
@RequestParam(required = false) Integer quarter,
@RequestParam(required = false) Integer month,
@RequestParam(required = false) Long departmentId,
@RequestParam(required = false) Long projectId) {
FinanceBudget budget = budgetService.findBudget(year, period, quarter, month, departmentId, projectId);
return R.ok(budget);
}
}

View File

@@ -0,0 +1,67 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceExpense;
import com.nanxiislet.admin.service.FinanceExpenseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
/**
* 支出管理控制器
*/
@RestController
@RequestMapping("/finance/expense")
@Tag(name = "支出管理", description = "支出记录的增删改查")
public class FinanceExpenseController {
@Resource
private FinanceExpenseService expenseService;
@GetMapping("/list")
@Operation(summary = "支出列表")
public R<PageResult<FinanceExpense>> list(BasePageQuery query) {
Page<FinanceExpense> page = expenseService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "支出详情")
public R<FinanceExpense> getById(@PathVariable Long id) {
FinanceExpense expense = expenseService.getById(id);
if (expense == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(expense);
}
@PostMapping
@Operation(summary = "新增支出")
public R<FinanceExpense> create(@Valid @RequestBody FinanceExpense expense) {
expense.setExpenseNo(expenseService.generateExpenseNo());
expenseService.save(expense);
return R.ok(expense);
}
@PutMapping("/{id}")
@Operation(summary = "更新支出")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceExpense expense) {
expense.setId(id);
expenseService.updateById(expense);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除支出")
public R<Void> delete(@PathVariable Long id) {
expenseService.removeById(id);
return R.ok();
}
}

View File

@@ -0,0 +1,70 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceIncome;
import com.nanxiislet.admin.service.FinanceIncomeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
/**
* 收入管理控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@RequestMapping("/finance/income")
@Tag(name = "收入管理", description = "收入记录的增删改查")
public class FinanceIncomeController {
@Resource
private FinanceIncomeService incomeService;
@GetMapping("/list")
@Operation(summary = "收入列表", description = "分页查询收入记录")
public R<PageResult<FinanceIncome>> list(BasePageQuery query) {
Page<FinanceIncome> page = incomeService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "收入详情", description = "根据ID获取收入记录详情")
public R<FinanceIncome> getById(@PathVariable Long id) {
FinanceIncome income = incomeService.getById(id);
if (income == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(income);
}
@PostMapping
@Operation(summary = "新增收入", description = "创建新的收入记录")
public R<FinanceIncome> create(@Valid @RequestBody FinanceIncome income) {
income.setIncomeNo(incomeService.generateIncomeNo());
incomeService.save(income);
return R.ok(income);
}
@PutMapping("/{id}")
@Operation(summary = "更新收入", description = "更新收入记录")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceIncome income) {
income.setId(id);
incomeService.updateById(income);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除收入", description = "删除收入记录")
public R<Void> delete(@PathVariable Long id) {
incomeService.removeById(id);
return R.ok();
}
}

View File

@@ -0,0 +1,93 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceInvoice;
import com.nanxiislet.admin.service.FinanceInvoiceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
/**
* 发票管理控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@RequestMapping("/finance/invoice")
@Tag(name = "发票管理", description = "发票记录的增删改查")
public class FinanceInvoiceController {
@Resource
private FinanceInvoiceService invoiceService;
@GetMapping("/list")
@Operation(summary = "发票列表", description = "分页查询发票记录")
public R<PageResult<FinanceInvoice>> list(BasePageQuery query) {
Page<FinanceInvoice> page = invoiceService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "发票详情", description = "根据ID获取发票记录详情")
public R<FinanceInvoice> getById(@PathVariable Long id) {
FinanceInvoice invoice = invoiceService.getById(id);
if (invoice == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(invoice);
}
@PostMapping
@Operation(summary = "申请开票", description = "创建新的发票申请")
public R<FinanceInvoice> create(@Valid @RequestBody FinanceInvoice invoice) {
invoice.setStatus("pending");
invoice.setSubmitTime(LocalDate.now());
invoiceService.save(invoice);
return R.ok(invoice);
}
@PutMapping("/{id}")
@Operation(summary = "更新发票", description = "更新发票信息")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceInvoice invoice) {
invoice.setId(id);
invoiceService.updateById(invoice);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除发票", description = "删除发票记录")
public R<Void> delete(@PathVariable Long id) {
invoiceService.removeById(id);
return R.ok();
}
@PostMapping("/{id}/issue")
@Operation(summary = "开具发票", description = "开具发票")
public R<Void> issue(@PathVariable Long id) {
invoiceService.issueInvoice(id);
return R.ok();
}
@PostMapping("/{id}/reject")
@Operation(summary = "驳回发票", description = "驳回发票申请")
public R<Void> reject(@PathVariable Long id, @RequestBody RejectRequest request) {
invoiceService.rejectInvoice(id, request.getReason());
return R.ok();
}
@Data
public static class RejectRequest {
private String reason;
}
}

View File

@@ -0,0 +1,79 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceReimbursement;
import com.nanxiislet.admin.service.FinanceReimbursementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
/**
* 报销管理控制器
*/
@RestController
@RequestMapping("/finance/reimbursement")
@Tag(name = "报销管理", description = "报销记录的增删改查")
public class FinanceReimbursementController {
@Resource
private FinanceReimbursementService reimbursementService;
@GetMapping("/list")
@Operation(summary = "报销列表")
public R<PageResult<FinanceReimbursement>> list(BasePageQuery query) {
Page<FinanceReimbursement> page = reimbursementService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "报销详情")
public R<FinanceReimbursement> getById(@PathVariable Long id) {
FinanceReimbursement reimbursement = reimbursementService.getById(id);
if (reimbursement == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(reimbursement);
}
@PostMapping
@Operation(summary = "新增报销")
public R<FinanceReimbursement> create(@Valid @RequestBody FinanceReimbursement reimbursement) {
reimbursement.setReimbursementNo(reimbursementService.generateReimbursementNo());
reimbursementService.save(reimbursement);
return R.ok(reimbursement);
}
@PutMapping("/{id}")
@Operation(summary = "更新报销")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceReimbursement reimbursement) {
reimbursement.setId(id);
reimbursementService.updateById(reimbursement);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除报销")
public R<Void> delete(@PathVariable Long id) {
reimbursementService.removeById(id);
return R.ok();
}
@PostMapping("/{id}/submit")
@Operation(summary = "提交审批")
public R<Void> submit(@PathVariable Long id) {
FinanceReimbursement reimbursement = reimbursementService.getById(id);
if (reimbursement == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
reimbursement.setStatus("pending");
reimbursementService.updateById(reimbursement);
return R.ok();
}
}

View File

@@ -0,0 +1,109 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.entity.FinanceSettlement;
import com.nanxiislet.admin.service.FinanceSettlementService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
/**
* 结算管理控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@RequestMapping("/finance/settlement")
@Tag(name = "结算管理", description = "结算记录的增删改查")
public class FinanceSettlementController {
@Resource
private FinanceSettlementService settlementService;
@GetMapping("/list")
@Operation(summary = "结算列表", description = "分页查询结算记录")
public R<PageResult<FinanceSettlement>> list(BasePageQuery query) {
Page<FinanceSettlement> page = settlementService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "结算详情", description = "根据ID获取结算记录详情")
public R<FinanceSettlement> getById(@PathVariable Long id) {
FinanceSettlement settlement = settlementService.getById(id);
if (settlement == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(settlement);
}
@PostMapping
@Operation(summary = "新增结算", description = "创建新的结算记录")
public R<FinanceSettlement> create(@Valid @RequestBody FinanceSettlement settlement) {
// 计算金额
settlementService.calculateAmounts(settlement);
// 默认状态
if (settlement.getStatus() == null) {
settlement.setStatus("pending");
}
if (settlement.getInvoiceStatus() == null) {
settlement.setInvoiceStatus("none");
}
settlementService.save(settlement);
return R.ok(settlement);
}
@PutMapping("/{id}")
@Operation(summary = "更新结算", description = "更新结算记录")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody FinanceSettlement settlement) {
settlement.setId(id);
// 重新计算金额
settlementService.calculateAmounts(settlement);
settlementService.updateById(settlement);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除结算", description = "删除结算记录")
public R<Void> delete(@PathVariable Long id) {
settlementService.removeById(id);
return R.ok();
}
@PostMapping("/{id}/approve")
@Operation(summary = "审核通过", description = "审核通过结算单")
public R<Void> approve(@PathVariable Long id) {
settlementService.approve(id);
return R.ok();
}
@PostMapping("/{id}/reject")
@Operation(summary = "驳回", description = "驳回结算单")
public R<Void> reject(@PathVariable Long id, @RequestBody RejectRequest request) {
settlementService.reject(id, request.getReason());
return R.ok();
}
@PostMapping("/{id}/confirm-payment")
@Operation(summary = "确认打款", description = "确认打款完成")
public R<Void> confirmPayment(@PathVariable Long id) {
settlementService.confirmPayment(id);
return R.ok();
}
@Data
public static class RejectRequest {
private String reason;
}
}

View File

@@ -0,0 +1,51 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 健康检查控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@Tag(name = "系统", description = "系统相关接口")
public class HealthController {
@Value("${spring.application.name:nanxiislet-admin}")
private String applicationName;
@Value("${server.port:8080}")
private String port;
@GetMapping("/health")
@Operation(summary = "健康检查", description = "检查服务健康状态")
public R<Map<String, Object>> health() {
Map<String, Object> result = new HashMap<>();
result.put("status", "UP");
result.put("application", applicationName);
result.put("port", port);
result.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return R.ok(result);
}
@GetMapping("/")
@Operation(summary = "首页", description = "API首页")
public R<Map<String, String>> index() {
Map<String, String> result = new HashMap<>();
result.put("name", "Nanxiislet Admin API");
result.put("version", "1.0.0");
result.put("docs", "/doc.html");
return R.ok(result);
}
}

View File

@@ -0,0 +1,138 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.dto.CertificateApplyRequest;
import com.nanxiislet.admin.dto.CertificateApplyResult;
import com.nanxiislet.admin.entity.PlatformCertificate;
import com.nanxiislet.admin.service.PlatformCertificateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 证书管理控制器
*
* @author NanxiIslet
* @since 2026-01-13
*/
@RestController
@RequestMapping("/platform/certificate")
@Tag(name = "证书管理", description = "管理SSL/TLS证书整合1Panel证书功能")
public class PlatformCertificateController {
@Resource
private PlatformCertificateService certificateService;
@Resource
private com.nanxiislet.admin.service.PlatformServerService serverService;
// ==================== 证书 CRUD ====================
@GetMapping("/list")
@Operation(summary = "证书列表", description = "获取指定服务器的证书列表")
public R<PageResult<PlatformCertificate>> list(
@RequestParam(required = false) Long serverId,
BasePageQuery query) {
Page<PlatformCertificate> page = certificateService.listPage(serverId, query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "证书详情", description = "获取证书详情,包含证书内容和私钥")
public R<PlatformCertificate> getDetail(@PathVariable Long id) {
PlatformCertificate cert = certificateService.getCertificateDetail(id);
if (cert == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(cert);
}
@DeleteMapping("/{id}")
@Operation(summary = "删除证书", description = "删除证书同时从1Panel删除")
public R<Void> delete(@PathVariable Long id) {
boolean success = certificateService.deleteCertificate(id);
if (!success) {
throw new BusinessException("删除失败");
}
return R.ok();
}
@PutMapping("/{id}/settings")
@Operation(summary = "更新证书设置", description = "更新自动续签和备注")
public R<Void> updateSettings(
@PathVariable Long id,
@RequestParam(required = false) Boolean autoRenew,
@RequestParam(required = false) String description) {
certificateService.updateCertificateSettings(id, autoRenew, description);
return R.ok();
}
// ==================== 证书申请 ====================
@PostMapping("/apply")
@Operation(summary = "申请证书", description = "调用1Panel API申请SSL证书")
public R<CertificateApplyResult> apply(@Valid @RequestBody CertificateApplyRequest request) {
CertificateApplyResult result = certificateService.applyCertificate(request);
return R.ok(result);
}
// ==================== 证书同步 ====================
@PostMapping("/sync/{serverId}")
@Operation(summary = "同步证书", description = "从1Panel同步证书列表到本地数据库")
public R<Map<String, Object>> sync(@PathVariable Long serverId) {
int count = certificateService.syncCertificatesFromPanel(serverId);
return R.ok(Map.of("syncCount", count, "message", "同步完成,共 " + count + " 个证书"));
}
// ==================== 1Panel 账户查询 ====================
@GetMapping("/acme-accounts/{serverId}")
@Operation(summary = "获取Acme账户列表", description = "从1Panel获取Acme账户列表")
public R<List<Map<String, Object>>> getAcmeAccounts(@PathVariable Long serverId) {
return R.ok(certificateService.getAcmeAccounts(serverId));
}
@GetMapping("/dns-accounts/{serverId}")
@Operation(summary = "获取DNS账户列表", description = "从1Panel获取DNS账户列表")
public R<List<Map<String, Object>>> getDnsAccounts(@PathVariable Long serverId) {
return R.ok(certificateService.getDnsAccounts(serverId));
}
@GetMapping("/websites/{serverId}")
@Operation(summary = "获取网站列表", description = "从1Panel获取网站列表")
public R<List<Map<String, Object>>> getWebsites(@PathVariable Long serverId) {
return R.ok(certificateService.getWebsites(serverId));
}
// ==================== 调试接口 ====================
@GetMapping("/debug/server/{serverId}")
@Operation(summary = "检查服务器1Panel配置", description = "用于调试检查服务器是否正确配置了1Panel API")
public R<Map<String, Object>> debugServerConfig(@PathVariable Long serverId) {
var server = serverService.getById(serverId);
if (server == null) {
return R.ok(Map.of("error", "服务器不存在", "serverId", serverId));
}
return R.ok(Map.of(
"serverId", serverId,
"serverName", server.getName() != null ? server.getName() : "",
"ip", server.getIp() != null ? server.getIp() : "",
"panelUrl", server.getPanelUrl() != null ? server.getPanelUrl() : "(未配置)",
"panelPort", server.getPanelPort() != null ? server.getPanelPort() : 42588,
"panelApiKeyConfigured", server.getPanelApiKey() != null && !server.getPanelApiKey().isEmpty(),
"panelApiKeyLength", server.getPanelApiKey() != null ? server.getPanelApiKey().length() : 0
));
}
}

View File

@@ -0,0 +1,275 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.dto.DomainDeployRequest;
import com.nanxiislet.admin.dto.DomainDeployResult;
import com.nanxiislet.admin.dto.DomainStatsDTO;
import com.nanxiislet.admin.entity.PlatformDomain;
import com.nanxiislet.admin.entity.PlatformServer;
import com.nanxiislet.admin.service.PlatformDomainService;
import com.nanxiislet.admin.service.PlatformServerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 域名管理控制器
*
* @author NanxiIslet
* @since 2026-01-13
*/
@RestController
@RequestMapping("/platform/domain")
@Tag(name = "域名管理", description = "域名的增删改查及部署")
public class PlatformDomainController {
@Resource
private PlatformDomainService domainService;
@Resource
private PlatformServerService serverService;
// ==================== 基础 CRUD ====================
@GetMapping("/list")
@Operation(summary = "域名列表")
public R<PageResult<PlatformDomain>> list(com.nanxiislet.admin.dto.DomainQueryDTO query) {
Page<PlatformDomain> page = domainService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/all")
@Operation(summary = "所有域名")
public R<List<PlatformDomain>> listAll() {
return R.ok(domainService.list());
}
@GetMapping("/{id}")
@Operation(summary = "域名详情")
public R<PlatformDomain> getById(@PathVariable Long id) {
PlatformDomain domain = domainService.getById(id);
if (domain == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(domain);
}
@PostMapping
@Operation(summary = "新增域名")
public R<PlatformDomain> create(@Valid @RequestBody PlatformDomain domain) {
// 检查域名是否已存在
PlatformDomain existing = domainService.getByDomain(domain.getDomain());
if (existing != null) {
throw new BusinessException("域名已存在");
}
// 设置默认值
if (domain.getStatus() == null) {
domain.setStatus("pending");
}
if (domain.getDnsStatus() == null) {
domain.setDnsStatus("checking");
}
if (domain.getSslStatus() == null) {
domain.setSslStatus("none");
}
if (domain.getDeployStatus() == null) {
domain.setDeployStatus("not_deployed");
}
if (domain.getPort() == null) {
domain.setPort(80);
}
// 自动填充服务器信息
if (domain.getServerId() != null) {
PlatformServer server = serverService.getById(domain.getServerId());
if (server != null) {
domain.setServerName(server.getName());
domain.setServerIp(server.getIp());
}
}
// 自动生成别名和站点路径
if (!StringUtils.hasText(domain.getAlias())) {
domain.setAlias(domain.getDomain().replace(".", "_"));
}
if (!StringUtils.hasText(domain.getSitePath())) {
domain.setSitePath("/opt/1panel/www/sites/" + domain.getDomain() + "/index");
}
// 如果绑定了SSL证书自动设置SSL状态和开启HTTPS
if (domain.getCertificateId() != null) {
domain.setSslStatus("valid");
domain.setEnableHttps(true);
}
domainService.save(domain);
return R.ok(domain);
}
@PutMapping("/{id}")
@Operation(summary = "更新域名")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody PlatformDomain domain) {
PlatformDomain existing = domainService.getById(id);
if (existing == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
domain.setId(id);
// 自动填充服务器信息
if (domain.getServerId() != null && !domain.getServerId().equals(existing.getServerId())) {
PlatformServer server = serverService.getById(domain.getServerId());
if (server != null) {
domain.setServerName(server.getName());
domain.setServerIp(server.getIp());
}
}
// 处理SSL和HTTPS状态
// 只有在用户未明确设置 enableHttps 时,才自动根据证书设置
if (domain.getEnableHttps() == null) {
// 用户未传入 enableHttps根据证书自动判断
if (domain.getCertificateId() != null) {
domain.setSslStatus("valid");
domain.setEnableHttps(true);
}
} else if (Boolean.FALSE.equals(domain.getEnableHttps())) {
// 用户明确关闭HTTPS
domain.setSslStatus("none");
} else if (Boolean.TRUE.equals(domain.getEnableHttps()) && domain.getCertificateId() != null) {
// 用户开启HTTPS且有证书
domain.setSslStatus("valid");
}
domainService.updateById(domain);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除域名")
public R<Void> delete(@PathVariable Long id) {
domainService.removeById(id);
return R.ok();
}
// ==================== 统计接口 ====================
@GetMapping("/stats")
@Operation(summary = "域名统计信息")
public R<DomainStatsDTO> getStats() {
return R.ok(domainService.getDomainStats());
}
// ==================== 部署相关接口 ====================
@PostMapping("/deploy")
@Operation(summary = "部署域名到1Panel", description = "包含创建网站、申请证书、配置HTTPS")
public R<DomainDeployResult> deploy(@Valid @RequestBody DomainDeployRequest request) {
DomainDeployResult result = domainService.deployDomain(request);
return R.ok(result);
}
@PostMapping("/undeploy/{id}")
@Operation(summary = "从1Panel删除部署")
public R<Void> undeploy(@PathVariable Long id) {
domainService.undeployDomain(id);
return R.ok();
}
@PostMapping("/{id}/check-dns")
@Operation(summary = "检查域名DNS解析状态")
public R<Map<String, Object>> checkDns(@PathVariable Long id) {
Map<String, Object> result = domainService.checkDomainDns(id);
return R.ok(result);
}
@PostMapping("/{id}/sync")
@Operation(summary = "从1Panel同步域名信息")
public R<PlatformDomain> syncFromPanel(@PathVariable Long id) {
PlatformDomain domain = domainService.syncDomainFromPanel(id);
return R.ok(domain);
}
@PostMapping("/sync-from-certificates/{serverId}")
@Operation(summary = "从证书同步域名", description = "将证书表中的域名同步到域名表")
public R<Map<String, Object>> syncFromCertificates(@PathVariable Long serverId) {
int count = domainService.syncDomainsFromCertificates(serverId);
return R.ok(Map.of("syncCount", count, "message", "同步完成,共 " + count + " 个域名"));
}
@PostMapping("/{id}/deploy-runtime")
@Operation(summary = "部署运行环境到1Panel", description = "创建运行时类型的网站")
public R<DomainDeployResult> deployRuntime(@PathVariable Long id) {
DomainDeployResult result = domainService.deployRuntime(id);
return R.ok(result);
}
// ==================== Nginx配置相关 ====================
@Resource
private com.nanxiislet.admin.service.OnePanelService onePanelService;
@GetMapping("/{id}/nginx-config")
@Operation(summary = "获取域名Nginx配置")
public R<Map<String, Object>> getNginxConfig(@PathVariable Long id) {
PlatformDomain domain = domainService.getById(id);
if (domain == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
if (domain.getServerId() == null || domain.getPanelWebsiteId() == null) {
throw new BusinessException("域名未部署或未绑定服务器");
}
Map<String, Object> config = onePanelService.getWebsiteNginxConfig(domain.getServerId(), domain.getPanelWebsiteId());
// 添加域名信息
config.put("domain", domain.getDomain());
return R.ok(config);
}
@PutMapping("/{id}/nginx-config")
@Operation(summary = "保存域名Nginx配置")
public R<Boolean> saveNginxConfig(@PathVariable Long id, @RequestBody Map<String, Object> params) {
PlatformDomain domain = domainService.getById(id);
if (domain == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
if (domain.getServerId() == null || domain.getPanelWebsiteId() == null) {
throw new BusinessException("域名未部署或未绑定服务器");
}
String content = (String) params.get("content");
if (content == null || content.isEmpty()) {
throw new BusinessException("配置内容不能为空");
}
boolean success = onePanelService.saveWebsiteNginxConfig(domain.getServerId(), domain.getPanelWebsiteId(), content);
return success ? R.ok(true) : R.fail("保存失败");
}
@PostMapping("/{id}/nginx-reload")
@Operation(summary = "重载Nginx")
public R<Boolean> reloadNginx(@PathVariable Long id) {
PlatformDomain domain = domainService.getById(id);
if (domain == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
if (domain.getServerId() == null) {
throw new BusinessException("域名未绑定服务器");
}
boolean success = onePanelService.reloadNginx(domain.getServerId());
return success ? R.ok(true) : R.fail("重载失败");
}
}

View File

@@ -0,0 +1,418 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.dto.DeployRequest;
import com.nanxiislet.admin.dto.DeployResult;
import com.nanxiislet.admin.entity.PlatformProject;
import com.nanxiislet.admin.service.OnePanelService;
import com.nanxiislet.admin.service.PlatformProjectService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
/**
* 项目管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/platform/project")
@Tag(name = "项目管理", description = "项目的增删改查和部署")
public class PlatformProjectController {
@Resource
private PlatformProjectService projectService;
@Resource
private OnePanelService onePanelService;
@Resource
private com.nanxiislet.admin.service.PlatformDomainService domainService;
@Resource
private com.nanxiislet.admin.service.SysMenuService menuService;
@GetMapping("/list")
@Operation(summary = "项目列表")
public R<PageResult<PlatformProject>> list(BasePageQuery query) {
Page<PlatformProject> page = projectService.listPage(query);
// 填充菜单数量
fillMenuCount(page.getRecords());
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/all")
@Operation(summary = "所有项目")
public R<List<PlatformProject>> listAll() {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlatformProject> wrapper =
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
// 过滤掉门户类项目
wrapper.ne(PlatformProject::getSystemType, "portal");
// 确保按照排序字段排序
wrapper.orderByAsc(PlatformProject::getSort);
List<PlatformProject> list = projectService.list(wrapper);
// 填充菜单数量
fillMenuCount(list);
return R.ok(list);
}
@GetMapping("/integrated")
@Operation(summary = "获取集成到框架的业务项目", description = "返回 systemType=admin 且 integrateToFramework=true 的项目")
public R<List<PlatformProject>> listIntegratedProjects() {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlatformProject> wrapper =
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
wrapper.eq(PlatformProject::getSystemType, "admin")
.eq(PlatformProject::getIntegrateToFramework, true)
.eq(PlatformProject::getStatus, "active")
.orderByAsc(PlatformProject::getSort);
List<PlatformProject> list = projectService.list(wrapper);
// 填充菜单数量
fillMenuCount(list);
return R.ok(list);
}
/**
* 填充菜单数量
*/
private void fillMenuCount(List<PlatformProject> projectList) {
if (projectList == null || projectList.isEmpty()) {
return;
}
for (PlatformProject project : projectList) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.nanxiislet.admin.entity.SysMenu> menuWrapper =
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
// 判定是否为主框架 NanxiAdmin
// 逻辑简称或Code匹配忽略大小写和空格或者 ID 为 9 (从截图看是9作为兜底)
String shortName = project.getShortName() != null ? project.getShortName().trim() : "";
String code = project.getCode() != null ? project.getCode().trim() : "";
boolean isMainProject = "NanxiAdmin".equalsIgnoreCase(shortName)
|| "NanxiAdmin".equalsIgnoreCase(code)
|| Long.valueOf(9).equals(project.getId());
if (isMainProject) {
// 主框架:统计 project_id 为 null 或等于其 id
menuWrapper.and(w -> w.eq(com.nanxiislet.admin.entity.SysMenu::getProjectId, project.getId())
.or()
.isNull(com.nanxiislet.admin.entity.SysMenu::getProjectId));
} else {
// 其他项目,只统计其 id
menuWrapper.eq(com.nanxiislet.admin.entity.SysMenu::getProjectId, project.getId());
}
// 只统计菜单和目录,不统计按钮
menuWrapper.in(com.nanxiislet.admin.entity.SysMenu::getType, "directory", "menu");
long count = menuService.count(menuWrapper);
project.setMenuCount(count);
log.info("Project: {}, ID: {}, IsMain: {}, MenuCount: {}", project.getShortName(), project.getId(), isMainProject, count);
}
}
@GetMapping("/{id}")
@Operation(summary = "项目详情")
public R<PlatformProject> getById(@PathVariable Long id) {
PlatformProject project = projectService.getById(id);
if (project == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(project);
}
@PostMapping
@Operation(summary = "新增项目")
public R<PlatformProject> create(@Valid @RequestBody PlatformProject project) {
projectService.save(project);
return R.ok(project);
}
@PutMapping("/{id}")
@Operation(summary = "更新项目")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody PlatformProject project) {
project.setId(id);
// 如果状态发生改变,尝试同步更新 1Panel 网站状态
if (project.getStatus() != null) {
PlatformProject existingProject = projectService.getById(id);
if (existingProject != null && !project.getStatus().equals(existingProject.getStatus())) {
// 判断是否已部署且绑定了 1Panel 网站
if (existingProject.getPanelWebsiteId() != null && existingProject.getServerId() != null) {
String operate = "active".equals(project.getStatus()) ? "start" : "stop";
onePanelService.operateWebsite(existingProject.getServerId(), existingProject.getPanelWebsiteId(), operate);
}
}
}
projectService.updateById(project);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除项目")
public R<Void> delete(@PathVariable Long id) {
PlatformProject project = projectService.getById(id);
if (project != null && project.getDomainId() != null) {
// 解除域名绑定并重置状态
com.nanxiislet.admin.entity.PlatformDomain domain = domainService.getById(project.getDomainId());
if (domain != null) {
// 如果已部署且绑定了 1Panel 网站,则调用 1Panel 接口删除网站
if (domain.getPanelWebsiteId() != null && domain.getServerId() != null) {
try {
onePanelService.deleteWebsite(domain.getServerId(), domain.getPanelWebsiteId());
} catch (Exception e) {
// 忽略删除网站失败,继续执行解绑逻辑
}
}
domain.setProjectId(null);
domain.setProjectName(null);
domain.setStatus("pending");
domain.setDeployStatus("pending");
domain.setPanelWebsiteId(null);
domain.setPanelSslId(null);
domain.setDnsStatus("pending");
domain.setSslStatus("pending");
domain.setEnableHttps(false);
domainService.updateById(domain);
// 尝试重新检测 DNS
try {
domainService.checkDomainDns(domain.getId());
} catch (Exception e) {
// 忽略 DNS 检测错误
}
}
}
projectService.removeById(id);
return R.ok();
}
// ==================== 部署相关接口 ====================
@PostMapping("/deploy")
@Operation(summary = "部署项目到服务器", description = "通过1Panel API将项目部署到服务器")
public R<DeployResult> deploy(@Valid @RequestBody DeployRequest request) {
DeployResult result = onePanelService.deployProject(request);
return R.ok(result);
}
@GetMapping("/{serverId}/websites")
@Operation(summary = "获取服务器网站列表", description = "从1Panel获取指定服务器的网站列表")
public R<String> getWebsites(@PathVariable Long serverId) {
String websites = onePanelService.getWebsites(serverId);
return R.ok(websites);
}
@GetMapping("/{serverId}/certificates")
@Operation(summary = "获取服务器证书列表", description = "从1Panel获取指定服务器的SSL证书列表")
public R<String> getCertificates(@PathVariable Long serverId) {
String certificates = onePanelService.getCertificates(serverId);
return R.ok(certificates);
}
@GetMapping("/check-deploy/{projectId}")
@Operation(summary = "检查项目部署状态", description = "检查项目对应的网站是否已在1Panel中存在")
public R<Map<String, Object>> checkDeployStatus(@PathVariable Long projectId) {
PlatformProject project = projectService.getById(projectId);
if (project == null) {
return R.fail("项目不存在");
}
Map<String, Object> result = new java.util.HashMap<>();
result.put("projectId", projectId);
result.put("domain", project.getDomain());
if (project.getServerId() == null || project.getDomain() == null || project.getDomain().isEmpty()) {
result.put("deployed", false);
result.put("websiteId", null);
result.put("message", "项目未配置服务器或域名");
return R.ok(result);
}
// 获取网站详细状态
Map<String, Object> websiteStatus = onePanelService.checkWebsiteStatus(project.getServerId(), project.getDomain());
boolean exists = Boolean.TRUE.equals(websiteStatus.get("exists"));
result.put("deployed", exists);
result.put("websiteId", websiteStatus.get("id"));
result.put("websiteStatus", websiteStatus.get("status"));
result.put("sslStatus", websiteStatus.get("sslStatus"));
result.put("protocol", websiteStatus.get("protocol"));
result.put("message", exists ? "已部署" : "未部署");
// 更新项目状态到数据库
if (exists) {
Long websiteId = (Long) websiteStatus.get("id");
String status = (String) websiteStatus.get("status");
String sslStatus = (String) websiteStatus.get("sslStatus");
String protocol = (String) websiteStatus.get("protocol");
boolean needUpdate = false;
// 更新 panelWebsiteId
if (websiteId != null && (project.getPanelWebsiteId() == null || !project.getPanelWebsiteId().equals(websiteId))) {
project.setPanelWebsiteId(websiteId);
needUpdate = true;
}
// 更新部署状态
if (!"success".equals(project.getLastDeployStatus())) {
project.setLastDeployStatus("success");
needUpdate = true;
}
// 更新项目状态Running -> active/启用)
if ("Running".equals(status) && !"active".equals(project.getStatus())) {
project.setStatus("active");
needUpdate = true;
}
// 更新 HTTPS 状态
if ("HTTPS".equals(protocol) && !Boolean.TRUE.equals(project.getEnableHttps())) {
project.setEnableHttps(true);
needUpdate = true;
}
// 更新部署路径
String sitePath = (String) websiteStatus.get("sitePath");
if (sitePath != null) {
// 强制追加 /index 目录
if (!sitePath.endsWith("/index")) {
if (sitePath.endsWith("/")) {
sitePath = sitePath + "index";
} else {
sitePath = sitePath + "/index";
}
}
if (!sitePath.equals(project.getDeployPath())) {
log.info("Updating Project {} deployPath to {}", project.getId(), sitePath); // Log
project.setDeployPath(sitePath);
needUpdate = true;
}
}
if (needUpdate) {
projectService.updateById(project);
}
}
return R.ok(result);
}
// ==================== 文件上传相关接口 ====================
@PostMapping("/upload/check")
@Operation(summary = "检查文件是否存在")
public R<List<String>> checkUploadFiles(@RequestBody Map<String, Object> params) {
Long projectId = Long.valueOf(params.get("projectId").toString());
List<String> paths = (List<String>) params.get("paths");
PlatformProject project = projectService.getById(projectId);
if (project == null || project.getServerId() == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
log.info("Checking files for Project {}: {}", projectId, paths); // Log input
List<String> result = onePanelService.checkFileBatch(project.getServerId(), paths);
log.info("Check result: {}", result); // Log output
return R.ok(result);
}
@PostMapping("/upload/prepare")
@Operation(summary = "准备上传(已废弃,保留向后兼容)", description = "由于 1Panel upload 接口支持 overwrite 参数,此接口已不再需要预先清理文件")
@Deprecated
public R<Boolean> prepareUpload(@RequestBody Map<String, Object> params) {
// upload 接口支持 overwrite=true不再需要预先清理文件
return R.ok(true);
}
@PostMapping(value = "/upload/chunk", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "分片上传文件")
public R<String> uploadChunk(
@RequestParam("projectId") Long projectId,
@RequestParam("filename") String filename,
@RequestParam("path") String path,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("chunkCount") int chunkCount,
@RequestPart("chunk") org.springframework.web.multipart.MultipartFile chunk) {
PlatformProject project = projectService.getById(projectId);
if (project == null || project.getServerId() == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(onePanelService.uploadFileChunk(project.getServerId(), filename, path, chunkIndex, chunkCount, chunk));
}
// ==================== Nginx配置相关 ====================
@GetMapping("/{projectId}/nginx-config")
@Operation(summary = "获取项目Nginx配置")
public R<Map<String, Object>> getNginxConfig(@PathVariable Long projectId) {
PlatformProject project = projectService.getById(projectId);
if (project == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
if (project.getServerId() == null || project.getPanelWebsiteId() == null) {
throw new BusinessException("项目未部署或未绑定服务器");
}
Map<String, Object> config = onePanelService.getWebsiteNginxConfig(project.getServerId(), project.getPanelWebsiteId());
// 添加域名信息
config.put("domain", project.getDomain());
return R.ok(config);
}
@PutMapping("/{projectId}/nginx-config")
@Operation(summary = "保存项目Nginx配置")
public R<Boolean> saveNginxConfig(@PathVariable Long projectId, @RequestBody Map<String, Object> params) {
PlatformProject project = projectService.getById(projectId);
if (project == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
if (project.getServerId() == null || project.getPanelWebsiteId() == null) {
throw new BusinessException("项目未部署或未绑定服务器");
}
String content = (String) params.get("content");
if (content == null || content.isEmpty()) {
throw new BusinessException("配置内容不能为空");
}
boolean success = onePanelService.saveWebsiteNginxConfig(project.getServerId(), project.getPanelWebsiteId(), content);
return success ? R.ok(true) : R.fail("保存失败");
}
@PostMapping("/{projectId}/nginx-reload")
@Operation(summary = "重载Nginx")
public R<Boolean> reloadNginx(@PathVariable Long projectId) {
PlatformProject project = projectService.getById(projectId);
if (project == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
if (project.getServerId() == null) {
throw new BusinessException("项目未绑定服务器");
}
boolean success = onePanelService.reloadNginx(project.getServerId());
return success ? R.ok(true) : R.fail("重载失败");
}
}

View File

@@ -0,0 +1,135 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.base.BasePageQuery;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.dto.ServerInfoDto;
import com.nanxiislet.admin.entity.PlatformServer;
import com.nanxiislet.admin.service.PlatformServerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 服务器管理控制器
*/
@RestController
@RequestMapping("/platform/server")
@Tag(name = "服务器管理", description = "服务器的增删改查")
public class PlatformServerController {
@Resource
private PlatformServerService serverService;
@GetMapping("/list")
@Operation(summary = "服务器列表(分页)")
public R<PageResult<PlatformServer>> list(BasePageQuery query) {
Page<PlatformServer> page = serverService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/all")
@Operation(summary = "所有服务器(基础数据,支持筛选)")
public R<List<PlatformServer>> listAll(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String status,
@RequestParam(required = false) String type
) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<PlatformServer> wrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
if (cn.hutool.core.util.StrUtil.isNotBlank(keyword)) {
wrapper.and(w -> w
.like(PlatformServer::getName, keyword)
.or().like(PlatformServer::getIp, keyword)
);
}
if (cn.hutool.core.util.StrUtil.isNotBlank(status)) {
wrapper.eq(PlatformServer::getStatus, status);
}
if (cn.hutool.core.util.StrUtil.isNotBlank(type)) {
wrapper.eq(PlatformServer::getType, type);
}
wrapper.orderByDesc(PlatformServer::getCreatedAt);
return R.ok(serverService.list(wrapper));
}
@GetMapping("/{id}")
@Operation(summary = "服务器详情(基础数据)")
public R<PlatformServer> getById(@PathVariable Long id) {
PlatformServer server = serverService.getById(id);
if (server == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(server);
}
@GetMapping("/{id}/status")
@Operation(summary = "获取服务器实时状态从1Panel获取CPU/内存/磁盘使用情况)")
public R<ServerInfoDto> getServerStatus(@PathVariable Long id) {
ServerInfoDto dto = serverService.getServerStatus(id);
if (dto == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(dto);
}
@PostMapping("/{id}/refresh")
@Operation(summary = "刷新服务器状态(同 /{id}/status")
public R<ServerInfoDto> refreshStatus(@PathVariable Long id) {
ServerInfoDto dto = serverService.getServerStatus(id);
if (dto == null) {
throw new BusinessException(ResultCode.DATA_NOT_EXIST);
}
return R.ok(dto);
}
@PostMapping
@Operation(summary = "新增服务器")
public R<PlatformServer> create(@Valid @RequestBody PlatformServer server) {
serverService.save(server);
return R.ok(server);
}
@PutMapping("/{id}")
@Operation(summary = "更新服务器")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody PlatformServer server) {
server.setId(id);
serverService.updateById(server);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除服务器")
public R<Void> delete(@PathVariable Long id) {
// 检查是否有绑定的项目
List<String> bindingProjects = serverService.getBindingProjects(id);
if (!bindingProjects.isEmpty()) {
String projectNames = String.join("", bindingProjects);
return R.fail("无法删除服务器,以下项目正在使用该服务器:" + projectNames + ",请先删除或修改这些项目的服务器配置");
}
// 删除该服务器绑定的域名
serverService.deleteBindingDomains(id);
// 删除该服务器绑定的证书
serverService.deleteBindingCertificates(id);
// 删除服务器
serverService.removeById(id);
return R.ok();
}
}

View File

@@ -0,0 +1,297 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.service.OnePanelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 运行时管理控制器
*/
@RestController
@RequestMapping("/platform/runtime")
@Tag(name = "运行时管理")
public class RuntimeController {
@Resource
private OnePanelService onePanelService;
/**
* 搜索运行时列表
*/
@PostMapping("/search")
@Operation(summary = "搜索运行时列表")
public R<Map<String, Object>> searchRuntimes(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String type = params.get("type") != null ? params.get("type").toString() : "node";
int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
int pageSize = params.get("pageSize") != null ? Integer.parseInt(params.get("pageSize").toString()) : 20;
Map<String, Object> result = onePanelService.searchRuntimes(serverId, type, page, pageSize);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 同步运行时状态
*/
@PostMapping("/sync")
@Operation(summary = "同步运行时状态")
public R<Void> syncRuntimes(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
Map<String, Object> result = onePanelService.syncRuntimes(serverId);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 操作运行时(启动/停止/重启)
*/
@PostMapping("/operate")
@Operation(summary = "操作运行时")
public R<Void> operateRuntime(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("id") == null) {
return R.fail("id不能为空");
}
if (params.get("operate") == null) {
return R.fail("operate不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
Long id = Long.parseLong(params.get("id").toString());
String operate = params.get("operate").toString();
Map<String, Object> result = onePanelService.operateRuntime(serverId, id, operate);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 删除运行时
*/
@PostMapping("/delete")
@Operation(summary = "删除运行时")
public R<Void> deleteRuntime(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("id") == null) {
return R.fail("id不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
Long id = Long.parseLong(params.get("id").toString());
boolean forceDelete = params.get("forceDelete") != null && Boolean.parseBoolean(params.get("forceDelete").toString());
boolean deleteFolder = params.get("deleteFolder") != null && Boolean.parseBoolean(params.get("deleteFolder").toString());
String codeDir = params.get("codeDir") != null ? params.get("codeDir").toString() : null;
Map<String, Object> result = onePanelService.deleteRuntime(serverId, id, forceDelete, deleteFolder, codeDir);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 搜索运行时应用列表
*/
@PostMapping("/apps/search")
@Operation(summary = "搜索运行时应用列表")
public R<Map<String, Object>> searchRuntimeApps(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String type = params.get("type") != null ? params.get("type").toString() : "node";
int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
int pageSize = params.get("pageSize") != null ? Integer.parseInt(params.get("pageSize").toString()) : 20;
Map<String, Object> result = onePanelService.searchRuntimeApps(serverId, type, page, pageSize);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 获取运行时版本详情
*/
@PostMapping("/detail")
@Operation(summary = "获取运行时版本详情")
public R<Map<String, Object>> getRuntimeDetail(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("appId") == null) {
return R.fail("appId不能为空");
}
if (params.get("version") == null) {
return R.fail("version不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
Long appId = Long.parseLong(params.get("appId").toString());
String version = params.get("version").toString();
Map<String, Object> result = onePanelService.getRuntimeDetail(serverId, appId, version);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 获取应用信息(包含版本列表)
*/
@PostMapping("/app/info")
@Operation(summary = "获取应用信息")
public R<Map<String, Object>> getAppInfo(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("appKey") == null) {
return R.fail("appKey不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String appKey = params.get("appKey").toString();
Map<String, Object> result = onePanelService.getAppInfo(serverId, appKey);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 创建运行时
*/
@PostMapping("/create")
@Operation(summary = "创建运行时")
public R<Void> createRuntime(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
params.remove("serverId");
Map<String, Object> result = onePanelService.createRuntime(serverId, params);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 获取容器日志
*/
@PostMapping("/container/log")
@Operation(summary = "获取容器日志")
public R<Map<String, Object>> getContainerLog(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String containerName = params.get("containerName") != null ? params.get("containerName").toString() : "";
String composePath = params.get("composePath") != null ? params.get("composePath").toString() : null;
if (containerName.isEmpty() && (composePath == null || composePath.isEmpty())) {
return R.fail("containerName和composePath不能同时为空");
}
Map<String, Object> result = onePanelService.getContainerLog(serverId, containerName, composePath);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok(result);
}
/**
* 更新运行时
*/
@PostMapping("/update")
@Operation(summary = "更新运行时")
public R<Void> updateRuntime(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("id") == null) {
return R.fail("id不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
params.remove("serverId");
Map<String, Object> result = onePanelService.updateRuntime(serverId, params);
if (result.containsKey("error")) {
return R.fail(result.get("error").toString());
}
return R.ok();
}
/**
* 获取Node脚本
*/
@PostMapping("/node/scripts")
@Operation(summary = "获取Node脚本")
public R<List<Map<String, Object>>> getNodeScripts(@RequestBody Map<String, Object> params) {
if (params.get("serverId") == null) {
return R.fail("serverId不能为空");
}
if (params.get("codeDir") == null) {
return R.fail("codeDir不能为空");
}
Long serverId = Long.parseLong(params.get("serverId").toString());
String codeDir = params.get("codeDir").toString();
List<Map<String, Object>> result = onePanelService.getNodeScripts(serverId, codeDir);
return R.ok(result);
}
}

View File

@@ -0,0 +1,153 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.dto.approval.ApprovalInstanceVO;
import com.nanxiislet.admin.dto.approval.ApprovalStats;
import com.nanxiislet.admin.dto.approval.ApprovalTemplateRequest;
import com.nanxiislet.admin.dto.approval.ApprovalTemplateVO;
import com.nanxiislet.admin.service.SysApprovalInstanceService;
import com.nanxiislet.admin.service.SysApprovalTemplateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 审批流程管理控制器
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Slf4j
@RestController
@RequestMapping("/system/approval")
@Tag(name = "审批流程管理", description = "审批模板、实例管理")
public class SysApprovalController {
@Resource
private SysApprovalTemplateService templateService;
@Resource
private SysApprovalInstanceService instanceService;
// ==================== 统计 ====================
@GetMapping("/stats")
@Operation(summary = "获取审批统计数据")
public R<ApprovalStats> getStats() {
return R.ok(templateService.getStats());
}
// ==================== 模板管理 ====================
@GetMapping("/template/page")
@Operation(summary = "分页查询模板列表")
public R<IPage<ApprovalTemplateVO>> templatePage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String scenario,
@RequestParam(required = false) Boolean enabled) {
return R.ok(templateService.listPage(page, pageSize, scenario, enabled));
}
@GetMapping("/template/list")
@Operation(summary = "获取所有模板列表")
public R<List<ApprovalTemplateVO>> templateList(
@RequestParam(required = false) String scenario) {
return R.ok(templateService.listAll(scenario));
}
@GetMapping("/template/{id}")
@Operation(summary = "获取模板详情")
public R<ApprovalTemplateVO> getTemplate(@PathVariable Long id) {
return R.ok(templateService.getDetail(id));
}
@PostMapping("/template")
@Operation(summary = "创建模板")
public R<Long> createTemplate(@RequestBody ApprovalTemplateRequest request) {
return R.ok(templateService.create(request));
}
@PutMapping("/template/{id}")
@Operation(summary = "更新模板")
public R<Void> updateTemplate(@PathVariable Long id, @RequestBody ApprovalTemplateRequest request) {
request.setId(id);
templateService.update(request);
return R.ok();
}
@DeleteMapping("/template/{id}")
@Operation(summary = "删除模板")
public R<Void> deleteTemplate(@PathVariable Long id) {
templateService.delete(id);
return R.ok();
}
@PostMapping("/template/{id}/toggle")
@Operation(summary = "切换模板启用状态")
public R<Void> toggleTemplate(@PathVariable Long id) {
templateService.toggle(id);
return R.ok();
}
// ==================== 实例管理 ====================
@GetMapping("/instance/page")
@Operation(summary = "分页查询实例列表")
public R<IPage<ApprovalInstanceVO>> instancePage(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String scenario,
@RequestParam(required = false) String status,
@RequestParam(required = false) String keyword) {
return R.ok(instanceService.listPage(page, pageSize, scenario, status, keyword));
}
@GetMapping("/instance/{id}")
@Operation(summary = "获取实例详情")
public R<ApprovalInstanceVO> getInstance(@PathVariable Long id) {
return R.ok(instanceService.getDetail(id));
}
@PostMapping("/instance/{id}/submit")
@Operation(summary = "提交审批")
public R<Void> submitInstance(@PathVariable Long id) {
instanceService.submit(id);
return R.ok();
}
@PostMapping("/instance/{id}/approve")
@Operation(summary = "审批操作")
public R<Void> approveInstance(@PathVariable Long id, @RequestBody ApproveRequest request) {
instanceService.approve(id, request.getNodeId(), request.getApproverId(), request.getAction(), request.getComment());
return R.ok();
}
@PostMapping("/instance/{id}/withdraw")
@Operation(summary = "撤回审批")
public R<Void> withdrawInstance(@PathVariable Long id) {
instanceService.withdraw(id);
return R.ok();
}
@PostMapping("/instance/{id}/cancel")
@Operation(summary = "取消审批")
public R<Void> cancelInstance(@PathVariable Long id) {
instanceService.cancel(id);
return R.ok();
}
@Data
public static class ApproveRequest {
private Long nodeId;
private Long approverId;
private String action; // approve/reject
private String comment;
}
}

View File

@@ -0,0 +1,73 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.entity.SysDept;
import com.nanxiislet.admin.service.SysDeptService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 部门管理控制器
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Slf4j
@RestController
@RequestMapping("/system/dept")
@Tag(name = "部门管理", description = "部门树形结构管理")
public class SysDeptController {
@Resource
private SysDeptService deptService;
@GetMapping("/tree")
@Operation(summary = "获取部门树")
public R<List<SysDept>> tree() {
return R.ok(deptService.listTree());
}
@GetMapping("/list")
@Operation(summary = "获取部门列表(扁平结构)")
public R<List<SysDept>> list() {
return R.ok(deptService.listAll());
}
@GetMapping("/{id}")
@Operation(summary = "获取部门详情")
public R<SysDept> getById(@PathVariable Long id) {
return R.ok(deptService.getById(id));
}
@PostMapping
@Operation(summary = "创建部门")
public R<Long> create(@RequestBody SysDept dept) {
return R.ok(deptService.create(dept));
}
@PutMapping("/{id}")
@Operation(summary = "更新部门")
public R<Void> update(@PathVariable Long id, @RequestBody SysDept dept) {
dept.setId(id);
deptService.updateDept(dept);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除部门")
public R<Void> delete(@PathVariable Long id) {
deptService.deleteDept(id);
return R.ok();
}
@GetMapping("/{id}/users")
@Operation(summary = "获取部门用户列表")
public R<List<Long>> getDeptUsers(@PathVariable Long id) {
return R.ok(deptService.getDeptUserIds(id));
}
}

View File

@@ -0,0 +1,126 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.entity.SysDict;
import com.nanxiislet.admin.entity.SysDictItem;
import com.nanxiislet.admin.service.SysDictItemService;
import com.nanxiislet.admin.service.SysDictService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 字典管理控制器
*
* @author NanxiIslet
* @since 2026-01-08
*/
@Slf4j
@RestController
@RequestMapping("/system/dict")
@Tag(name = "字典管理", description = "字典类型和字典项CRUD")
public class SysDictController {
@Resource
private SysDictService dictService;
@Resource
private SysDictItemService dictItemService;
// ==================== 字典类型 ====================
@GetMapping("/page")
@Operation(summary = "分页查询字典")
public R<IPage<SysDict>> page(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String code) {
return R.ok(dictService.page(page, pageSize, name, code));
}
@GetMapping("/list")
@Operation(summary = "获取所有字典")
public R<List<SysDict>> list() {
return R.ok(dictService.list());
}
@GetMapping("/{id}")
@Operation(summary = "获取字典详情")
public R<SysDict> getById(@PathVariable Long id) {
return R.ok(dictService.getById(id));
}
@GetMapping("/code/{code}")
@Operation(summary = "根据编码获取字典")
public R<SysDict> getByCode(@PathVariable String code) {
return R.ok(dictService.getByCode(code));
}
@PostMapping
@Operation(summary = "创建字典")
public R<Long> create(@RequestBody SysDict dict) {
return R.ok(dictService.create(dict));
}
@PutMapping("/{id}")
@Operation(summary = "更新字典")
public R<Void> update(@PathVariable Long id, @RequestBody SysDict dict) {
dict.setId(id);
dictService.updateDict(dict);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除字典")
public R<Void> delete(@PathVariable Long id) {
dictService.deleteDict(id);
return R.ok();
}
// ==================== 字典项 ====================
@GetMapping("/{dictId}/items")
@Operation(summary = "获取字典项列表")
public R<List<SysDictItem>> listItems(@PathVariable Long dictId) {
return R.ok(dictItemService.listByDictId(dictId));
}
@GetMapping("/code/{code}/items")
@Operation(summary = "根据字典编码获取字典项")
public R<List<SysDictItem>> listItemsByCode(@PathVariable String code) {
return R.ok(dictItemService.listByDictCode(code));
}
@GetMapping("/item/{id}")
@Operation(summary = "获取字典项详情")
public R<SysDictItem> getItemById(@PathVariable Long id) {
return R.ok(dictItemService.getById(id));
}
@PostMapping("/item")
@Operation(summary = "创建字典项")
public R<Long> createItem(@RequestBody SysDictItem item) {
return R.ok(dictItemService.create(item));
}
@PutMapping("/item/{id}")
@Operation(summary = "更新字典项")
public R<Void> updateItem(@PathVariable Long id, @RequestBody SysDictItem item) {
item.setId(id);
dictItemService.updateItem(item);
return R.ok();
}
@DeleteMapping("/item/{id}")
@Operation(summary = "删除字典项")
public R<Void> deleteItem(@PathVariable Long id) {
dictItemService.deleteItem(id);
return R.ok();
}
}

View File

@@ -0,0 +1,85 @@
package com.nanxiislet.admin.controller;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.entity.SysMenu;
import com.nanxiislet.admin.service.SysMenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 菜单管理控制器
*
* @author NanxiIslet
* @since 2024-01-08
*/
@Slf4j
@RestController
@RequestMapping("/system/menu")
@Tag(name = "菜单管理", description = "菜单CRUD等")
public class SysMenuController {
@Resource
private SysMenuService menuService;
@Resource
private com.nanxiislet.admin.service.PlatformProjectService projectService;
@GetMapping("/tree")
@Operation(summary = "获取菜单树")
public R<List<SysMenu>> tree() {
return R.ok(menuService.listTree());
}
@GetMapping("/list")
@Operation(summary = "获取所有菜单支持按项目ID筛选")
public R<List<SysMenu>> list(@RequestParam(required = false) Long projectId) {
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysMenu> wrapper =
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
if (projectId != null) {
// 特殊逻辑如果是主框架项目NanxiAdmin包含 projectId 为空的记录
// 为此我们需要先查项目信息
com.nanxiislet.admin.entity.PlatformProject project = projectService.getById(projectId);
if (project != null && ("NanxiAdmin".equalsIgnoreCase(project.getShortName()) || "NanxiAdmin".equals(project.getCode()))) {
wrapper.and(w -> w.eq(SysMenu::getProjectId, projectId).or().isNull(SysMenu::getProjectId));
} else {
wrapper.eq(SysMenu::getProjectId, projectId);
}
}
wrapper.orderByAsc(SysMenu::getParentId).orderByAsc(SysMenu::getSort);
return R.ok(menuService.list(wrapper));
}
@GetMapping("/{id}")
@Operation(summary = "获取菜单详情")
public R<SysMenu> getById(@PathVariable Long id) {
return R.ok(menuService.getById(id));
}
@PostMapping
@Operation(summary = "创建菜单")
public R<Long> create(@RequestBody SysMenu menu) {
return R.ok(menuService.create(menu));
}
@PutMapping("/{id}")
@Operation(summary = "更新菜单")
public R<Void> update(@PathVariable Long id, @RequestBody SysMenu menu) {
menu.setId(id);
menuService.updateMenu(menu);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除菜单")
public R<Void> delete(@PathVariable Long id) {
menuService.deleteMenu(id);
return R.ok();
}
}

View File

@@ -0,0 +1,99 @@
package com.nanxiislet.admin.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.entity.SysRole;
import com.nanxiislet.admin.service.SysRoleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 角色管理控制器
*
* @author NanxiIslet
* @since 2024-01-08
*/
@Slf4j
@RestController
@RequestMapping("/system/role")
@Tag(name = "角色管理", description = "角色CRUD、权限分配等")
public class SysRoleController {
@Resource
private SysRoleService roleService;
@GetMapping("/page")
@Operation(summary = "分页查询角色")
public R<IPage<SysRole>> page(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword) {
return R.ok(roleService.listPage(page, pageSize, keyword));
}
@GetMapping("/list")
@Operation(summary = "获取所有有效角色")
public R<List<SysRole>> list() {
return R.ok(roleService.listAll());
}
@GetMapping("/list-all")
@Operation(summary = "获取所有角色选项(简化版)")
public R<List<java.util.Map<String, String>>> listAll() {
List<SysRole> roles = roleService.listAll();
List<java.util.Map<String, String>> options = roles.stream()
.map(role -> {
java.util.Map<String, String> map = new java.util.HashMap<>();
map.put("code", role.getCode());
map.put("name", role.getName());
return map;
})
.toList();
return R.ok(options);
}
@GetMapping("/{id}")
@Operation(summary = "获取角色详情")
public R<SysRole> getById(@PathVariable Long id) {
return R.ok(roleService.getById(id));
}
@PostMapping
@Operation(summary = "创建角色")
public R<Long> create(@RequestBody SysRole role) {
return R.ok(roleService.create(role));
}
@PutMapping("/{id}")
@Operation(summary = "更新角色")
public R<Void> update(@PathVariable Long id, @RequestBody SysRole role) {
role.setId(id);
roleService.updateRole(role);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除角色")
public R<Void> delete(@PathVariable Long id) {
roleService.deleteRole(id);
return R.ok();
}
@GetMapping("/{id}/menus")
@Operation(summary = "获取角色的菜单ID列表")
public R<List<Long>> getRoleMenus(@PathVariable Long id) {
return R.ok(roleService.getRoleMenuIds(id));
}
@PostMapping("/{id}/menus")
@Operation(summary = "分配菜单权限")
public R<Void> assignMenus(@PathVariable Long id, @RequestBody List<Long> menuIds) {
roleService.assignMenus(id, menuIds);
return R.ok();
}
}

View File

@@ -0,0 +1,89 @@
package com.nanxiislet.admin.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/**
* 文件上传控制器
*/
@Slf4j
@RestController
@RequestMapping("/upload")
@Tag(name = "文件上传", description = "文件上传相关接口")
public class UploadController {
@Value("${nanxiislet.upload.path:./uploads}")
private String uploadPath;
@Value("${nanxiislet.upload.max-size:104857600}")
private long maxSize;
@PostMapping
@Operation(summary = "上传文件")
public R<Map<String, String>> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("请选择文件");
}
if (file.getSize() > maxSize) {
throw new BusinessException(ResultCode.FILE_SIZE_EXCEEDED);
}
try {
String originalFilename = file.getOriginalFilename();
String extension = FileUtil.extName(originalFilename);
String newFilename = IdUtil.fastSimpleUUID() + "." + extension;
// 按日期分目录
String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String relativePath = dateDir + "/" + newFilename;
String fullPath = uploadPath + "/" + relativePath;
File destFile = new File(fullPath);
FileUtil.mkParentDirs(destFile);
file.transferTo(destFile);
log.info("文件上传成功: {} -> {}", originalFilename, fullPath);
Map<String, String> result = new HashMap<>();
result.put("url", "/uploads/" + relativePath);
result.put("filename", originalFilename);
result.put("size", String.valueOf(file.getSize()));
return R.ok(result);
} catch (Exception e) {
log.error("文件上传失败", e);
throw new BusinessException(ResultCode.FILE_UPLOAD_ERROR);
}
}
@PostMapping("/image")
@Operation(summary = "上传图片")
public R<Map<String, String>> uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("请选择图片");
}
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new BusinessException(ResultCode.FILE_TYPE_NOT_ALLOWED, "只能上传图片文件");
}
return upload(file);
}
}

View File

@@ -0,0 +1,116 @@
package com.nanxiislet.admin.controller;
import cn.dev33.satoken.annotation.SaCheckRole;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nanxiislet.admin.common.exception.BusinessException;
import com.nanxiislet.admin.common.result.PageResult;
import com.nanxiislet.admin.common.result.R;
import com.nanxiislet.admin.common.result.ResultCode;
import com.nanxiislet.admin.dto.system.UserQuery;
import com.nanxiislet.admin.entity.SysUser;
import com.nanxiislet.admin.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
/**
* 用户管理控制器
*
* @author NanxiIslet
* @since 2024-01-06
*/
@RestController
@RequestMapping("/system/user")
@Tag(name = "用户管理", description = "系统用户的增删改查")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/list")
@Operation(summary = "用户列表", description = "分页查询用户列表")
@SaCheckRole("admin")
public R<PageResult<SysUser>> list(UserQuery query) {
Page<SysUser> page = userService.listPage(query);
return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()));
}
@GetMapping("/{id}")
@Operation(summary = "用户详情", description = "根据ID获取用户详情")
@SaCheckRole("admin")
public R<SysUser> getById(@PathVariable Long id) {
SysUser user = userService.getById(id);
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_EXIST);
}
user.setPassword(null);
return R.ok(user);
}
@PostMapping
@Operation(summary = "新增用户", description = "创建新用户")
@SaCheckRole("admin")
public R<Void> create(@Valid @RequestBody SysUser user) {
userService.createUser(user);
return R.ok();
}
@PutMapping("/{id}")
@Operation(summary = "更新用户", description = "更新用户信息")
@SaCheckRole("admin")
public R<Void> update(@PathVariable Long id, @Valid @RequestBody SysUser user) {
user.setId(id);
userService.updateUser(user);
return R.ok();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户", description = "删除用户")
@SaCheckRole("admin")
public R<Void> delete(@PathVariable Long id) {
userService.removeById(id);
return R.ok();
}
@PostMapping("/{id}/reset-password")
@Operation(summary = "重置密码", description = "管理员重置用户密码")
@SaCheckRole("admin")
public R<Void> resetPassword(@PathVariable Long id, @RequestBody ResetPasswordRequest request) {
userService.resetPassword(id, request.getNewPassword());
return R.ok();
}
@PostMapping("/change-password")
@Operation(summary = "修改密码", description = "用户修改自己的密码")
public R<Void> changePassword(@RequestBody ChangePasswordRequest request) {
cn.dev33.satoken.stp.StpUtil.checkLogin();
Long userId = Long.parseLong(cn.dev33.satoken.stp.StpUtil.getLoginId().toString());
userService.changePassword(userId, request.getOldPassword(), request.getNewPassword());
return R.ok();
}
@PutMapping("/{id}/status")
@Operation(summary = "修改状态", description = "启用/禁用用户")
@SaCheckRole("admin")
public R<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
SysUser user = new SysUser();
user.setId(id);
user.setStatus(status);
userService.updateById(user);
return R.ok();
}
@Data
public static class ResetPasswordRequest {
private String newPassword;
}
@Data
public static class ChangePasswordRequest {
private String oldPassword;
private String newPassword;
}
}

View File

@@ -0,0 +1,46 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 证书申请请求DTO
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@Schema(description = "证书申请请求")
public class CertificateApplyRequest {
@NotNull(message = "服务器ID不能为空")
@Schema(description = "服务器ID")
private Long serverId;
@NotBlank(message = "主域名不能为空")
@Schema(description = "主域名")
private String primaryDomain;
@Schema(description = "其他域名(逗号分隔)")
private String otherDomains;
@NotNull(message = "Acme账户ID不能为空")
@Schema(description = "Acme账户ID")
private Long acmeAccountId;
@NotNull(message = "DNS账户ID不能为空")
@Schema(description = "DNS账户ID")
private Long dnsAccountId;
@Schema(description = "密钥算法 P256/P384/RSA2048/RSA4096")
private String keyType = "P256";
@Schema(description = "自动续签")
private Boolean autoRenew = true;
@Schema(description = "备注")
private String description;
}

View File

@@ -0,0 +1,80 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 证书申请结果DTO
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@Schema(description = "证书申请结果")
public class CertificateApplyResult {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "结果消息")
private String message;
@Schema(description = "1Panel证书ID")
private Long panelSslId;
@Schema(description = "本地证书记录ID")
private Long certificateId;
@Schema(description = "申请步骤")
private List<ApplyStep> steps = new ArrayList<>();
@Data
@Schema(description = "申请步骤")
public static class ApplyStep {
@Schema(description = "步骤名称")
private String step;
@Schema(description = "状态 success/failed/skipped")
private String status;
@Schema(description = "消息")
private String message;
public ApplyStep() {}
public ApplyStep(String step, String status, String message) {
this.step = step;
this.status = status;
this.message = message;
}
}
public static CertificateApplyResult success(String message) {
CertificateApplyResult result = new CertificateApplyResult();
result.setSuccess(true);
result.setMessage(message);
return result;
}
public static CertificateApplyResult failed(String message) {
CertificateApplyResult result = new CertificateApplyResult();
result.setSuccess(false);
result.setMessage(message);
return result;
}
public void addSuccessStep(String step, String message) {
this.steps.add(new ApplyStep(step, "success", message));
}
public void addFailedStep(String step, String message) {
this.steps.add(new ApplyStep(step, "failed", message));
}
public void addSkippedStep(String step, String message) {
this.steps.add(new ApplyStep(step, "skipped", message));
}
}

View File

@@ -0,0 +1,30 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 部署请求DTO
*
* @author NanxiIslet
* @since 2024-01-10
*/
@Data
@Schema(description = "部署请求")
public class DeployRequest {
@Schema(description = "项目ID", required = true)
private Long projectId;
@Schema(description = "是否启用HTTPS")
private Boolean enableHttps = true;
@Schema(description = "Acme账户ID启用HTTPS时需要")
private Long acmeAccountId;
@Schema(description = "DNS账户ID启用HTTPS时需要")
private Long dnsAccountId;
@Schema(description = "如果网站不存在是否自动创建")
private Boolean createIfNotExist = true;
}

View File

@@ -0,0 +1,114 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 部署结果DTO
*
* @author NanxiIslet
* @since 2024-01-10
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "部署结果")
public class DeployResult {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "结果消息")
private String message;
@Schema(description = "1Panel网站ID")
private Long websiteId;
@Schema(description = "1Panel证书ID")
private Long sslCertificateId;
@Schema(description = "部署步骤详情")
@Builder.Default
private List<DeployStep> steps = new ArrayList<>();
/**
* 部署步骤
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DeployStep {
@Schema(description = "步骤名称")
private String step;
@Schema(description = "步骤状态 success/failed/skipped")
private String status;
@Schema(description = "步骤消息")
private String message;
}
/**
* 添加成功步骤
*/
public void addSuccessStep(String stepName, String message) {
this.steps.add(DeployStep.builder()
.step(stepName)
.status("success")
.message(message)
.build());
}
/**
* 添加失败步骤
*/
public void addFailedStep(String stepName, String message) {
this.steps.add(DeployStep.builder()
.step(stepName)
.status("failed")
.message(message)
.build());
}
/**
* 添加跳过步骤
*/
public void addSkippedStep(String stepName, String message) {
this.steps.add(DeployStep.builder()
.step(stepName)
.status("skipped")
.message(message)
.build());
}
/**
* 创建成功结果
*/
public static DeployResult success(String message) {
return DeployResult.builder()
.success(true)
.message(message)
.steps(new ArrayList<>())
.build();
}
/**
* 创建失败结果
*/
public static DeployResult failed(String message) {
return DeployResult.builder()
.success(false)
.message(message)
.steps(new ArrayList<>())
.build();
}
}

View File

@@ -0,0 +1,33 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
/**
* 域名部署请求DTO
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@Schema(description = "域名部署请求")
public class DomainDeployRequest {
@NotNull(message = "域名ID不能为空")
@Schema(description = "域名ID")
private Long domainId;
@Schema(description = "是否启用HTTPS")
private Boolean enableHttps;
@Schema(description = "Acme账户ID申请SSL证书时需要")
private Long acmeAccountId;
@Schema(description = "DNS账户ID申请SSL证书时需要")
private Long dnsAccountId;
@Schema(description = "如果网站不存在是否自动创建")
private Boolean createIfNotExist = true;
}

View File

@@ -0,0 +1,80 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 域名部署结果DTO
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@Schema(description = "域名部署结果")
public class DomainDeployResult {
@Schema(description = "是否成功")
private Boolean success;
@Schema(description = "结果消息")
private String message;
@Schema(description = "1Panel网站ID")
private Long websiteId;
@Schema(description = "1Panel证书ID")
private Long sslCertificateId;
@Schema(description = "部署步骤列表")
private List<DeployStep> steps = new ArrayList<>();
@Data
@Schema(description = "部署步骤")
public static class DeployStep {
@Schema(description = "步骤名称")
private String step;
@Schema(description = "状态 success/failed/skipped")
private String status;
@Schema(description = "消息")
private String message;
public DeployStep() {}
public DeployStep(String step, String status, String message) {
this.step = step;
this.status = status;
this.message = message;
}
}
public static DomainDeployResult success(String message) {
DomainDeployResult result = new DomainDeployResult();
result.setSuccess(true);
result.setMessage(message);
return result;
}
public static DomainDeployResult failed(String message) {
DomainDeployResult result = new DomainDeployResult();
result.setSuccess(false);
result.setMessage(message);
return result;
}
public void addSuccessStep(String step, String message) {
this.steps.add(new DeployStep(step, "success", message));
}
public void addFailedStep(String step, String message) {
this.steps.add(new DeployStep(step, "failed", message));
}
public void addSkippedStep(String step, String message) {
this.steps.add(new DeployStep(step, "skipped", message));
}
}

View File

@@ -0,0 +1,26 @@
package com.nanxiislet.admin.dto;
import com.nanxiislet.admin.common.base.BasePageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 域名查询参数
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DomainQueryDTO extends BasePageQuery {
@Schema(description = "服务器ID")
private Long serverId;
@Schema(description = "域名状态")
private String status;
@Schema(description = "SSL状态")
private String sslStatus;
}

View File

@@ -0,0 +1,30 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 域名统计DTO
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@Schema(description = "域名统计信息")
public class DomainStatsDTO {
@Schema(description = "总计")
private Long total;
@Schema(description = "正常数量")
private Long active;
@Schema(description = "待配置数量")
private Long pending;
@Schema(description = "SSL即将过期数量")
private Long sslExpiring;
@Schema(description = "已部署数量")
private Long deployed;
}

View File

@@ -0,0 +1,130 @@
package com.nanxiislet.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* 服务器信息DTO包含实时资源使用情况
*
* @author NanxiIslet
* @since 2024-01-10
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "服务器信息DTO")
public class ServerInfoDto {
@Schema(description = "服务器ID")
private Long id;
@Schema(description = "服务器名称")
private String name;
@Schema(description = "公网IP")
private String ip;
@Schema(description = "内网IP")
private String internalIp;
@Schema(description = "SSH端口")
private Integer port;
@Schema(description = "服务器类型 physical/virtual/cloud")
private String type;
@Schema(description = "状态 online/offline/warning/maintenance")
private String status;
@Schema(description = "操作系统")
private String os;
@Schema(description = "标签")
private List<String> tags;
@Schema(description = "1Panel面板地址")
private String panelUrl;
@Schema(description = "1Panel面板端口")
private Integer panelPort;
@Schema(description = "描述")
private String description;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Schema(description = "更新时间")
private LocalDateTime updatedAt;
// ==================== 资源使用情况从1Panel获取 ====================
@Schema(description = "CPU信息")
private CpuInfo cpu;
@Schema(description = "内存信息")
private MemoryInfo memory;
@Schema(description = "磁盘信息")
private DiskInfo disk;
/**
* CPU信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CpuInfo {
@Schema(description = "CPU核心数")
private Integer cores;
@Schema(description = "CPU使用率(%)")
private Double usage;
}
/**
* 内存信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MemoryInfo {
@Schema(description = "总内存(GB)")
private Double total;
@Schema(description = "已用内存(GB)")
private Double used;
@Schema(description = "内存使用率(%)")
private Double usage;
}
/**
* 磁盘信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DiskInfo {
@Schema(description = "总磁盘(GB)")
private Double total;
@Schema(description = "已用磁盘(GB)")
private Double used;
@Schema(description = "磁盘使用率(%)")
private Double usage;
}
}

View File

@@ -0,0 +1,24 @@
package com.nanxiislet.admin.dto.approval;
import com.nanxiislet.admin.entity.SysApprovalInstance;
import com.nanxiislet.admin.entity.SysApprovalRecord;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 审批实例VO包含审批记录
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ApprovalInstanceVO extends SysApprovalInstance {
/**
* 审批记录列表
*/
private List<SysApprovalRecord> records;
}

View File

@@ -0,0 +1,48 @@
package com.nanxiislet.admin.dto.approval;
import lombok.Data;
/**
* 审批统计数据
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
public class ApprovalStats {
/**
* 模板总数
*/
private Long totalTemplates;
/**
* 已启用模板数
*/
private Long enabledTemplates;
/**
* 实例总数
*/
private Long totalInstances;
/**
* 待处理实例数
*/
private Long pendingInstances;
/**
* 审批中实例数
*/
private Long inProgressInstances;
/**
* 已通过实例数
*/
private Long approvedInstances;
/**
* 已拒绝实例数
*/
private Long rejectedInstances;
}

View File

@@ -0,0 +1,46 @@
package com.nanxiislet.admin.dto.approval;
import com.nanxiislet.admin.entity.SysApprovalNode;
import lombok.Data;
import java.util.List;
/**
* 创建/更新审批模板请求
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
public class ApprovalTemplateRequest {
/**
* 模板ID更新时需要
*/
private Long id;
/**
* 模板名称
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 适用场景
*/
private String scenario;
/**
* 是否启用
*/
private Boolean enabled;
/**
* 审批节点列表
*/
private List<SysApprovalNode> nodes;
}

View File

@@ -0,0 +1,24 @@
package com.nanxiislet.admin.dto.approval;
import com.nanxiislet.admin.entity.SysApprovalNode;
import com.nanxiislet.admin.entity.SysApprovalTemplate;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 审批模板VO包含节点信息
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ApprovalTemplateVO extends SysApprovalTemplate {
/**
* 审批节点列表
*/
private List<SysApprovalNode> nodes;
}

View File

@@ -0,0 +1,27 @@
package com.nanxiislet.admin.dto.auth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 验证码响应DTO
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "验证码响应")
public class CaptchaResponse {
@Schema(description = "验证码Key")
private String captchaKey;
@Schema(description = "验证码图片Base64")
private String captchaImage;
}

View File

@@ -0,0 +1,32 @@
package com.nanxiislet.admin.dto.auth;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 登录请求DTO
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@Schema(description = "登录请求")
public class LoginRequest {
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "密码不能为空")
private String password;
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "验证码不能为空")
private String captcha;
@Schema(description = "验证码Key", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "验证码Key不能为空")
private String captchaKey;
}

View File

@@ -0,0 +1,33 @@
package com.nanxiislet.admin.dto.auth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 登录响应DTO
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "登录响应")
public class LoginResponse {
@Schema(description = "访问令牌")
private String token;
@Schema(description = "刷新令牌")
private String refreshToken;
@Schema(description = "过期时间(秒)")
private long expires;
@Schema(description = "用户信息")
private UserInfoVO userInfo;
}

View File

@@ -0,0 +1,56 @@
package com.nanxiislet.admin.dto.auth;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 用户信息VO
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户信息")
public class UserInfoVO {
@Schema(description = "用户ID")
private Long id;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "头像")
private String avatar;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String phone;
@Schema(description = "角色")
private String role;
@Schema(description = "权限列表")
private List<String> permissions;
@Schema(description = "菜单列表")
private List<com.nanxiislet.admin.entity.SysMenu> menus;
@Schema(description = "创建时间")
private String createTime;
@Schema(description = "最后登录时间")
private String lastLoginTime;
}

View File

@@ -0,0 +1,36 @@
package com.nanxiislet.admin.dto.query;
import com.nanxiislet.admin.common.base.BasePageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支出查询条件
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "支出查询条件")
public class ExpenseQuery extends BasePageQuery {
@Schema(description = "支出类型")
private String type;
@Schema(description = "项目ID")
private Long projectId;
@Schema(description = "部门ID")
private Long departmentId;
@Schema(description = "状态")
private String status;
@Schema(description = "开始日期")
private String startDate;
@Schema(description = "结束日期")
private String endDate;
}

View File

@@ -0,0 +1,36 @@
package com.nanxiislet.admin.dto.query;
import com.nanxiislet.admin.common.base.BasePageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 收入查询条件
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "收入查询条件")
public class IncomeQuery extends BasePageQuery {
@Schema(description = "收入类型")
private String type;
@Schema(description = "客户ID")
private Long customerId;
@Schema(description = "项目ID")
private Long projectId;
@Schema(description = "状态")
private String status;
@Schema(description = "开始日期")
private String startDate;
@Schema(description = "结束日期")
private String endDate;
}

View File

@@ -0,0 +1,27 @@
package com.nanxiislet.admin.dto.query;
import com.nanxiislet.admin.common.base.BasePageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 项目查询条件
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "项目查询条件")
public class ProjectQuery extends BasePageQuery {
@Schema(description = "项目类型")
private String type;
@Schema(description = "服务器ID")
private Long serverId;
@Schema(description = "状态")
private String status;
}

View File

@@ -0,0 +1,26 @@
package com.nanxiislet.admin.dto.system;
import com.nanxiislet.admin.common.base.BasePageQuery;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户查询参数
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserQuery extends BasePageQuery {
@Schema(description = "角色编码")
private String role;
@Schema(description = "部门ID")
private Long deptId;
@Schema(description = "状态 0-禁用 1-正常")
private Integer status;
}

View File

@@ -0,0 +1,58 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 财务账户实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("finance_account")
@Schema(description = "财务账户")
public class FinanceAccount extends BaseEntity {
@Schema(description = "账户名称")
private String name;
@Schema(description = "账户类型 corporate-对公账户 merchant-商户号")
private String type;
@Schema(description = "开户银行")
private String bankName;
@Schema(description = "开户支行")
private String bankBranch;
@Schema(description = "银行账号")
private String accountNo;
@Schema(description = "商户ID")
private String merchantId;
@Schema(description = "商户平台 wechat/alipay/unionpay/other")
private String merchantPlatform;
@Schema(description = "AppID")
private String appId;
@Schema(description = "当前余额")
private BigDecimal balance;
@Schema(description = "状态 active-正常 inactive-禁用")
private String status;
@Schema(description = "是否默认账户")
private Boolean isDefault;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,88 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
/**
* 预算记录实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "finance_budget", autoResultMap = true)
@Schema(description = "预算记录")
public class FinanceBudget extends BaseEntity {
@Schema(description = "预算名称")
private String name;
@Schema(description = "预算周期 monthly/quarterly/yearly")
private String period;
@Schema(description = "年份")
private Integer year;
@Schema(description = "季度 1-4")
private Integer quarter;
@Schema(description = "月份 1-12")
private Integer month;
@Schema(description = "部门ID")
private Long departmentId;
@Schema(description = "部门名称")
private String departmentName;
@Schema(description = "项目ID")
private Long projectId;
@Schema(description = "项目名称")
private String projectName;
@Schema(description = "预算明细(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<BudgetItem> items;
@Schema(description = "总预算")
private BigDecimal totalBudget;
@Schema(description = "已使用金额")
private BigDecimal usedAmount;
@Schema(description = "剩余金额")
private BigDecimal remainingAmount;
@Schema(description = "使用率 0-100")
private BigDecimal usageRate;
@Schema(description = "状态 draft/active/completed/cancelled")
private String status;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建人名称")
private String createdByName;
/**
* 预算明细项
*/
@Data
public static class BudgetItem {
private String expenseType;
private BigDecimal budgetAmount;
private BigDecimal usedAmount;
private BigDecimal remainingAmount;
}
}

View File

@@ -0,0 +1,83 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 支出记录实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("finance_expense")
@Schema(description = "支出记录")
public class FinanceExpense extends BaseEntity {
@Schema(description = "支出编号")
private String expenseNo;
@Schema(description = "支出类型 salary/office/rent/travel/marketing/equipment/service/tax/social_insurance/other")
private String type;
@Schema(description = "支出名称")
private String title;
@Schema(description = "收款方名称")
private String payeeName;
@Schema(description = "收款方账号")
private String payeeAccount;
@Schema(description = "收款方开户行")
private String payeeBankName;
@Schema(description = "金额")
private BigDecimal amount;
@Schema(description = "关联项目ID")
private Long projectId;
@Schema(description = "关联项目名称")
private String projectName;
@Schema(description = "部门ID")
private Long departmentId;
@Schema(description = "部门名称")
private String departmentName;
@Schema(description = "付款账户ID")
private Long accountId;
@Schema(description = "付款账户名称")
private String accountName;
@Schema(description = "状态 draft/pending/approved/paid/rejected")
private String status;
@Schema(description = "审批流程ID")
private Long approvalId;
@Schema(description = "审批状态")
private String approvalStatus;
@Schema(description = "附件路径(JSON格式)")
private String attachments;
@Schema(description = "付款日期")
private LocalDate paymentDate;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建人名称")
private String createdByName;
}

View File

@@ -0,0 +1,89 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 收入记录实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("finance_income")
@Schema(description = "收入记录")
public class FinanceIncome extends BaseEntity {
@Schema(description = "收入编号")
private String incomeNo;
@Schema(description = "收入类型 project/service_fee/consulting/commission/other")
private String type;
@Schema(description = "收入名称")
private String title;
@Schema(description = "客户ID")
private Long customerId;
@Schema(description = "客户名称")
private String customerName;
@Schema(description = "客户联系方式")
private String customerContact;
@Schema(description = "关联项目ID")
private Long projectId;
@Schema(description = "关联项目名称")
private String projectName;
@Schema(description = "合同编号")
private String contractNo;
@Schema(description = "总金额")
private BigDecimal totalAmount;
@Schema(description = "已收金额")
private BigDecimal receivedAmount;
@Schema(description = "待收金额")
private BigDecimal pendingAmount;
@Schema(description = "收款账户ID")
private Long accountId;
@Schema(description = "收款账户名称")
private String accountName;
@Schema(description = "状态 pending/partial/received/overdue")
private String status;
@Schema(description = "预计收款日期")
private LocalDate expectedDate;
@Schema(description = "实际收款日期")
private LocalDate actualDate;
@Schema(description = "是否需要发票")
private Boolean invoiceRequired;
@Schema(description = "发票是否已开")
private Boolean invoiceIssued;
@Schema(description = "发票号")
private String invoiceNo;
@Schema(description = "备注")
private String remark;
@Schema(description = "创建人名称")
private String createdByName;
}

View File

@@ -0,0 +1,53 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 发票记录实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("finance_invoice")
@Schema(description = "发票记录")
public class FinanceInvoice extends BaseEntity {
@Schema(description = "关联结算单ID")
private Long settlementId;
@Schema(description = "发票类型 vat_special/vat_normal/personal")
private String type;
@Schema(description = "发票抬头")
private String title;
@Schema(description = "税号")
private String taxCode;
@Schema(description = "金额")
private BigDecimal amount;
@Schema(description = "电子发票文件URL")
private String fileUrl;
@Schema(description = "状态 pending/issued/rejected")
private String status;
@Schema(description = "提交时间")
private LocalDate submitTime;
@Schema(description = "开票时间")
private LocalDate issueTime;
@Schema(description = "驳回原因")
private String rejectReason;
}

View File

@@ -0,0 +1,100 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* 报销记录实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName(value = "finance_reimbursement", autoResultMap = true)
@Schema(description = "报销记录")
public class FinanceReimbursement extends BaseEntity {
@Schema(description = "报销单号")
private String reimbursementNo;
@Schema(description = "报销类型 travel/meal/transport/communication/office/other")
private String type;
@Schema(description = "报销标题")
private String title;
@Schema(description = "申请人ID")
private Long applicantId;
@Schema(description = "申请人名称")
private String applicantName;
@Schema(description = "部门ID")
private Long departmentId;
@Schema(description = "部门名称")
private String departmentName;
@Schema(description = "总金额")
private BigDecimal totalAmount;
@Schema(description = "报销明细(JSON格式)")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<ReimbursementItem> items;
@Schema(description = "状态 draft/pending/approved/paid/rejected")
private String status;
@Schema(description = "审批流程ID")
private Long approvalId;
@Schema(description = "审批状态")
private String approvalStatus;
@Schema(description = "当前审批人")
private String currentApprover;
@Schema(description = "收款人银行户名")
private String bankAccountName;
@Schema(description = "收款人银行账号")
private String bankAccountNo;
@Schema(description = "收款人开户行")
private String bankName;
@Schema(description = "付款账户ID")
private Long paymentAccountId;
@Schema(description = "付款日期")
private LocalDate paymentDate;
@Schema(description = "付款备注")
private String paymentRemark;
@Schema(description = "备注")
private String remark;
/**
* 报销明细项
*/
@Data
public static class ReimbursementItem {
private Long id;
private String type;
private String description;
private BigDecimal amount;
private LocalDate occurDate;
private List<String> attachments;
}
}

View File

@@ -0,0 +1,80 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* 结算记录实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("finance_settlement")
@Schema(description = "结算记录")
public class FinanceSettlement extends BaseEntity {
@Schema(description = "项目ID")
private Long projectId;
@Schema(description = "项目名称")
private String projectName;
@Schema(description = "人才ID")
private Long talentId;
@Schema(description = "人才名称")
private String talentName;
@Schema(description = "结算周期/月份")
private String period;
@Schema(description = "项目总额")
private BigDecimal totalAmount;
@Schema(description = "平台服务费")
private BigDecimal platformFee;
@Schema(description = "应纳税所得额")
private BigDecimal taxableAmount;
@Schema(description = "税率")
private BigDecimal taxRate;
@Schema(description = "扣税金额")
private BigDecimal taxAmount;
@Schema(description = "实发金额")
private BigDecimal actualAmount;
@Schema(description = "状态 pending/paying/completed/rejected")
private String status;
@Schema(description = "发票状态 none/pending/received")
private String invoiceStatus;
@Schema(description = "银行户名")
private String bankAccountName;
@Schema(description = "银行账号")
private String bankAccountNo;
@Schema(description = "开户银行")
private String bankName;
@Schema(description = "审核时间")
private LocalDate auditTime;
@Schema(description = "打款时间")
private LocalDate paymentTime;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,89 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 证书实体
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("platform_certificate")
@Schema(description = "证书信息")
public class PlatformCertificate extends BaseEntity {
@Schema(description = "关联服务器ID")
private Long serverId;
@Schema(description = "服务器名称")
private String serverName;
@Schema(description = "1Panel证书ID")
private Long panelSslId;
@Schema(description = "主域名")
private String primaryDomain;
@Schema(description = "其他域名(逗号分隔)")
private String otherDomains;
@Schema(description = "证书主体名称")
private String cn;
@Schema(description = "颁发组织")
private String organization;
@Schema(description = "验证方式 dnsAccount/httpManual")
private String provider;
@Schema(description = "Acme账户ID")
private Long acmeAccountId;
@Schema(description = "Acme账户邮箱")
private String acmeAccountEmail;
@Schema(description = "DNS账户ID")
private Long dnsAccountId;
@Schema(description = "DNS账户名称")
private String dnsAccountName;
@Schema(description = "DNS账户类型")
private String dnsAccountType;
@Schema(description = "密钥算法 P256/P384/RSA2048/RSA4096")
private String keyType;
@Schema(description = "状态 pending/valid/expired/error")
private String status;
@Schema(description = "自动续签")
private Boolean autoRenew;
@Schema(description = "生效时间")
private LocalDate startDate;
@Schema(description = "过期时间")
private LocalDate expireDate;
@Schema(description = "证书内容")
private String certContent;
@Schema(description = "私钥内容")
private String keyContent;
@Schema(description = "备注")
private String description;
@Schema(description = "最后同步时间")
private LocalDateTime lastSyncTime;
}

View File

@@ -0,0 +1,115 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
/**
* 域名实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("platform_domain")
@Schema(description = "域名信息")
public class PlatformDomain extends BaseEntity {
@Schema(description = "域名")
private String domain;
@Schema(description = "关联项目ID")
private Long projectId;
@Schema(description = "关联项目名称")
private String projectName;
@Schema(description = "关联服务器ID")
private Long serverId;
@Schema(description = "关联服务器名称")
private String serverName;
@Schema(description = "服务器IP")
private String serverIp;
@Schema(description = "状态 active/pending/expired/error")
private String status;
@Schema(description = "DNS状态 resolved/unresolved/checking")
private String dnsStatus;
@Schema(description = "DNS记录(JSON格式)")
private String dnsRecords;
@Schema(description = "SSL状态 valid/expiring/expired/none")
private String sslStatus;
@Schema(description = "SSL过期时间")
private LocalDate sslExpireDate;
@Schema(description = "证书ID")
private String certificateId;
@Schema(description = "证书名称")
private String certificateName;
@Schema(description = "Nginx配置路径")
private String nginxConfigPath;
@Schema(description = "代理地址")
private String proxyPass;
@Schema(description = "端口")
private Integer port;
@Schema(description = "是否启用HTTPS")
private Boolean enableHttps;
@Schema(description = "是否强制HTTPS")
private Boolean forceHttps;
@Schema(description = "描述")
private String description;
@Schema(description = "1Panel网站ID")
private Long panelWebsiteId;
@Schema(description = "1Panel证书ID")
private Long panelSslId;
@Schema(description = "网站目录路径")
private String sitePath;
@Schema(description = "网站别名")
private String alias;
@Schema(description = "部署状态 not_deployed/deploying/deployed/failed")
private String deployStatus;
@Schema(description = "最后部署时间")
private java.time.LocalDateTime lastDeployTime;
@Schema(description = "最后部署消息")
private String lastDeployMessage;
@Schema(description = "关联运行环境ID")
private Long runtimeId;
@Schema(description = "运行环境所属服务器ID")
private Long runtimeServerId;
@Schema(description = "运行环境名称")
private String runtimeName;
@Schema(description = "运行环境类型 java/node")
private String runtimeType;
@Schema(description = "运行环境部署状态 not_deployed/deploying/deployed/failed")
private String runtimeDeployStatus;
}

View File

@@ -0,0 +1,107 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 项目实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("platform_project")
@Schema(description = "项目信息")
public class PlatformProject extends BaseEntity {
@Schema(description = "项目名称")
private String name;
@Schema(description = "项目编码")
private String code;
@Schema(description = "项目简称")
private String shortName;
@Schema(description = "项目类型")
private String type;
@Schema(description = "项目分组 default/business/internal/test")
private String projectGroup;
@Schema(description = "Logo文字")
private String logo;
@Schema(description = "主题色")
private String color;
@Schema(description = "绑定域名")
private String domain;
@Schema(description = "访问地址")
private String url;
@Schema(description = "关联域名ID")
private Long domainId;
@Schema(description = "关联服务器ID")
private Long serverId;
@Schema(description = "服务器名称")
private String serverName;
@Schema(description = "部署路径")
private String deployPath;
@Schema(description = "当前版本")
private String version;
@Schema(description = "状态 active/inactive/deploying")
private String status;
@Schema(description = "图标")
private String icon;
@Schema(description = "描述")
private String description;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "是否启用HTTPS")
private Boolean enableHttps;
@Schema(description = "1Panel网站ID")
private Long panelWebsiteId;
@Schema(description = "1Panel证书ID")
private Long panelSslId;
@Schema(description = "最后部署时间")
private LocalDateTime lastDeployTime;
@Schema(description = "最后部署状态 success/failed/deploying")
private String lastDeployStatus;
@Schema(description = "最后部署消息")
private String lastDeployMessage;
@Schema(description = "系统类型 admin/portal字典项system")
private String systemType;
@Schema(description = "是否集成到框架仅管理端有效字典项integration")
private Boolean integrateToFramework;
@Schema(description = "菜单数量")
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long menuCount;
}

View File

@@ -0,0 +1,67 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 服务器实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("platform_server")
@Schema(description = "服务器信息")
public class PlatformServer extends BaseEntity {
@Schema(description = "服务器名称")
private String name;
@Schema(description = "公网IP")
private String ip;
@Schema(description = "内网IP")
private String internalIp;
@Schema(description = "SSH端口")
private Integer port;
@Schema(description = "服务器类型 physical/virtual/cloud")
private String type;
@Schema(description = "状态 online/offline/warning/maintenance")
private String status;
@Schema(description = "操作系统")
private String os;
@Schema(description = "CPU核心数")
private Integer cpuCores;
@Schema(description = "内存大小(GB)")
private Integer memoryTotal;
@Schema(description = "磁盘大小(GB)")
private Integer diskTotal;
@Schema(description = "标签(JSON格式)")
private String tags;
@Schema(description = "1Panel面板地址")
private String panelUrl;
@Schema(description = "1Panel面板端口")
private Integer panelPort;
@Schema(description = "1Panel API密钥")
private String panelApiKey;
@Schema(description = "描述")
private String description;
}

View File

@@ -0,0 +1,107 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批实例实体
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@TableName("sys_approval_instance")
public class SysApprovalInstance {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 模板ID
*/
private Long templateId;
/**
* 模板名称
*/
private String templateName;
/**
* 适用场景
*/
private String scenario;
/**
* 业务类型
*/
private String businessType;
/**
* 业务ID
*/
private Long businessId;
/**
* 业务标题
*/
private String businessTitle;
/**
* 发起人ID
*/
private Long initiatorId;
/**
* 发起人名称
*/
private String initiatorName;
/**
* 发起人头像
*/
private String initiatorAvatar;
/**
* 状态: pending-待提交 in_progress-审批中 approved-已通过 rejected-已拒绝 withdrawn-已撤回 cancelled-已取消
*/
private String status;
/**
* 当前节点ID
*/
private Long currentNodeId;
/**
* 当前节点名称
*/
private String currentNodeName;
/**
* 提交时间
*/
private LocalDateTime submittedAt;
/**
* 完成时间
*/
private LocalDateTime completedAt;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,89 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 审批流程节点实体
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@TableName(value = "sys_approval_node", autoResultMap = true)
public class SysApprovalNode {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 模板ID
*/
private Long templateId;
/**
* 节点名称
*/
private String name;
/**
* 审批人类型: specified-指定人员 role-按角色 superior-上级领导 self_select-发起人自选
*/
private String approverType;
/**
* 审批人ID列表
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Long> approverIds;
/**
* 审批角色编码
*/
private String approverRole;
/**
* 部门ID按部门时使用
*/
private Long deptId;
/**
* 部门名称
*/
private String deptName;
/**
* 审批方式: and-会签 or-或签
*/
private String approvalMode;
/**
* 超时时间(小时)
*/
private Integer timeoutHours;
/**
* 超时操作: skip-跳过 reject-驳回
*/
private String timeoutAction;
/**
* 节点顺序
*/
private Integer sort;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,64 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批记录实体
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@TableName("sys_approval_record")
public class SysApprovalRecord {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 实例ID
*/
private Long instanceId;
/**
* 节点ID
*/
private Long nodeId;
/**
* 节点名称
*/
private String nodeName;
/**
* 审批人ID
*/
private Long approverId;
/**
* 审批人名称
*/
private String approverName;
/**
* 审批人头像
*/
private String approverAvatar;
/**
* 操作: approve-通过 reject-驳回 transfer-转交 return-退回
*/
private String action;
/**
* 审批意见
*/
private String comment;
/**
* 操作时间
*/
private LocalDateTime operatedAt;
}

View File

@@ -0,0 +1,69 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 审批流程模板实体
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@TableName("sys_approval_template")
public class SysApprovalTemplate {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 模板名称
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 适用场景
*/
private String scenario;
/**
* 是否启用 0-禁用 1-启用
*/
private Integer enabled;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
/**
* 创建人ID
*/
@TableField(fill = FieldFill.INSERT)
private Long createdBy;
/**
* 更新人ID
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updatedBy;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,106 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 部门实体
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Data
@TableName("sys_dept")
public class SysDept {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 父部门ID0表示顶级部门
*/
private Long parentId;
/**
* 部门名称
*/
private String name;
/**
* 部门编码
*/
private String code;
/**
* 部门负责人ID
*/
private Long leaderId;
/**
* 部门负责人名称
*/
private String leaderName;
/**
* 联系电话
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 排序
*/
private Integer sort;
/**
* 状态 0-禁用 1-正常
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
/**
* 创建人ID
*/
@TableField(fill = FieldFill.INSERT)
private Long createdBy;
/**
* 更新人ID
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updatedBy;
/**
* 删除标记
*/
@TableLogic
private Integer deleted;
/**
* 子部门列表(非数据库字段)
*/
@TableField(exist = false)
private List<SysDept> children;
}

View File

@@ -0,0 +1,32 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 字典类型实体
*
* @author NanxiIslet
* @since 2026-01-08
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_dict")
@Schema(description = "字典类型")
public class SysDict extends BaseEntity {
@Schema(description = "字典名称")
private String name;
@Schema(description = "字典编码")
private String code;
@Schema(description = "备注")
private String remark;
@Schema(description = "状态 0-禁用 1-正常")
private Integer status;
}

View File

@@ -0,0 +1,41 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 字典项实体
*
* @author NanxiIslet
* @since 2026-01-08
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_dict_item")
@Schema(description = "字典项")
public class SysDictItem extends BaseEntity {
@Schema(description = "字典ID")
private Long dictId;
@Schema(description = "字典项标签")
private String label;
@Schema(description = "字典项值")
private String value;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "是否默认 0-否 1-是")
private Integer isDefault;
@Schema(description = "备注")
private String remark;
@Schema(description = "状态 0-禁用 1-正常")
private Integer status;
}

View File

@@ -0,0 +1,59 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 菜单实体
*
* @author NanxiIslet
* @since 2024-01-08
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_menu")
@Schema(description = "菜单信息")
public class SysMenu extends BaseEntity {
@Schema(description = "关联项目ID")
private Long projectId;
@Schema(description = "父菜单ID")
private Long parentId;
@Schema(description = "菜单名称")
private String name;
@Schema(description = "菜单编码/Key")
private String code;
@Schema(description = "菜单类型 directory-目录 menu-菜单 button-按钮")
private String type;
@Schema(description = "路由路径")
private String path;
@Schema(description = "组件路径")
private String component;
@Schema(description = "图标")
private String icon;
@Schema(description = "权限标识")
private String permission;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "是否隐藏 0-显示 1-隐藏")
private Integer hidden;
@Schema(description = "状态 0-禁用 1-正常")
private Integer status;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,35 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 角色实体
*
* @author NanxiIslet
* @since 2024-01-08
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_role")
@Schema(description = "角色信息")
public class SysRole extends BaseEntity {
@Schema(description = "角色编码")
private String code;
@Schema(description = "角色名称")
private String name;
@Schema(description = "角色描述")
private String description;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "状态 0-禁用 1-正常")
private Integer status;
}

View File

@@ -0,0 +1,35 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 角色菜单关联实体
*
* @author NanxiIslet
* @since 2024-01-08
*/
@Data
@TableName("sys_role_menu")
@Schema(description = "角色菜单关联")
public class SysRoleMenu implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "角色ID")
private Long roleId;
@Schema(description = "菜单ID")
private Long menuId;
}

View File

@@ -0,0 +1,59 @@
package com.nanxiislet.admin.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.nanxiislet.admin.common.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户实体
*
* @author NanxiIslet
* @since 2024-01-06
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
@Schema(description = "用户信息")
public class SysUser extends BaseEntity {
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "头像")
private String avatar;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String phone;
@Schema(description = "角色编码")
private String role;
@Schema(description = "部门ID")
private Long deptId;
@Schema(description = "部门名称")
private String deptName;
@Schema(description = "状态 0-禁用 1-正常")
private Integer status;
@Schema(description = "最后登录时间")
private java.time.LocalDateTime lastLoginTime;
@Schema(description = "最后登录IP")
private String lastLoginIp;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceAccount;
import org.apache.ibatis.annotations.Mapper;
/**
* 财务账户 Mapper
*/
@Mapper
public interface FinanceAccountMapper extends BaseMapper<FinanceAccount> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceBudget;
import org.apache.ibatis.annotations.Mapper;
/**
* 预算记录 Mapper
*/
@Mapper
public interface FinanceBudgetMapper extends BaseMapper<FinanceBudget> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceExpense;
import org.apache.ibatis.annotations.Mapper;
/**
* 支出记录 Mapper
*/
@Mapper
public interface FinanceExpenseMapper extends BaseMapper<FinanceExpense> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceIncome;
import org.apache.ibatis.annotations.Mapper;
/**
* 收入记录 Mapper
*/
@Mapper
public interface FinanceIncomeMapper extends BaseMapper<FinanceIncome> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceInvoice;
import org.apache.ibatis.annotations.Mapper;
/**
* 发票记录 Mapper
*/
@Mapper
public interface FinanceInvoiceMapper extends BaseMapper<FinanceInvoice> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceReimbursement;
import org.apache.ibatis.annotations.Mapper;
/**
* 报销记录 Mapper
*/
@Mapper
public interface FinanceReimbursementMapper extends BaseMapper<FinanceReimbursement> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.FinanceSettlement;
import org.apache.ibatis.annotations.Mapper;
/**
* 结算记录 Mapper
*/
@Mapper
public interface FinanceSettlementMapper extends BaseMapper<FinanceSettlement> {
}

View File

@@ -0,0 +1,15 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.PlatformCertificate;
import org.apache.ibatis.annotations.Mapper;
/**
* 证书 Mapper
*
* @author NanxiIslet
* @since 2026-01-13
*/
@Mapper
public interface PlatformCertificateMapper extends BaseMapper<PlatformCertificate> {
}

View File

@@ -0,0 +1,18 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.PlatformDomain;
import org.apache.ibatis.annotations.Mapper;
/**
* 域名 Mapper
*/
@Mapper
public interface PlatformDomainMapper extends BaseMapper<PlatformDomain> {
@org.apache.ibatis.annotations.Select("SELECT * FROM platform_domain WHERE domain = #{domain} LIMIT 1")
PlatformDomain findByDomainIncludeDeleted(@org.apache.ibatis.annotations.Param("domain") String domain);
@org.apache.ibatis.annotations.Update("UPDATE platform_domain SET deleted = 0, updated_at = NOW() WHERE id = #{id}")
void restoreById(@org.apache.ibatis.annotations.Param("id") Long id);
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.PlatformProject;
import org.apache.ibatis.annotations.Mapper;
/**
* 项目 Mapper
*/
@Mapper
public interface PlatformProjectMapper extends BaseMapper<PlatformProject> {
}

View File

@@ -0,0 +1,12 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.PlatformServer;
import org.apache.ibatis.annotations.Mapper;
/**
* 服务器 Mapper
*/
@Mapper
public interface PlatformServerMapper extends BaseMapper<PlatformServer> {
}

View File

@@ -0,0 +1,15 @@
package com.nanxiislet.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nanxiislet.admin.entity.SysApprovalInstance;
import org.apache.ibatis.annotations.Mapper;
/**
* 审批实例Mapper
*
* @author NanxiIslet
* @since 2026-01-09
*/
@Mapper
public interface SysApprovalInstanceMapper extends BaseMapper<SysApprovalInstance> {
}

Some files were not shown because too many files have changed in this diff Show More