commit 40db55e85d90c3dbef2b0d97d817d96fbaede313 Author: super Date: Tue Jan 27 20:48:47 2026 +0800 首次提交后端接口 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab4c169 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6f0589 --- /dev/null +++ b/README.md @@ -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 diff --git a/docs/domain_integration_guide.md b/docs/domain_integration_guide.md new file mode 100644 index 0000000..f02c338 --- /dev/null +++ b/docs/domain_integration_guide.md @@ -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. 部署完成后,项目的"部署"按钮会自动同步状态 diff --git a/init-database.bat b/init-database.bat new file mode 100644 index 0000000..fb7ff94 --- /dev/null +++ b/init-database.bat @@ -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 diff --git a/logs/nanxiislet-admin.log.2026-01-21.0.gz b/logs/nanxiislet-admin.log.2026-01-21.0.gz new file mode 100644 index 0000000..471cbee Binary files /dev/null and b/logs/nanxiislet-admin.log.2026-01-21.0.gz differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3588fa0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,223 @@ + + + 4.0.0 + + com.nanxiislet + nanxiislet-admin + 1.0.0-SNAPSHOT + jar + + NanxiIslet Admin Backend + 南溪小岛管理后台 - Spring Boot后端服务 + + + 21 + UTF-8 + UTF-8 + 3.3.4 + 3.5.7 + 0.12.6 + 5.8.31 + 4.5.0 + 1.39.0 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + com.mysql + mysql-connector-j + runtime + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + com.alibaba + druid-spring-boot-3-starter + 1.2.23 + + + + + cn.dev33 + sa-token-spring-boot3-starter + ${sa-token.version} + + + cn.dev33 + sa-token-redis-jackson + ${sa-token.version} + + + + + io.jsonwebtoken + jjwt-api + ${jwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jwt.version} + runtime + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + + cn.hutool + hutool-all + ${hutool.version} + + + + + org.projectlombok + lombok + true + + + + + org.springframework.security + spring-security-crypto + + + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-pool2 + + + + + commons-io + commons-io + 2.17.0 + + + + + com.alibaba + easyexcel + 4.0.3 + + + + + pro.fessional + kaptcha + 2.3.3 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + UTF-8 + + -parameters + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + org.projectlombok + lombok + + + + + + + repackage + + + + + + + + + + aliyun + Aliyun Maven Repository + https://maven.aliyun.com/repository/public + + + + diff --git a/src/main/java/com/nanxiislet/admin/NanxiisletAdminApplication.java b/src/main/java/com/nanxiislet/admin/NanxiisletAdminApplication.java new file mode 100644 index 0000000..c3a35c2 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/NanxiisletAdminApplication.java @@ -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("===================================================="); + } +} diff --git a/src/main/java/com/nanxiislet/admin/common/base/BaseEntity.java b/src/main/java/com/nanxiislet/admin/common/base/BaseEntity.java new file mode 100644 index 0000000..7e7c370 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/base/BaseEntity.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/common/base/BasePageQuery.java b/src/main/java/com/nanxiislet/admin/common/base/BasePageQuery.java new file mode 100644 index 0000000..b33232b --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/base/BasePageQuery.java @@ -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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/common/exception/BusinessException.java b/src/main/java/com/nanxiislet/admin/common/exception/BusinessException.java new file mode 100644 index 0000000..c381a85 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/exception/BusinessException.java @@ -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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/common/exception/GlobalExceptionHandler.java b/src/main/java/com/nanxiislet/admin/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..c074b18 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/exception/GlobalExceptionHandler.java @@ -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 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 handleNotLoginException(NotLoginException e, HttpServletRequest request) { + log.warn("未登录访问: {} - {}", request.getRequestURI(), e.getMessage()); + return R.fail(ResultCode.UNAUTHORIZED); + } + + /** + * 无权限异常 + */ + @ExceptionHandler(NotPermissionException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public R handleNotPermissionException(NotPermissionException e, HttpServletRequest request) { + log.warn("无权限访问: {} - {}", request.getRequestURI(), e.getMessage()); + return R.fail(ResultCode.FORBIDDEN); + } + + /** + * 无角色异常 + */ + @ExceptionHandler(NotRoleException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public R 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 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 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 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 handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { + log.warn("不支持的请求方法: {}", e.getMethod()); + return R.fail(ResultCode.METHOD_NOT_ALLOWED); + } + + /** + * 404异常 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public R handleNoHandlerFoundException(NoHandlerFoundException e) { + log.warn("资源不存在: {}", e.getRequestURL()); + return R.fail(ResultCode.NOT_FOUND); + } + + /** + * 数据库唯一键冲突异常 + */ + @ExceptionHandler(org.springframework.dao.DuplicateKeyException.class) + @ResponseStatus(HttpStatus.OK) + public R 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 handleException(Exception e, HttpServletRequest request) { + log.error("系统异常: {} - {}", request.getRequestURI(), e.getMessage(), e); + return R.fail(ResultCode.INTERNAL_ERROR, "系统繁忙,请稍后重试"); + } +} diff --git a/src/main/java/com/nanxiislet/admin/common/result/PageResult.java b/src/main/java/com/nanxiislet/admin/common/result/PageResult.java new file mode 100644 index 0000000..d4fae48 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/result/PageResult.java @@ -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 implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "数据列表") + private List 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 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 PageResult of(List records, long total, long page, long pageSize) { + return new PageResult<>(records, total, page, pageSize); + } + + /** + * 空分页结果 + */ + public static PageResult empty() { + return new PageResult<>(List.of(), 0, 1, 10); + } +} + diff --git a/src/main/java/com/nanxiislet/admin/common/result/R.java b/src/main/java/com/nanxiislet/admin/common/result/R.java new file mode 100644 index 0000000..b4b8052 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/result/R.java @@ -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 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 R ok() { + return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null); + } + + /** + * 成功 + */ + public static R ok(T data) { + return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); + } + + /** + * 成功 + */ + public static R ok(String message, T data) { + return new R<>(ResultCode.SUCCESS.getCode(), message, data); + } + + /** + * 失败 + */ + public static R fail() { + return new R<>(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), null); + } + + /** + * 失败 + */ + public static R fail(String message) { + return new R<>(ResultCode.FAILED.getCode(), message, null); + } + + /** + * 失败 + */ + public static R fail(int code, String message) { + return new R<>(code, message, null); + } + + /** + * 失败 + */ + public static R fail(ResultCode resultCode) { + return new R<>(resultCode.getCode(), resultCode.getMessage(), null); + } + + /** + * 失败 + */ + public static R fail(ResultCode resultCode, String message) { + return new R<>(resultCode.getCode(), message, null); + } + + /** + * 判断是否成功 + */ + public boolean isSuccess() { + return this.code == ResultCode.SUCCESS.getCode(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/common/result/ResultCode.java b/src/main/java/com/nanxiislet/admin/common/result/ResultCode.java new file mode 100644 index 0000000..f8d276b --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/common/result/ResultCode.java @@ -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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/CaptchaConfig.java b/src/main/java/com/nanxiislet/admin/config/CaptchaConfig.java new file mode 100644 index 0000000..63fcf56 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/CaptchaConfig.java @@ -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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/DatabaseInitializer.java b/src/main/java/com/nanxiislet/admin/config/DatabaseInitializer.java new file mode 100644 index 0000000..db0e2a6 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/DatabaseInitializer.java @@ -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); + } + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/MybatisPlusConfig.java b/src/main/java/com/nanxiislet/admin/config/MybatisPlusConfig.java new file mode 100644 index 0000000..dccd67f --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/MybatisPlusConfig.java @@ -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; + } + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/RedisConfig.java b/src/main/java/com/nanxiislet/admin/config/RedisConfig.java new file mode 100644 index 0000000..888bcff --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate 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 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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/StpInterfaceImpl.java b/src/main/java/com/nanxiislet/admin/config/StpInterfaceImpl.java new file mode 100644 index 0000000..3bff68d --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/StpInterfaceImpl.java @@ -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 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 menus = menuService.getMenusByRoleCode(roleCode); + if (menus == null || menus.isEmpty()) { + return new ArrayList<>(); + } + + return menus.stream() + .map(SysMenu::getCode) + .distinct() + .toList(); + } + + /** + * 获取用户角色列表 + */ + @Override + public List getRoleList(Object loginId, String loginType) { + Long userId = Long.parseLong(loginId.toString()); + SysUser user = authService.getById(userId); + if (user == null) { + return new ArrayList<>(); + } + + List 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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/SwaggerConfig.java b/src/main/java/com/nanxiislet/admin/config/SwaggerConfig.java new file mode 100644 index 0000000..729f4d6 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/SwaggerConfig.java @@ -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)); + } +} diff --git a/src/main/java/com/nanxiislet/admin/config/WebMvcConfig.java b/src/main/java/com/nanxiislet/admin/config/WebMvcConfig.java new file mode 100644 index 0000000..4cee022 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/config/WebMvcConfig.java @@ -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/"); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/AuthController.java b/src/main/java/com/nanxiislet/admin/controller/AuthController.java new file mode 100644 index 0000000..f9cc854 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/AuthController.java @@ -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 getCaptcha() { + return R.ok(authService.generateCaptcha()); + } + + @PostMapping("/login") + @Operation(summary = "用户登录") + public R login(@Valid @RequestBody LoginRequest request) { + return R.ok(authService.login(request)); + } + + @PostMapping("/logout") + @Operation(summary = "用户登出") + public R logout() { + authService.logout(); + return R.ok(); + } + + @GetMapping("/user-info") + @Operation(summary = "获取当前用户信息") + public R getCurrentUser() { + return R.ok(authService.getCurrentUser()); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/DashboardController.java b/src/main/java/com/nanxiislet/admin/controller/DashboardController.java new file mode 100644 index 0000000..e17e3b4 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/DashboardController.java @@ -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 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> getOverview() { + Map 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> getQuickStats() { + List 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; + } + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/DatabaseController.java b/src/main/java/com/nanxiislet/admin/controller/DatabaseController.java new file mode 100644 index 0000000..e88e52f --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/DatabaseController.java @@ -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> checkAppInstalled(@RequestBody Map params) { + Long serverId = Long.parseLong(params.get("serverId").toString()); + String key = params.get("key").toString(); + String name = params.get("name").toString(); + + Map 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> searchDatabases(@RequestBody Map 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 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> createDatabase(@RequestBody Map params) { + Long serverId = Long.parseLong(params.get("serverId").toString()); + + // 移除serverId,只保留1Panel需要的参数 + params.remove("serverId"); + + Map 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 deleteDatabase(@RequestBody Map 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 updateDescription(@RequestBody Map 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 changePassword(@RequestBody Map 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 operateApp(@RequestBody Map 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> getFormatOptions(@RequestBody Map 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 options = onePanelService.getDatabaseFormatOptions(serverId, type, database, format); + return R.ok(options); + } + + /** + * 获取应用信息(如Redis) + */ + @PostMapping("/app/info") + @Operation(summary = "获取应用信息") + public R> getAppInfo(@RequestBody Map 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 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> getAppDetail(@RequestBody Map 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 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> installApp(@RequestBody Map params) { + if (params.get("serverId") == null) { + return R.fail("serverId不能为空"); + } + Long serverId = Long.parseLong(params.get("serverId").toString()); + + // 移除serverId,只保留1Panel需要的参数 + params.remove("serverId"); + + Map 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> readTaskLog(@RequestBody Map 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 result = onePanelService.readTaskLog(serverId, taskId, page, pageSize); + + if (result.containsKey("error")) { + return R.fail(result.get("error").toString()); + } + + return R.ok(result); + } +} + diff --git a/src/main/java/com/nanxiislet/admin/controller/FileController.java b/src/main/java/com/nanxiislet/admin/controller/FileController.java new file mode 100644 index 0000000..138e1c3 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FileController.java @@ -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> listFiles(@RequestBody Map 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 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 mkdir(@RequestBody Map 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 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 deleteFile(@RequestBody Map 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 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 createFile(@RequestBody Map 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 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 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 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> checkUploadFiles(@RequestBody Map params) { + if (params.get("serverId") == null) { + return R.fail("serverId不能为空"); + } + Long serverId = Long.parseLong(params.get("serverId").toString()); + java.util.List paths = (java.util.List) params.get("paths"); + + java.util.List 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 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> 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 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> mergeChunks(@RequestBody Map 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 result = onePanelService.mergeChunks(serverId, path, filename, + chunkCount, fileSize, uploadId); + + if (result.containsKey("error")) { + return R.fail(result.get("error").toString()); + } + + return R.ok(result); + } +} + diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceAccountController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceAccountController.java new file mode 100644 index 0000000..621cda9 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceAccountController.java @@ -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() { + return R.ok(accountService.list()); + } + + @GetMapping("/active") + @Operation(summary = "活跃账户列表") + public R> listActive() { + return R.ok(accountService.listActive()); + } + + @GetMapping("/{id}") + @Operation(summary = "账户详情") + public R 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 create(@Valid @RequestBody FinanceAccount account) { + accountService.save(account); + return R.ok(account); + } + + @PutMapping("/{id}") + @Operation(summary = "更新账户") + public R update(@PathVariable Long id, @Valid @RequestBody FinanceAccount account) { + account.setId(id); + accountService.updateById(account); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除账户") + public R delete(@PathVariable Long id) { + accountService.removeById(id); + return R.ok(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceBudgetController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceBudgetController.java new file mode 100644 index 0000000..89e9af0 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceBudgetController.java @@ -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> list(BasePageQuery query) { + Page 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 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 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 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 delete(@PathVariable Long id) { + budgetService.removeById(id); + return R.ok(); + } + + @GetMapping("/find") + @Operation(summary = "查找预算", description = "按条件查找预算") + public R 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); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceExpenseController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceExpenseController.java new file mode 100644 index 0000000..7c231d6 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceExpenseController.java @@ -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> list(BasePageQuery query) { + Page page = expenseService.listPage(query); + return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize())); + } + + @GetMapping("/{id}") + @Operation(summary = "支出详情") + public R 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 create(@Valid @RequestBody FinanceExpense expense) { + expense.setExpenseNo(expenseService.generateExpenseNo()); + expenseService.save(expense); + return R.ok(expense); + } + + @PutMapping("/{id}") + @Operation(summary = "更新支出") + public R update(@PathVariable Long id, @Valid @RequestBody FinanceExpense expense) { + expense.setId(id); + expenseService.updateById(expense); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除支出") + public R delete(@PathVariable Long id) { + expenseService.removeById(id); + return R.ok(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceIncomeController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceIncomeController.java new file mode 100644 index 0000000..5a88a94 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceIncomeController.java @@ -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> list(BasePageQuery query) { + Page 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 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 create(@Valid @RequestBody FinanceIncome income) { + income.setIncomeNo(incomeService.generateIncomeNo()); + incomeService.save(income); + return R.ok(income); + } + + @PutMapping("/{id}") + @Operation(summary = "更新收入", description = "更新收入记录") + public R update(@PathVariable Long id, @Valid @RequestBody FinanceIncome income) { + income.setId(id); + incomeService.updateById(income); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除收入", description = "删除收入记录") + public R delete(@PathVariable Long id) { + incomeService.removeById(id); + return R.ok(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceInvoiceController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceInvoiceController.java new file mode 100644 index 0000000..f590873 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceInvoiceController.java @@ -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> list(BasePageQuery query) { + Page 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 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 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 update(@PathVariable Long id, @Valid @RequestBody FinanceInvoice invoice) { + invoice.setId(id); + invoiceService.updateById(invoice); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除发票", description = "删除发票记录") + public R delete(@PathVariable Long id) { + invoiceService.removeById(id); + return R.ok(); + } + + @PostMapping("/{id}/issue") + @Operation(summary = "开具发票", description = "开具发票") + public R issue(@PathVariable Long id) { + invoiceService.issueInvoice(id); + return R.ok(); + } + + @PostMapping("/{id}/reject") + @Operation(summary = "驳回发票", description = "驳回发票申请") + public R reject(@PathVariable Long id, @RequestBody RejectRequest request) { + invoiceService.rejectInvoice(id, request.getReason()); + return R.ok(); + } + + @Data + public static class RejectRequest { + private String reason; + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceReimbursementController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceReimbursementController.java new file mode 100644 index 0000000..ee24c0e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceReimbursementController.java @@ -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> list(BasePageQuery query) { + Page page = reimbursementService.listPage(query); + return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize())); + } + + @GetMapping("/{id}") + @Operation(summary = "报销详情") + public R 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 create(@Valid @RequestBody FinanceReimbursement reimbursement) { + reimbursement.setReimbursementNo(reimbursementService.generateReimbursementNo()); + reimbursementService.save(reimbursement); + return R.ok(reimbursement); + } + + @PutMapping("/{id}") + @Operation(summary = "更新报销") + public R update(@PathVariable Long id, @Valid @RequestBody FinanceReimbursement reimbursement) { + reimbursement.setId(id); + reimbursementService.updateById(reimbursement); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除报销") + public R delete(@PathVariable Long id) { + reimbursementService.removeById(id); + return R.ok(); + } + + @PostMapping("/{id}/submit") + @Operation(summary = "提交审批") + public R 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(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/FinanceSettlementController.java b/src/main/java/com/nanxiislet/admin/controller/FinanceSettlementController.java new file mode 100644 index 0000000..b4adb31 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/FinanceSettlementController.java @@ -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> list(BasePageQuery query) { + Page 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 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 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 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 delete(@PathVariable Long id) { + settlementService.removeById(id); + return R.ok(); + } + + @PostMapping("/{id}/approve") + @Operation(summary = "审核通过", description = "审核通过结算单") + public R approve(@PathVariable Long id) { + settlementService.approve(id); + return R.ok(); + } + + @PostMapping("/{id}/reject") + @Operation(summary = "驳回", description = "驳回结算单") + public R reject(@PathVariable Long id, @RequestBody RejectRequest request) { + settlementService.reject(id, request.getReason()); + return R.ok(); + } + + @PostMapping("/{id}/confirm-payment") + @Operation(summary = "确认打款", description = "确认打款完成") + public R confirmPayment(@PathVariable Long id) { + settlementService.confirmPayment(id); + return R.ok(); + } + + @Data + public static class RejectRequest { + private String reason; + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/HealthController.java b/src/main/java/com/nanxiislet/admin/controller/HealthController.java new file mode 100644 index 0000000..523254d --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/HealthController.java @@ -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> health() { + Map 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> index() { + Map result = new HashMap<>(); + result.put("name", "Nanxiislet Admin API"); + result.put("version", "1.0.0"); + result.put("docs", "/doc.html"); + return R.ok(result); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/PlatformCertificateController.java b/src/main/java/com/nanxiislet/admin/controller/PlatformCertificateController.java new file mode 100644 index 0000000..029c423 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/PlatformCertificateController.java @@ -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> list( + @RequestParam(required = false) Long serverId, + BasePageQuery query) { + Page 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 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 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 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 apply(@Valid @RequestBody CertificateApplyRequest request) { + CertificateApplyResult result = certificateService.applyCertificate(request); + return R.ok(result); + } + + // ==================== 证书同步 ==================== + + @PostMapping("/sync/{serverId}") + @Operation(summary = "同步证书", description = "从1Panel同步证书列表到本地数据库") + public R> 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>> getAcmeAccounts(@PathVariable Long serverId) { + return R.ok(certificateService.getAcmeAccounts(serverId)); + } + + @GetMapping("/dns-accounts/{serverId}") + @Operation(summary = "获取DNS账户列表", description = "从1Panel获取DNS账户列表") + public R>> getDnsAccounts(@PathVariable Long serverId) { + return R.ok(certificateService.getDnsAccounts(serverId)); + } + + @GetMapping("/websites/{serverId}") + @Operation(summary = "获取网站列表", description = "从1Panel获取网站列表") + public R>> getWebsites(@PathVariable Long serverId) { + return R.ok(certificateService.getWebsites(serverId)); + } + + // ==================== 调试接口 ==================== + + @GetMapping("/debug/server/{serverId}") + @Operation(summary = "检查服务器1Panel配置", description = "用于调试:检查服务器是否正确配置了1Panel API") + public R> 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 + )); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/PlatformDomainController.java b/src/main/java/com/nanxiislet/admin/controller/PlatformDomainController.java new file mode 100644 index 0000000..9c38848 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/PlatformDomainController.java @@ -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> list(com.nanxiislet.admin.dto.DomainQueryDTO query) { + Page page = domainService.listPage(query); + return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize())); + } + + @GetMapping("/all") + @Operation(summary = "所有域名") + public R> listAll() { + return R.ok(domainService.list()); + } + + @GetMapping("/{id}") + @Operation(summary = "域名详情") + public R 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 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 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 delete(@PathVariable Long id) { + domainService.removeById(id); + return R.ok(); + } + + // ==================== 统计接口 ==================== + + @GetMapping("/stats") + @Operation(summary = "域名统计信息") + public R getStats() { + return R.ok(domainService.getDomainStats()); + } + + // ==================== 部署相关接口 ==================== + + @PostMapping("/deploy") + @Operation(summary = "部署域名到1Panel", description = "包含创建网站、申请证书、配置HTTPS") + public R deploy(@Valid @RequestBody DomainDeployRequest request) { + DomainDeployResult result = domainService.deployDomain(request); + return R.ok(result); + } + + @PostMapping("/undeploy/{id}") + @Operation(summary = "从1Panel删除部署") + public R undeploy(@PathVariable Long id) { + domainService.undeployDomain(id); + return R.ok(); + } + + @PostMapping("/{id}/check-dns") + @Operation(summary = "检查域名DNS解析状态") + public R> checkDns(@PathVariable Long id) { + Map result = domainService.checkDomainDns(id); + return R.ok(result); + } + + @PostMapping("/{id}/sync") + @Operation(summary = "从1Panel同步域名信息") + public R syncFromPanel(@PathVariable Long id) { + PlatformDomain domain = domainService.syncDomainFromPanel(id); + return R.ok(domain); + } + + @PostMapping("/sync-from-certificates/{serverId}") + @Operation(summary = "从证书同步域名", description = "将证书表中的域名同步到域名表") + public R> 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 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> 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 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 saveNginxConfig(@PathVariable Long id, @RequestBody Map 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 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("重载失败"); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/PlatformProjectController.java b/src/main/java/com/nanxiislet/admin/controller/PlatformProjectController.java new file mode 100644 index 0000000..4812d4b --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/PlatformProjectController.java @@ -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> list(BasePageQuery query) { + Page 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> listAll() { + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper wrapper = + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>(); + // 过滤掉门户类项目 + wrapper.ne(PlatformProject::getSystemType, "portal"); + // 确保按照排序字段排序 + wrapper.orderByAsc(PlatformProject::getSort); + + List list = projectService.list(wrapper); + // 填充菜单数量 + fillMenuCount(list); + return R.ok(list); + } + + @GetMapping("/integrated") + @Operation(summary = "获取集成到框架的业务项目", description = "返回 systemType=admin 且 integrateToFramework=true 的项目") + public R> listIntegratedProjects() { + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper 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 list = projectService.list(wrapper); + + // 填充菜单数量 + fillMenuCount(list); + + return R.ok(list); + } + + /** + * 填充菜单数量 + */ + private void fillMenuCount(List projectList) { + if (projectList == null || projectList.isEmpty()) { + return; + } + for (PlatformProject project : projectList) { + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper 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 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 create(@Valid @RequestBody PlatformProject project) { + projectService.save(project); + return R.ok(project); + } + + @PutMapping("/{id}") + @Operation(summary = "更新项目") + public R 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 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 deploy(@Valid @RequestBody DeployRequest request) { + DeployResult result = onePanelService.deployProject(request); + return R.ok(result); + } + + @GetMapping("/{serverId}/websites") + @Operation(summary = "获取服务器网站列表", description = "从1Panel获取指定服务器的网站列表") + public R getWebsites(@PathVariable Long serverId) { + String websites = onePanelService.getWebsites(serverId); + return R.ok(websites); + } + + @GetMapping("/{serverId}/certificates") + @Operation(summary = "获取服务器证书列表", description = "从1Panel获取指定服务器的SSL证书列表") + public R getCertificates(@PathVariable Long serverId) { + String certificates = onePanelService.getCertificates(serverId); + return R.ok(certificates); + } + + @GetMapping("/check-deploy/{projectId}") + @Operation(summary = "检查项目部署状态", description = "检查项目对应的网站是否已在1Panel中存在") + public R> checkDeployStatus(@PathVariable Long projectId) { + PlatformProject project = projectService.getById(projectId); + if (project == null) { + return R.fail("项目不存在"); + } + + Map 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 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> checkUploadFiles(@RequestBody Map params) { + Long projectId = Long.valueOf(params.get("projectId").toString()); + List paths = (List) 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 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 prepareUpload(@RequestBody Map 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 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> 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 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 saveNginxConfig(@PathVariable Long projectId, @RequestBody Map 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 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("重载失败"); + } + +} + diff --git a/src/main/java/com/nanxiislet/admin/controller/PlatformServerController.java b/src/main/java/com/nanxiislet/admin/controller/PlatformServerController.java new file mode 100644 index 0000000..c8b65c0 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/PlatformServerController.java @@ -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> list(BasePageQuery query) { + Page page = serverService.listPage(query); + return R.ok(PageResult.of(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize())); + } + + @GetMapping("/all") + @Operation(summary = "所有服务器(基础数据,支持筛选)") + public R> listAll( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) String status, + @RequestParam(required = false) String type + ) { + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper 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 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 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 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 create(@Valid @RequestBody PlatformServer server) { + serverService.save(server); + return R.ok(server); + } + + @PutMapping("/{id}") + @Operation(summary = "更新服务器") + public R update(@PathVariable Long id, @Valid @RequestBody PlatformServer server) { + server.setId(id); + serverService.updateById(server); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除服务器") + public R delete(@PathVariable Long id) { + // 检查是否有绑定的项目 + List 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(); + } + +} + + diff --git a/src/main/java/com/nanxiislet/admin/controller/RuntimeController.java b/src/main/java/com/nanxiislet/admin/controller/RuntimeController.java new file mode 100644 index 0000000..55ff402 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/RuntimeController.java @@ -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> searchRuntimes(@RequestBody Map 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 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 syncRuntimes(@RequestBody Map params) { + if (params.get("serverId") == null) { + return R.fail("serverId不能为空"); + } + Long serverId = Long.parseLong(params.get("serverId").toString()); + + Map result = onePanelService.syncRuntimes(serverId); + + if (result.containsKey("error")) { + return R.fail(result.get("error").toString()); + } + + return R.ok(); + } + + /** + * 操作运行时(启动/停止/重启) + */ + @PostMapping("/operate") + @Operation(summary = "操作运行时") + public R operateRuntime(@RequestBody Map 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 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 deleteRuntime(@RequestBody Map 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 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> searchRuntimeApps(@RequestBody Map 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 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> getRuntimeDetail(@RequestBody Map 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 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> getAppInfo(@RequestBody Map 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 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 createRuntime(@RequestBody Map params) { + if (params.get("serverId") == null) { + return R.fail("serverId不能为空"); + } + + Long serverId = Long.parseLong(params.get("serverId").toString()); + params.remove("serverId"); + + Map 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> getContainerLog(@RequestBody Map 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 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 updateRuntime(@RequestBody Map 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 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>> getNodeScripts(@RequestBody Map 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> result = onePanelService.getNodeScripts(serverId, codeDir); + return R.ok(result); + } +} + diff --git a/src/main/java/com/nanxiislet/admin/controller/SysApprovalController.java b/src/main/java/com/nanxiislet/admin/controller/SysApprovalController.java new file mode 100644 index 0000000..f1f98d1 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/SysApprovalController.java @@ -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 getStats() { + return R.ok(templateService.getStats()); + } + + // ==================== 模板管理 ==================== + + @GetMapping("/template/page") + @Operation(summary = "分页查询模板列表") + public R> 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> templateList( + @RequestParam(required = false) String scenario) { + return R.ok(templateService.listAll(scenario)); + } + + @GetMapping("/template/{id}") + @Operation(summary = "获取模板详情") + public R getTemplate(@PathVariable Long id) { + return R.ok(templateService.getDetail(id)); + } + + @PostMapping("/template") + @Operation(summary = "创建模板") + public R createTemplate(@RequestBody ApprovalTemplateRequest request) { + return R.ok(templateService.create(request)); + } + + @PutMapping("/template/{id}") + @Operation(summary = "更新模板") + public R updateTemplate(@PathVariable Long id, @RequestBody ApprovalTemplateRequest request) { + request.setId(id); + templateService.update(request); + return R.ok(); + } + + @DeleteMapping("/template/{id}") + @Operation(summary = "删除模板") + public R deleteTemplate(@PathVariable Long id) { + templateService.delete(id); + return R.ok(); + } + + @PostMapping("/template/{id}/toggle") + @Operation(summary = "切换模板启用状态") + public R toggleTemplate(@PathVariable Long id) { + templateService.toggle(id); + return R.ok(); + } + + // ==================== 实例管理 ==================== + + @GetMapping("/instance/page") + @Operation(summary = "分页查询实例列表") + public R> 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 getInstance(@PathVariable Long id) { + return R.ok(instanceService.getDetail(id)); + } + + @PostMapping("/instance/{id}/submit") + @Operation(summary = "提交审批") + public R submitInstance(@PathVariable Long id) { + instanceService.submit(id); + return R.ok(); + } + + @PostMapping("/instance/{id}/approve") + @Operation(summary = "审批操作") + public R 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 withdrawInstance(@PathVariable Long id) { + instanceService.withdraw(id); + return R.ok(); + } + + @PostMapping("/instance/{id}/cancel") + @Operation(summary = "取消审批") + public R 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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/SysDeptController.java b/src/main/java/com/nanxiislet/admin/controller/SysDeptController.java new file mode 100644 index 0000000..d7aef96 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/SysDeptController.java @@ -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> tree() { + return R.ok(deptService.listTree()); + } + + @GetMapping("/list") + @Operation(summary = "获取部门列表(扁平结构)") + public R> list() { + return R.ok(deptService.listAll()); + } + + @GetMapping("/{id}") + @Operation(summary = "获取部门详情") + public R getById(@PathVariable Long id) { + return R.ok(deptService.getById(id)); + } + + @PostMapping + @Operation(summary = "创建部门") + public R create(@RequestBody SysDept dept) { + return R.ok(deptService.create(dept)); + } + + @PutMapping("/{id}") + @Operation(summary = "更新部门") + public R update(@PathVariable Long id, @RequestBody SysDept dept) { + dept.setId(id); + deptService.updateDept(dept); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除部门") + public R delete(@PathVariable Long id) { + deptService.deleteDept(id); + return R.ok(); + } + + @GetMapping("/{id}/users") + @Operation(summary = "获取部门用户列表") + public R> getDeptUsers(@PathVariable Long id) { + return R.ok(deptService.getDeptUserIds(id)); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/SysDictController.java b/src/main/java/com/nanxiislet/admin/controller/SysDictController.java new file mode 100644 index 0000000..4a1f231 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/SysDictController.java @@ -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> 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() { + return R.ok(dictService.list()); + } + + @GetMapping("/{id}") + @Operation(summary = "获取字典详情") + public R getById(@PathVariable Long id) { + return R.ok(dictService.getById(id)); + } + + @GetMapping("/code/{code}") + @Operation(summary = "根据编码获取字典") + public R getByCode(@PathVariable String code) { + return R.ok(dictService.getByCode(code)); + } + + @PostMapping + @Operation(summary = "创建字典") + public R create(@RequestBody SysDict dict) { + return R.ok(dictService.create(dict)); + } + + @PutMapping("/{id}") + @Operation(summary = "更新字典") + public R update(@PathVariable Long id, @RequestBody SysDict dict) { + dict.setId(id); + dictService.updateDict(dict); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除字典") + public R delete(@PathVariable Long id) { + dictService.deleteDict(id); + return R.ok(); + } + + // ==================== 字典项 ==================== + + @GetMapping("/{dictId}/items") + @Operation(summary = "获取字典项列表") + public R> listItems(@PathVariable Long dictId) { + return R.ok(dictItemService.listByDictId(dictId)); + } + + @GetMapping("/code/{code}/items") + @Operation(summary = "根据字典编码获取字典项") + public R> listItemsByCode(@PathVariable String code) { + return R.ok(dictItemService.listByDictCode(code)); + } + + @GetMapping("/item/{id}") + @Operation(summary = "获取字典项详情") + public R getItemById(@PathVariable Long id) { + return R.ok(dictItemService.getById(id)); + } + + @PostMapping("/item") + @Operation(summary = "创建字典项") + public R createItem(@RequestBody SysDictItem item) { + return R.ok(dictItemService.create(item)); + } + + @PutMapping("/item/{id}") + @Operation(summary = "更新字典项") + public R updateItem(@PathVariable Long id, @RequestBody SysDictItem item) { + item.setId(id); + dictItemService.updateItem(item); + return R.ok(); + } + + @DeleteMapping("/item/{id}") + @Operation(summary = "删除字典项") + public R deleteItem(@PathVariable Long id) { + dictItemService.deleteItem(id); + return R.ok(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/SysMenuController.java b/src/main/java/com/nanxiislet/admin/controller/SysMenuController.java new file mode 100644 index 0000000..b9c4215 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/SysMenuController.java @@ -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> tree() { + return R.ok(menuService.listTree()); + } + + @GetMapping("/list") + @Operation(summary = "获取所有菜单,支持按项目ID筛选") + public R> list(@RequestParam(required = false) Long projectId) { + com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper 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 getById(@PathVariable Long id) { + return R.ok(menuService.getById(id)); + } + + @PostMapping + @Operation(summary = "创建菜单") + public R create(@RequestBody SysMenu menu) { + return R.ok(menuService.create(menu)); + } + + @PutMapping("/{id}") + @Operation(summary = "更新菜单") + public R update(@PathVariable Long id, @RequestBody SysMenu menu) { + menu.setId(id); + menuService.updateMenu(menu); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除菜单") + public R delete(@PathVariable Long id) { + menuService.deleteMenu(id); + return R.ok(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/SysRoleController.java b/src/main/java/com/nanxiislet/admin/controller/SysRoleController.java new file mode 100644 index 0000000..73c448e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/SysRoleController.java @@ -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> 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() { + return R.ok(roleService.listAll()); + } + + @GetMapping("/list-all") + @Operation(summary = "获取所有角色选项(简化版)") + public R>> listAll() { + List roles = roleService.listAll(); + List> options = roles.stream() + .map(role -> { + java.util.Map 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 getById(@PathVariable Long id) { + return R.ok(roleService.getById(id)); + } + + @PostMapping + @Operation(summary = "创建角色") + public R create(@RequestBody SysRole role) { + return R.ok(roleService.create(role)); + } + + @PutMapping("/{id}") + @Operation(summary = "更新角色") + public R update(@PathVariable Long id, @RequestBody SysRole role) { + role.setId(id); + roleService.updateRole(role); + return R.ok(); + } + + @DeleteMapping("/{id}") + @Operation(summary = "删除角色") + public R delete(@PathVariable Long id) { + roleService.deleteRole(id); + return R.ok(); + } + + @GetMapping("/{id}/menus") + @Operation(summary = "获取角色的菜单ID列表") + public R> getRoleMenus(@PathVariable Long id) { + return R.ok(roleService.getRoleMenuIds(id)); + } + + @PostMapping("/{id}/menus") + @Operation(summary = "分配菜单权限") + public R assignMenus(@PathVariable Long id, @RequestBody List menuIds) { + roleService.assignMenus(id, menuIds); + return R.ok(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/UploadController.java b/src/main/java/com/nanxiislet/admin/controller/UploadController.java new file mode 100644 index 0000000..3be839c --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/UploadController.java @@ -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> 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 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> 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); + } +} diff --git a/src/main/java/com/nanxiislet/admin/controller/UserController.java b/src/main/java/com/nanxiislet/admin/controller/UserController.java new file mode 100644 index 0000000..43e43e3 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/controller/UserController.java @@ -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> list(UserQuery query) { + Page 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 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 create(@Valid @RequestBody SysUser user) { + userService.createUser(user); + return R.ok(); + } + + @PutMapping("/{id}") + @Operation(summary = "更新用户", description = "更新用户信息") + @SaCheckRole("admin") + public R 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 delete(@PathVariable Long id) { + userService.removeById(id); + return R.ok(); + } + + @PostMapping("/{id}/reset-password") + @Operation(summary = "重置密码", description = "管理员重置用户密码") + @SaCheckRole("admin") + public R resetPassword(@PathVariable Long id, @RequestBody ResetPasswordRequest request) { + userService.resetPassword(id, request.getNewPassword()); + return R.ok(); + } + + @PostMapping("/change-password") + @Operation(summary = "修改密码", description = "用户修改自己的密码") + public R 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 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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/dto/CertificateApplyRequest.java b/src/main/java/com/nanxiislet/admin/dto/CertificateApplyRequest.java new file mode 100644 index 0000000..6b0f849 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/CertificateApplyRequest.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/CertificateApplyResult.java b/src/main/java/com/nanxiislet/admin/dto/CertificateApplyResult.java new file mode 100644 index 0000000..905db30 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/CertificateApplyResult.java @@ -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 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)); + } +} diff --git a/src/main/java/com/nanxiislet/admin/dto/DeployRequest.java b/src/main/java/com/nanxiislet/admin/dto/DeployRequest.java new file mode 100644 index 0000000..e2ec7c3 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/DeployRequest.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/DeployResult.java b/src/main/java/com/nanxiislet/admin/dto/DeployResult.java new file mode 100644 index 0000000..2c2e9b1 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/DeployResult.java @@ -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 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(); + } +} diff --git a/src/main/java/com/nanxiislet/admin/dto/DomainDeployRequest.java b/src/main/java/com/nanxiislet/admin/dto/DomainDeployRequest.java new file mode 100644 index 0000000..2a0232e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/DomainDeployRequest.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/DomainDeployResult.java b/src/main/java/com/nanxiislet/admin/dto/DomainDeployResult.java new file mode 100644 index 0000000..b15f05a --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/DomainDeployResult.java @@ -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 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)); + } +} diff --git a/src/main/java/com/nanxiislet/admin/dto/DomainQueryDTO.java b/src/main/java/com/nanxiislet/admin/dto/DomainQueryDTO.java new file mode 100644 index 0000000..871dfe3 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/DomainQueryDTO.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/DomainStatsDTO.java b/src/main/java/com/nanxiislet/admin/dto/DomainStatsDTO.java new file mode 100644 index 0000000..fa32233 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/DomainStatsDTO.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/ServerInfoDto.java b/src/main/java/com/nanxiislet/admin/dto/ServerInfoDto.java new file mode 100644 index 0000000..a134666 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/ServerInfoDto.java @@ -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 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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalInstanceVO.java b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalInstanceVO.java new file mode 100644 index 0000000..e2da3e2 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalInstanceVO.java @@ -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 records; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalStats.java b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalStats.java new file mode 100644 index 0000000..46cc0ea --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalStats.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalTemplateRequest.java b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalTemplateRequest.java new file mode 100644 index 0000000..d3829df --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalTemplateRequest.java @@ -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 nodes; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalTemplateVO.java b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalTemplateVO.java new file mode 100644 index 0000000..965fd81 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/approval/ApprovalTemplateVO.java @@ -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 nodes; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/auth/CaptchaResponse.java b/src/main/java/com/nanxiislet/admin/dto/auth/CaptchaResponse.java new file mode 100644 index 0000000..e1bc3ae --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/auth/CaptchaResponse.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/auth/LoginRequest.java b/src/main/java/com/nanxiislet/admin/dto/auth/LoginRequest.java new file mode 100644 index 0000000..4f1b4ec --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/auth/LoginRequest.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/auth/LoginResponse.java b/src/main/java/com/nanxiislet/admin/dto/auth/LoginResponse.java new file mode 100644 index 0000000..7aadfd7 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/auth/LoginResponse.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/auth/UserInfoVO.java b/src/main/java/com/nanxiislet/admin/dto/auth/UserInfoVO.java new file mode 100644 index 0000000..f952c09 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/auth/UserInfoVO.java @@ -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 permissions; + + @Schema(description = "菜单列表") + private List menus; + + @Schema(description = "创建时间") + private String createTime; + + @Schema(description = "最后登录时间") + private String lastLoginTime; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/query/ExpenseQuery.java b/src/main/java/com/nanxiislet/admin/dto/query/ExpenseQuery.java new file mode 100644 index 0000000..e534421 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/query/ExpenseQuery.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/query/IncomeQuery.java b/src/main/java/com/nanxiislet/admin/dto/query/IncomeQuery.java new file mode 100644 index 0000000..55a11a2 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/query/IncomeQuery.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/query/ProjectQuery.java b/src/main/java/com/nanxiislet/admin/dto/query/ProjectQuery.java new file mode 100644 index 0000000..7e9bce3 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/query/ProjectQuery.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/dto/system/UserQuery.java b/src/main/java/com/nanxiislet/admin/dto/system/UserQuery.java new file mode 100644 index 0000000..a256469 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/dto/system/UserQuery.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceAccount.java b/src/main/java/com/nanxiislet/admin/entity/FinanceAccount.java new file mode 100644 index 0000000..22cafa8 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceAccount.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceBudget.java b/src/main/java/com/nanxiislet/admin/entity/FinanceBudget.java new file mode 100644 index 0000000..68fad0e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceBudget.java @@ -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 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; + } +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceExpense.java b/src/main/java/com/nanxiislet/admin/entity/FinanceExpense.java new file mode 100644 index 0000000..80bb36a --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceExpense.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceIncome.java b/src/main/java/com/nanxiislet/admin/entity/FinanceIncome.java new file mode 100644 index 0000000..c872fe4 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceIncome.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceInvoice.java b/src/main/java/com/nanxiislet/admin/entity/FinanceInvoice.java new file mode 100644 index 0000000..9d2b7c5 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceInvoice.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceReimbursement.java b/src/main/java/com/nanxiislet/admin/entity/FinanceReimbursement.java new file mode 100644 index 0000000..d5e91e5 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceReimbursement.java @@ -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 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 attachments; + } +} diff --git a/src/main/java/com/nanxiislet/admin/entity/FinanceSettlement.java b/src/main/java/com/nanxiislet/admin/entity/FinanceSettlement.java new file mode 100644 index 0000000..2efb643 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/FinanceSettlement.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/PlatformCertificate.java b/src/main/java/com/nanxiislet/admin/entity/PlatformCertificate.java new file mode 100644 index 0000000..cf36238 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/PlatformCertificate.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/PlatformDomain.java b/src/main/java/com/nanxiislet/admin/entity/PlatformDomain.java new file mode 100644 index 0000000..9e51f71 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/PlatformDomain.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/PlatformProject.java b/src/main/java/com/nanxiislet/admin/entity/PlatformProject.java new file mode 100644 index 0000000..513ab5f --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/PlatformProject.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/PlatformServer.java b/src/main/java/com/nanxiislet/admin/entity/PlatformServer.java new file mode 100644 index 0000000..6f76b75 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/PlatformServer.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysApprovalInstance.java b/src/main/java/com/nanxiislet/admin/entity/SysApprovalInstance.java new file mode 100644 index 0000000..1990a94 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysApprovalInstance.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysApprovalNode.java b/src/main/java/com/nanxiislet/admin/entity/SysApprovalNode.java new file mode 100644 index 0000000..6c8f40e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysApprovalNode.java @@ -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 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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysApprovalRecord.java b/src/main/java/com/nanxiislet/admin/entity/SysApprovalRecord.java new file mode 100644 index 0000000..425a95d --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysApprovalRecord.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysApprovalTemplate.java b/src/main/java/com/nanxiislet/admin/entity/SysApprovalTemplate.java new file mode 100644 index 0000000..4e8ea17 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysApprovalTemplate.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysDept.java b/src/main/java/com/nanxiislet/admin/entity/SysDept.java new file mode 100644 index 0000000..bc2891c --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysDept.java @@ -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; + + /** + * 父部门ID,0表示顶级部门 + */ + 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 children; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysDict.java b/src/main/java/com/nanxiislet/admin/entity/SysDict.java new file mode 100644 index 0000000..ae0ef38 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysDict.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysDictItem.java b/src/main/java/com/nanxiislet/admin/entity/SysDictItem.java new file mode 100644 index 0000000..caf3c32 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysDictItem.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysMenu.java b/src/main/java/com/nanxiislet/admin/entity/SysMenu.java new file mode 100644 index 0000000..4a2007e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysMenu.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysRole.java b/src/main/java/com/nanxiislet/admin/entity/SysRole.java new file mode 100644 index 0000000..efdef0b --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysRole.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysRoleMenu.java b/src/main/java/com/nanxiislet/admin/entity/SysRoleMenu.java new file mode 100644 index 0000000..f9114b6 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysRoleMenu.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/entity/SysUser.java b/src/main/java/com/nanxiislet/admin/entity/SysUser.java new file mode 100644 index 0000000..1f39dcd --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/entity/SysUser.java @@ -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; +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceAccountMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceAccountMapper.java new file mode 100644 index 0000000..c86c755 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceAccountMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceBudgetMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceBudgetMapper.java new file mode 100644 index 0000000..5ebfb7d --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceBudgetMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceExpenseMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceExpenseMapper.java new file mode 100644 index 0000000..9ccd865 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceExpenseMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceIncomeMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceIncomeMapper.java new file mode 100644 index 0000000..e474f39 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceIncomeMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceInvoiceMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceInvoiceMapper.java new file mode 100644 index 0000000..afbc9f4 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceInvoiceMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceReimbursementMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceReimbursementMapper.java new file mode 100644 index 0000000..05067f6 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceReimbursementMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/FinanceSettlementMapper.java b/src/main/java/com/nanxiislet/admin/mapper/FinanceSettlementMapper.java new file mode 100644 index 0000000..880ca66 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/FinanceSettlementMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/PlatformCertificateMapper.java b/src/main/java/com/nanxiislet/admin/mapper/PlatformCertificateMapper.java new file mode 100644 index 0000000..aacc0f5 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/PlatformCertificateMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/PlatformDomainMapper.java b/src/main/java/com/nanxiislet/admin/mapper/PlatformDomainMapper.java new file mode 100644 index 0000000..6b93a9a --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/PlatformDomainMapper.java @@ -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 { + + @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); +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/PlatformProjectMapper.java b/src/main/java/com/nanxiislet/admin/mapper/PlatformProjectMapper.java new file mode 100644 index 0000000..8834038 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/PlatformProjectMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/PlatformServerMapper.java b/src/main/java/com/nanxiislet/admin/mapper/PlatformServerMapper.java new file mode 100644 index 0000000..e291ece --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/PlatformServerMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysApprovalInstanceMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalInstanceMapper.java new file mode 100644 index 0000000..1baed41 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalInstanceMapper.java @@ -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 { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysApprovalNodeMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalNodeMapper.java new file mode 100644 index 0000000..ef6f0a2 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalNodeMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysApprovalNode; +import org.apache.ibatis.annotations.Mapper; + +/** + * 审批流程节点Mapper + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Mapper +public interface SysApprovalNodeMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysApprovalRecordMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalRecordMapper.java new file mode 100644 index 0000000..937c646 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalRecordMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysApprovalRecord; +import org.apache.ibatis.annotations.Mapper; + +/** + * 审批记录Mapper + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Mapper +public interface SysApprovalRecordMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysApprovalTemplateMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalTemplateMapper.java new file mode 100644 index 0000000..9cba0a1 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysApprovalTemplateMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysApprovalTemplate; +import org.apache.ibatis.annotations.Mapper; + +/** + * 审批流程模板Mapper + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Mapper +public interface SysApprovalTemplateMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysDeptMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysDeptMapper.java new file mode 100644 index 0000000..58b7d28 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysDeptMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysDept; +import org.apache.ibatis.annotations.Mapper; + +/** + * 部门Mapper + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Mapper +public interface SysDeptMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysDictItemMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysDictItemMapper.java new file mode 100644 index 0000000..e15f406 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysDictItemMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysDictItem; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典项 Mapper + * + * @author NanxiIslet + * @since 2026-01-08 + */ +@Mapper +public interface SysDictItemMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysDictMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysDictMapper.java new file mode 100644 index 0000000..fbea3e4 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysDictMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysDict; +import org.apache.ibatis.annotations.Mapper; + +/** + * 字典类型 Mapper + * + * @author NanxiIslet + * @since 2026-01-08 + */ +@Mapper +public interface SysDictMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysMenuMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysMenuMapper.java new file mode 100644 index 0000000..a87040a --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysMenuMapper.java @@ -0,0 +1,38 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysMenu; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 菜单Mapper + * + * @author NanxiIslet + * @since 2024-01-08 + */ +@Mapper +public interface SysMenuMapper extends BaseMapper { + + /** + * 根据角色ID查询菜单 + */ + @Select("SELECT m.* FROM sys_menu m " + + "INNER JOIN sys_role_menu rm ON m.id = rm.menu_id " + + "WHERE rm.role_id = #{roleId} AND m.deleted = 0 " + + "ORDER BY m.sort") + List selectMenusByRoleId(@Param("roleId") Long roleId); + + /** + * 根据角色编码查询菜单 + */ + @Select("SELECT m.* FROM sys_menu m " + + "INNER JOIN sys_role_menu rm ON m.id = rm.menu_id " + + "INNER JOIN sys_role r ON rm.role_id = r.id " + + "WHERE r.code = #{roleCode} AND m.deleted = 0 AND r.deleted = 0 " + + "ORDER BY m.sort") + List selectMenusByRoleCode(@Param("roleCode") String roleCode); +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysRoleMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysRoleMapper.java new file mode 100644 index 0000000..6723ada --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysRoleMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysRole; +import org.apache.ibatis.annotations.Mapper; + +/** + * 角色Mapper + * + * @author NanxiIslet + * @since 2024-01-08 + */ +@Mapper +public interface SysRoleMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysRoleMenuMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..3604182 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysRoleMenuMapper.java @@ -0,0 +1,26 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysRoleMenu; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 角色菜单关联Mapper + * + * @author NanxiIslet + * @since 2024-01-08 + */ +@Mapper +public interface SysRoleMenuMapper extends BaseMapper { + + /** + * 根据角色ID删除关联 + */ + int deleteByRoleId(@Param("roleId") Long roleId); + + /** + * 根据菜单ID删除关联 + */ + int deleteByMenuId(@Param("menuId") Long menuId); +} diff --git a/src/main/java/com/nanxiislet/admin/mapper/SysUserMapper.java b/src/main/java/com/nanxiislet/admin/mapper/SysUserMapper.java new file mode 100644 index 0000000..bbd95e6 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/mapper/SysUserMapper.java @@ -0,0 +1,15 @@ +package com.nanxiislet.admin.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.nanxiislet.admin.entity.SysUser; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户 Mapper + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Mapper +public interface SysUserMapper extends BaseMapper { +} diff --git a/src/main/java/com/nanxiislet/admin/service/AuthService.java b/src/main/java/com/nanxiislet/admin/service/AuthService.java new file mode 100644 index 0000000..a225160 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/AuthService.java @@ -0,0 +1,49 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.dto.auth.*; +import com.nanxiislet.admin.entity.SysUser; + +/** + * 认证服务接口 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +public interface AuthService extends IService { + + /** + * 生成验证码 + * + * @return 验证码响应 + */ + CaptchaResponse generateCaptcha(); + + /** + * 用户登录 + * + * @param request 登录请求 + * @return 登录响应 + */ + LoginResponse login(LoginRequest request); + + /** + * 用户登出 + */ + void logout(); + + /** + * 获取当前登录用户信息 + * + * @return 用户信息 + */ + UserInfoVO getCurrentUser(); + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @return 用户信息 + */ + SysUser getByUsername(String username); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceAccountService.java b/src/main/java/com/nanxiislet/admin/service/FinanceAccountService.java new file mode 100644 index 0000000..a2264f7 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceAccountService.java @@ -0,0 +1,14 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.entity.FinanceAccount; + +import java.util.List; + +/** + * 财务账户服务接口 + */ +public interface FinanceAccountService extends IService { + List listActive(); + FinanceAccount getDefault(); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceBudgetService.java b/src/main/java/com/nanxiislet/admin/service/FinanceBudgetService.java new file mode 100644 index 0000000..5c57414 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceBudgetService.java @@ -0,0 +1,53 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceBudget; + +import java.math.BigDecimal; + +/** + * 预算管理服务接口 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +public interface FinanceBudgetService extends IService { + + /** + * 分页查询预算记录 + * + * @param query 查询条件 + * @return 分页结果 + */ + Page listPage(BasePageQuery query); + + /** + * 按条件查询预算 + * + * @param year 年份 + * @param period 周期 + * @param quarter 季度 + * @param month 月份 + * @param departmentId 部门ID + * @param projectId 项目ID + * @return 预算记录 + */ + FinanceBudget findBudget(Integer year, String period, Integer quarter, Integer month, Long departmentId, Long projectId); + + /** + * 更新预算使用金额 + * + * @param budgetId 预算ID + * @param usedAmount 新增使用金额 + */ + void updateUsedAmount(Long budgetId, BigDecimal usedAmount); + + /** + * 计算并更新使用率 + * + * @param budgetId 预算ID + */ + void calculateUsageRate(Long budgetId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceExpenseService.java b/src/main/java/com/nanxiislet/admin/service/FinanceExpenseService.java new file mode 100644 index 0000000..8bf016d --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceExpenseService.java @@ -0,0 +1,14 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceExpense; + +/** + * 支出管理服务接口 + */ +public interface FinanceExpenseService extends IService { + Page listPage(BasePageQuery query); + String generateExpenseNo(); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceIncomeService.java b/src/main/java/com/nanxiislet/admin/service/FinanceIncomeService.java new file mode 100644 index 0000000..6e3371a --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceIncomeService.java @@ -0,0 +1,30 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceIncome; + +/** + * 收入管理服务接口 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +public interface FinanceIncomeService extends IService { + + /** + * 分页查询收入记录 + * + * @param query 查询条件 + * @return 分页结果 + */ + Page listPage(BasePageQuery query); + + /** + * 生成收入编号 + * + * @return 收入编号 + */ + String generateIncomeNo(); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceInvoiceService.java b/src/main/java/com/nanxiislet/admin/service/FinanceInvoiceService.java new file mode 100644 index 0000000..c4b6b06 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceInvoiceService.java @@ -0,0 +1,38 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceInvoice; + +/** + * 发票管理服务接口 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +public interface FinanceInvoiceService extends IService { + + /** + * 分页查询发票记录 + * + * @param query 查询条件 + * @return 分页结果 + */ + Page listPage(BasePageQuery query); + + /** + * 开具发票 + * + * @param id 发票ID + */ + void issueInvoice(Long id); + + /** + * 驳回发票申请 + * + * @param id 发票ID + * @param reason 驳回原因 + */ + void rejectInvoice(Long id, String reason); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceReimbursementService.java b/src/main/java/com/nanxiislet/admin/service/FinanceReimbursementService.java new file mode 100644 index 0000000..ed89c02 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceReimbursementService.java @@ -0,0 +1,14 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceReimbursement; + +/** + * 报销管理服务接口 + */ +public interface FinanceReimbursementService extends IService { + Page listPage(BasePageQuery query); + String generateReimbursementNo(); +} diff --git a/src/main/java/com/nanxiislet/admin/service/FinanceSettlementService.java b/src/main/java/com/nanxiislet/admin/service/FinanceSettlementService.java new file mode 100644 index 0000000..74f7a16 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/FinanceSettlementService.java @@ -0,0 +1,52 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceSettlement; + +/** + * 结算管理服务接口 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +public interface FinanceSettlementService extends IService { + + /** + * 分页查询结算记录 + * + * @param query 查询条件 + * @return 分页结果 + */ + Page listPage(BasePageQuery query); + + /** + * 审核通过 + * + * @param id 结算单ID + */ + void approve(Long id); + + /** + * 驳回 + * + * @param id 结算单ID + * @param reason 驳回原因 + */ + void reject(Long id, String reason); + + /** + * 确认打款 + * + * @param id 结算单ID + */ + void confirmPayment(Long id); + + /** + * 计算税金和实发金额 + * + * @param settlement 结算单 + */ + void calculateAmounts(FinanceSettlement settlement); +} diff --git a/src/main/java/com/nanxiislet/admin/service/OnePanelService.java b/src/main/java/com/nanxiislet/admin/service/OnePanelService.java new file mode 100644 index 0000000..4ee81e8 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/OnePanelService.java @@ -0,0 +1,437 @@ +package com.nanxiislet.admin.service; + +import com.nanxiislet.admin.dto.DeployRequest; +import com.nanxiislet.admin.dto.DeployResult; + +/** + * 1Panel服务接口 + * 用于与1Panel服务器进行交互 + * + * @author NanxiIslet + * @since 2024-01-10 + */ +public interface OnePanelService { + + /** + * 执行项目部署 + * + * @param request 部署请求 + * @return 部署结果 + */ + DeployResult deployProject(DeployRequest request); + + /** + * 获取网站列表 + * + * @param serverId 服务器ID + * @return 网站列表JSON字符串 + */ + String getWebsites(Long serverId); + + /** + * 获取证书列表 + * + * @param serverId 服务器ID + * @return 证书列表JSON字符串 + */ + String getCertificates(Long serverId); + + /** + * 检查网站是否已在1Panel中存在 + * + * @param serverId 服务器ID + * @param domain 域名 + * @return 网站ID,如果不存在返回null + */ + Long checkWebsiteExists(Long serverId, String domain); + + /** + * 检查网站状态并返回详细信息 + * + * @param serverId 服务器ID + * @param domain 域名 + * @return 网站详细信息Map,包含 id, status, sslStatus, protocol 等 + */ + java.util.Map checkWebsiteStatus(Long serverId, String domain); + + /** + * 操作网站(启动/停止) + * + * @param serverId 服务器ID + * @param websiteId 网站ID + * @param operate 操作类型:start, stop + * @return 是否成功 + */ + boolean operateWebsite(Long serverId, Long websiteId, String operate); + + /** + * 删除网站 + * + * @param serverId 服务器ID + * @param websiteId 网站ID + * @return 是否成功 + */ + boolean deleteWebsite(Long serverId, Long websiteId); + + /** + * 批量检查文件是否存在 + * + * @param serverId 服务器ID + * @param paths 文件路径列表 + * @return 存在的重复文件列表 + */ + java.util.List checkFileBatch(Long serverId, java.util.List paths); + + /** + * 查询文件/目录详情 + * + * @param serverId 服务器ID + * @param path 路径 + * @return 文件详情 JSON 字符串 + */ + String searchFile(Long serverId, String path); + + /** + * 删除文件或目录 + * + * @param serverId 服务器ID + * @param path 路径 + * @param isDir 是否目录 + * @return 是否成功 + */ + boolean deleteFile(Long serverId, String path, boolean isDir); + + /** + * 分片上传文件 + * + * @param serverId 服务器ID + * @param filename 文件名 + * @param path 目标目录路径 + * @param chunkIndex 分片索引 + * @param chunkCount 分片总数 + * @param fileContent 分片内容(字节数组) + * @return 是否上传成功(成功返回null或空消息,失败返回错误信息) + */ + String uploadFileChunk(Long serverId, String filename, String path, int chunkIndex, int chunkCount, org.springframework.web.multipart.MultipartFile fileContent); + + /** + * 检查1Panel应用安装状态 + * + * @param serverId 服务器ID + * @param key 应用key,如mysql, postgresql, redis + * @param name 应用名称 + * @return 应用安装状态信息Map + */ + java.util.Map checkAppInstalled(Long serverId, String key, String name); + + /** + * 查询数据库列表 + * + * @param serverId 服务器ID + * @param database 数据库类型,如mysql + * @param page 页码 + * @param pageSize 每页数量 + * @return 数据库列表信息 + */ + java.util.Map searchDatabases(Long serverId, String database, int page, int pageSize); + + /** + * 创建数据库 + * + * @param serverId 服务器ID + * @param params 创建参数 + * @return 创建结果 + */ + java.util.Map createDatabase(Long serverId, java.util.Map params); + + /** + * 删除数据库 + * + * @param serverId 服务器ID + * @param params 删除参数 + * @return 是否成功 + */ + boolean deleteDatabase(Long serverId, java.util.Map params); + + /** + * 修改数据库描述 + * + * @param serverId 服务器ID + * @param params 修改参数 + * @return 是否成功 + */ + boolean updateDatabaseDescription(Long serverId, java.util.Map params); + + /** + * 修改数据库密码 + * + * @param serverId 服务器ID + * @param params 修改参数 + * @return 是否成功 + */ + boolean changeDatabasePassword(Long serverId, java.util.Map params); + + /** + * 操作应用(启动/停止/重启) + * + * @param serverId 服务器ID + * @param params 操作参数 + * @return 是否成功 + */ + boolean operateApp(Long serverId, java.util.Map params); + + /** + * 获取数据库字符集排序规则选项 + * + * @param serverId 服务器ID + * @param type 数据库类型 + * @param database 数据库名称 + * @param format 字符集 (可选) + * @return 选项列表 + */ + java.util.List getDatabaseFormatOptions(Long serverId, String type, String database, String format); + + /** + * 获取应用信息(如Redis) + * + * @param serverId 服务器ID + * @param appKey 应用key,如redis, mysql等 + * @return 应用信息Map + */ + java.util.Map getAppInfo(Long serverId, String appKey); + + /** + * 获取应用版本详情 + * + * @param serverId 服务器ID + * @param appId 应用ID + * @param version 版本号 + * @return 应用版本详情Map + */ + java.util.Map getAppDetail(Long serverId, Long appId, String version); + + /** + * 安装应用 + * + * @param serverId 服务器ID + * @param params 安装参数 + * @return 安装结果 + */ + java.util.Map installApp(Long serverId, java.util.Map params); + + /** + * 读取任务日志 + * + * @param serverId 服务器ID + * @param taskId 任务ID + * @param page 页码 + * @param pageSize 每页数量 + * @return 日志内容 + */ + java.util.Map readTaskLog(Long serverId, String taskId, int page, int pageSize); + + /** + * 搜索运行时列表 + * + * @param serverId 服务器ID + * @param type 运行时类型(php/java/node/go/python/dotnet) + * @param page 页码 + * @param pageSize 每页数量 + * @return 运行时列表 + */ + java.util.Map searchRuntimes(Long serverId, String type, int page, int pageSize); + + /** + * 同步运行时状态 + * + * @param serverId 服务器ID + * @return 结果 + */ + java.util.Map syncRuntimes(Long serverId); + + /** + * 操作运行时(启动/停止/重启) + * + * @param serverId 服务器ID + * @param id 运行时ID + * @param operate 操作类型(start/stop/restart) + * @return 结果 + */ + java.util.Map operateRuntime(Long serverId, Long id, String operate); + + /** + * 删除运行时 + * + * @param serverId 服务器ID + * @param id 运行时ID + * @param forceDelete 是否强制删除 + * @param deleteFolder 是否删除关联文件夹 + * @param codeDir 文件夹路径 + * @return 结果 + */ + java.util.Map deleteRuntime(Long serverId, Long id, boolean forceDelete, boolean deleteFolder, String codeDir); + + /** + * 搜索运行时应用列表 + * + * @param serverId 服务器ID + * @param type 运行时类型(php/java/node/go/python/dotnet) + * @param page 页码 + * @param pageSize 每页数量 + * @return 应用列表 + */ + java.util.Map searchRuntimeApps(Long serverId, String type, int page, int pageSize); + + /** + * 获取运行时版本详情 + * + * @param serverId 服务器ID + * @param appId 应用ID + * @param version 版本号 + * @return 版本详情 + */ + java.util.Map getRuntimeDetail(Long serverId, Long appId, String version); + + /** + * 创建运行时 + * + * @param serverId 服务器ID + * @param params 创建参数 + * @return 结果 + */ + java.util.Map createRuntime(Long serverId, java.util.Map params); + + /** + * 获取文件/目录列表 + * + * @param serverId 服务器ID + * @param path 路径 + * @return 文件列表 + */ + java.util.Map listFiles(Long serverId, String path); + + /** + * + * @param serverId 服务器ID + * @param path 路径 + * @return 结果 + */ + java.util.Map mkdir(Long serverId, String path); + + /** + * + * @param serverId 服务器ID + * @param path 路径 + * @param isDir 是否为目录 + * @return 结果 + */ + java.util.Map deleteFile(Long serverId, String path, Boolean isDir); + + /** + * 创建文件 + * + * @param serverId 服务器ID + * @param path 路径 + * @return 结果 + */ + java.util.Map createFile(Long serverId, String path); + + /** + * 上传文件 + * + * @param serverId 服务器ID + * @param path 上传目录 + * @param file 文件流 + * @return 结果 + */ + java.util.Map uploadFile(Long serverId, String path, org.springframework.web.multipart.MultipartFile file); + + /** + * 获取容器日志 + * + * @param serverId 服务器ID + * @param containerName 容器名称 + * @param composePath DockerCompose文件路径 (可选) + * @return 结果 + */ + java.util.Map getContainerLog(Long serverId, String containerName, String composePath); + + /** + * 更新运行时 + * + * @param serverId 服务器ID + * @param params 更新参数 + * @return 结果 + */ + java.util.Map updateRuntime(Long serverId, java.util.Map params); + + + /** + * 获取Node脚本列表 + * + * @param serverId 服务器ID + * @param codeDir 代码目录 + * @return 脚本列表 + */ + java.util.List> getNodeScripts(Long serverId, String codeDir); + + /** + * 分片上传文件到 1Panel(真正的分片上传) + * + * @param serverId 服务器ID + * @param path 目标目录路径 + * @param filename 文件名 + * @param chunkIndex 分片索引(从0开始) + * @param chunkCount 分片总数 + * @param fileSize 文件总大小(字节) + * @param uploadId 上传ID(用于标识同一个上传任务) + * @param chunk 分片内容 + * @return 结果 + */ + java.util.Map uploadChunk(Long serverId, String path, String filename, + int chunkIndex, int chunkCount, long fileSize, String uploadId, + org.springframework.web.multipart.MultipartFile chunk); + + /** + * 合并已上传的分片 + * + * @param serverId 服务器ID + * @param path 目标目录路径 + * @param filename 文件名 + * @param chunkCount 分片总数 + * @param fileSize 文件总大小(字节) + * @param uploadId 上传ID + * @return 结果 + */ + java.util.Map mergeChunks(Long serverId, String path, String filename, + int chunkCount, long fileSize, String uploadId); + + /** + * 获取网站Nginx配置 + * + * @param serverId 服务器ID + * @param websiteId 网站ID + * @return Nginx配置信息,包含 path, content, name 等 + */ + java.util.Map getWebsiteNginxConfig(Long serverId, Long websiteId); + + /** + * 保存网站Nginx配置 + * + * @param serverId 服务器ID + * @param websiteId 网站ID + * @param content 配置内容 + * @return 是否成功 + */ + boolean saveWebsiteNginxConfig(Long serverId, Long websiteId, String content); + + /** + * 重载Nginx + * + * @param serverId 服务器ID + * @return 是否成功 + */ + boolean reloadNginx(Long serverId); + +} + diff --git a/src/main/java/com/nanxiislet/admin/service/PlatformCertificateService.java b/src/main/java/com/nanxiislet/admin/service/PlatformCertificateService.java new file mode 100644 index 0000000..493f349 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/PlatformCertificateService.java @@ -0,0 +1,93 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.dto.CertificateApplyRequest; +import com.nanxiislet.admin.dto.CertificateApplyResult; +import com.nanxiislet.admin.entity.PlatformCertificate; + +import java.util.List; +import java.util.Map; + +/** + * 证书管理服务接口 + * + * @author NanxiIslet + * @since 2026-01-13 + */ +public interface PlatformCertificateService extends IService { + + /** + * 分页查询证书列表 + */ + Page listPage(Long serverId, BasePageQuery query); + + /** + * 申请证书 + * 调用1Panel API申请证书,并保存到本地数据库 + * + * @param request 申请请求 + * @return 申请结果 + */ + CertificateApplyResult applyCertificate(CertificateApplyRequest request); + + /** + * 从1Panel同步证书列表 + * + * @param serverId 服务器ID + * @return 同步的证书数量 + */ + int syncCertificatesFromPanel(Long serverId); + + /** + * 获取证书详情(包含证书内容和私钥) + * + * @param id 证书ID + * @return 证书详情 + */ + PlatformCertificate getCertificateDetail(Long id); + + /** + * 删除证书 + * 同时删除1Panel上的证书 + * + * @param id 证书ID + * @return 是否成功 + */ + boolean deleteCertificate(Long id); + + /** + * 更新证书设置 + * + * @param id 证书ID + * @param autoRenew 是否自动续签 + * @param description 备注 + * @return 是否成功 + */ + boolean updateCertificateSettings(Long id, Boolean autoRenew, String description); + + /** + * 获取服务器的Acme账户列表 + * + * @param serverId 服务器ID + * @return Acme账户列表 + */ + List> getAcmeAccounts(Long serverId); + + /** + * 获取服务器的DNS账户列表 + * + * @param serverId 服务器ID + * @return DNS账户列表 + */ + List> getDnsAccounts(Long serverId); + + /** + * 获取服务器的网站列表 + * + * @param serverId 服务器ID + * @return 网站列表 + */ + List> getWebsites(Long serverId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/PlatformDomainService.java b/src/main/java/com/nanxiislet/admin/service/PlatformDomainService.java new file mode 100644 index 0000000..f9749b1 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/PlatformDomainService.java @@ -0,0 +1,91 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.dto.DomainDeployRequest; +import com.nanxiislet.admin.dto.DomainDeployResult; +import com.nanxiislet.admin.dto.DomainQueryDTO; +import com.nanxiislet.admin.dto.DomainStatsDTO; +import com.nanxiislet.admin.entity.PlatformDomain; + +import java.util.Map; + +/** + * 域名管理服务接口 + * + * @author NanxiIslet + * @since 2026-01-13 + */ +public interface PlatformDomainService extends IService { + + /** + * 分页查询域名列表 + */ + Page listPage(DomainQueryDTO query); + + /** + * 部署域名到1Panel + * 包含:创建网站、申请证书、配置HTTPS + * + * @param request 部署请求 + * @return 部署结果 + */ + DomainDeployResult deployDomain(DomainDeployRequest request); + + /** + * 检查域名DNS解析状态 + * + * @param domainId 域名ID + * @return 检查结果 + */ + Map checkDomainDns(Long domainId); + + /** + * 从1Panel同步域名信息 + * + * @param domainId 域名ID + * @return 更新后的域名信息 + */ + PlatformDomain syncDomainFromPanel(Long domainId); + + /** + * 获取域名统计信息 + * + * @return 统计信息 + */ + DomainStatsDTO getDomainStats(); + + /** + * 根据域名查找记录 + * + * @param domain 域名 + * @return 域名记录 + */ + PlatformDomain getByDomain(String domain); + + /** + * 从证书中同步域名 + * 将证书表中的域名同步到域名表 + * + * @param serverId 服务器ID + * @return 同步的域名数量 + */ + int syncDomainsFromCertificates(Long serverId); + + /** + * 从1Panel中删除部署(删除网站) + * 保留数据库记录,但清除部署关联信息 + * + * @param domainId 域名ID + */ + void undeployDomain(Long domainId); + + /** + * 部署运行环境到1Panel + * 创建运行时类型的网站 + * + * @param domainId 域名ID + * @return 部署结果 + */ + DomainDeployResult deployRuntime(Long domainId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/PlatformProjectService.java b/src/main/java/com/nanxiislet/admin/service/PlatformProjectService.java new file mode 100644 index 0000000..dfffedf --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/PlatformProjectService.java @@ -0,0 +1,13 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.PlatformProject; + +/** + * 项目管理服务接口 + */ +public interface PlatformProjectService extends IService { + Page listPage(BasePageQuery query); +} diff --git a/src/main/java/com/nanxiislet/admin/service/PlatformServerService.java b/src/main/java/com/nanxiislet/admin/service/PlatformServerService.java new file mode 100644 index 0000000..5acc25c --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/PlatformServerService.java @@ -0,0 +1,39 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.dto.ServerInfoDto; +import com.nanxiislet.admin.entity.PlatformServer; + +/** + * 服务器管理服务接口 + */ +public interface PlatformServerService extends IService { + + /** + * 分页查询服务器列表 + */ + Page listPage(BasePageQuery query); + + /** + * 获取服务器实时状态(从1Panel获取) + */ + ServerInfoDto getServerStatus(Long id); + + /** + * 获取绑定到该服务器的项目名称列表 + */ + java.util.List getBindingProjects(Long serverId); + + /** + * 删除该服务器绑定的所有域名 + */ + void deleteBindingDomains(Long serverId); + + /** + * 删除该服务器绑定的所有证书 + */ + void deleteBindingCertificates(Long serverId); +} + diff --git a/src/main/java/com/nanxiislet/admin/service/SysApprovalInstanceService.java b/src/main/java/com/nanxiislet/admin/service/SysApprovalInstanceService.java new file mode 100644 index 0000000..2b6d546 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysApprovalInstanceService.java @@ -0,0 +1,50 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.dto.approval.ApprovalInstanceVO; +import com.nanxiislet.admin.entity.SysApprovalInstance; + +/** + * 审批实例服务接口 + * + * @author NanxiIslet + * @since 2026-01-09 + */ +public interface SysApprovalInstanceService extends IService { + + /** + * 分页查询实例列表 + */ + IPage listPage(Integer page, Integer pageSize, String scenario, String status, String keyword); + + /** + * 获取实例详情 + */ + ApprovalInstanceVO getDetail(Long id); + + /** + * 创建审批实例 + */ + Long create(String businessType, Long businessId, String businessTitle, Long initiatorId); + + /** + * 提交审批 + */ + void submit(Long id); + + /** + * 审批操作 + */ + void approve(Long id, Long nodeId, Long approverId, String action, String comment); + + /** + * 撤回审批 + */ + void withdraw(Long id); + + /** + * 取消审批 + */ + void cancel(Long id); +} diff --git a/src/main/java/com/nanxiislet/admin/service/SysApprovalTemplateService.java b/src/main/java/com/nanxiislet/admin/service/SysApprovalTemplateService.java new file mode 100644 index 0000000..fe5f94e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysApprovalTemplateService.java @@ -0,0 +1,59 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +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.entity.SysApprovalTemplate; + +import java.util.List; + +/** + * 审批流程模板服务接口 + * + * @author NanxiIslet + * @since 2026-01-09 + */ +public interface SysApprovalTemplateService extends IService { + + /** + * 获取审批统计数据 + */ + ApprovalStats getStats(); + + /** + * 分页查询模板列表 + */ + IPage listPage(Integer page, Integer pageSize, String scenario, Boolean enabled); + + /** + * 获取所有模板(包含节点) + */ + List listAll(String scenario); + + /** + * 获取模板详情(包含节点) + */ + ApprovalTemplateVO getDetail(Long id); + + /** + * 创建模板 + */ + Long create(ApprovalTemplateRequest request); + + /** + * 更新模板 + */ + void update(ApprovalTemplateRequest request); + + /** + * 删除模板 + */ + void delete(Long id); + + /** + * 切换启用状态 + */ + void toggle(Long id); +} diff --git a/src/main/java/com/nanxiislet/admin/service/SysDeptService.java b/src/main/java/com/nanxiislet/admin/service/SysDeptService.java new file mode 100644 index 0000000..bc372e8 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysDeptService.java @@ -0,0 +1,45 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.entity.SysDept; + +import java.util.List; + +/** + * 部门服务接口 + * + * @author NanxiIslet + * @since 2026-01-09 + */ +public interface SysDeptService extends IService { + + /** + * 获取部门树形结构 + */ + List listTree(); + + /** + * 获取所有部门列表(扁平结构) + */ + List listAll(); + + /** + * 创建部门 + */ + Long create(SysDept dept); + + /** + * 更新部门 + */ + void updateDept(SysDept dept); + + /** + * 删除部门 + */ + void deleteDept(Long id); + + /** + * 获取部门用户列表 + */ + List getDeptUserIds(Long deptId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/SysDictItemService.java b/src/main/java/com/nanxiislet/admin/service/SysDictItemService.java new file mode 100644 index 0000000..18a593f --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysDictItemService.java @@ -0,0 +1,45 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.entity.SysDictItem; + +import java.util.List; + +/** + * 字典项 Service + * + * @author NanxiIslet + * @since 2026-01-08 + */ +public interface SysDictItemService extends IService { + + /** + * 根据字典ID获取字典项列表 + */ + List listByDictId(Long dictId); + + /** + * 根据字典编码获取字典项列表 + */ + List listByDictCode(String dictCode); + + /** + * 创建字典项 + */ + Long create(SysDictItem item); + + /** + * 更新字典项 + */ + void updateItem(SysDictItem item); + + /** + * 删除字典项 + */ + void deleteItem(Long id); + + /** + * 根据字典ID删除所有字典项 + */ + void deleteByDictId(Long dictId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/SysDictService.java b/src/main/java/com/nanxiislet/admin/service/SysDictService.java new file mode 100644 index 0000000..eaeaa27 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysDictService.java @@ -0,0 +1,39 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.entity.SysDict; + +/** + * 字典类型 Service + * + * @author NanxiIslet + * @since 2026-01-08 + */ +public interface SysDictService extends IService { + + /** + * 分页查询字典 + */ + IPage page(Integer page, Integer pageSize, String name, String code); + + /** + * 根据编码获取字典 + */ + SysDict getByCode(String code); + + /** + * 创建字典 + */ + Long create(SysDict dict); + + /** + * 更新字典 + */ + void updateDict(SysDict dict); + + /** + * 删除字典 + */ + void deleteDict(Long id); +} diff --git a/src/main/java/com/nanxiislet/admin/service/SysMenuService.java b/src/main/java/com/nanxiislet/admin/service/SysMenuService.java new file mode 100644 index 0000000..530c170 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysMenuService.java @@ -0,0 +1,50 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.entity.SysMenu; + +import java.util.List; + +/** + * 菜单服务接口 + * + * @author NanxiIslet + * @since 2024-01-08 + */ +public interface SysMenuService extends IService { + + /** + * 获取所有菜单(树形结构) + */ + List listTree(); + + /** + * 获取所有菜单(平铺结构) + */ + List listAll(); + + /** + * 创建菜单 + */ + Long create(SysMenu menu); + + /** + * 更新菜单 + */ + void updateMenu(SysMenu menu); + + /** + * 删除菜单 + */ + void deleteMenu(Long id); + + /** + * 根据角色编码获取菜单(树形结构) + */ + List getMenusByRoleCode(String roleCode); + + /** + * 根据角色ID获取菜单 + */ + List getMenusByRoleId(Long roleId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/SysRoleService.java b/src/main/java/com/nanxiislet/admin/service/SysRoleService.java new file mode 100644 index 0000000..ceeff5e --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/SysRoleService.java @@ -0,0 +1,56 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.entity.SysRole; + +import java.util.List; + +/** + * 角色服务接口 + * + * @author NanxiIslet + * @since 2024-01-08 + */ +public interface SysRoleService extends IService { + + /** + * 分页查询角色 + */ + IPage listPage(Integer page, Integer pageSize, String keyword); + + /** + * 获取所有有效角色 + */ + List listAll(); + + /** + * 创建角色 + */ + Long create(SysRole role); + + /** + * 更新角色 + */ + void updateRole(SysRole role); + + /** + * 删除角色 + */ + void deleteRole(Long id); + + /** + * 根据编码查询角色 + */ + SysRole getByCode(String code); + + /** + * 分配菜单权限 + */ + void assignMenus(Long roleId, List menuIds); + + /** + * 获取角色的菜单ID列表 + */ + List getRoleMenuIds(Long roleId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/UserService.java b/src/main/java/com/nanxiislet/admin/service/UserService.java new file mode 100644 index 0000000..fd67fbf --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/UserService.java @@ -0,0 +1,63 @@ +package com.nanxiislet.admin.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.nanxiislet.admin.dto.system.UserQuery; +import com.nanxiislet.admin.entity.SysUser; + +/** + * 用户管理服务接口 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +public interface UserService extends IService { + + /** + * 分页查询用户 + * + * @param query 查询条件 + * @return 分页结果 + */ + Page listPage(UserQuery query); + + /** + * 创建用户 + * + * @param user 用户信息 + */ + void createUser(SysUser user); + + /** + * 更新用户 + * + * @param user 用户信息 + */ + void updateUser(SysUser user); + + /** + * 重置密码 + * + * @param userId 用户ID + * @param newPassword 新密码 + */ + void resetPassword(Long userId, String newPassword); + + /** + * 修改密码 + * + * @param userId 用户ID + * @param oldPassword 旧密码 + * @param newPassword 新密码 + */ + void changePassword(Long userId, String oldPassword, String newPassword); + + /** + * 检查用户名是否存在 + * + * @param username 用户名 + * @param excludeId 排除的用户ID + * @return 是否存在 + */ + boolean isUsernameExists(String username, Long excludeId); +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/AuthServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..b5c7644 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/AuthServiceImpl.java @@ -0,0 +1,223 @@ +package com.nanxiislet.admin.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.util.IdUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.google.code.kaptcha.Producer; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.common.result.ResultCode; +import com.nanxiislet.admin.dto.auth.*; +import com.nanxiislet.admin.entity.SysMenu; +import com.nanxiislet.admin.entity.SysUser; +import com.nanxiislet.admin.mapper.SysUserMapper; +import com.nanxiislet.admin.service.AuthService; +import com.nanxiislet.admin.service.SysMenuService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 认证服务实现 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Slf4j +@Service +public class AuthServiceImpl extends ServiceImpl implements AuthService { + + private static final String CAPTCHA_PREFIX = "captcha:"; + private static final long CAPTCHA_EXPIRE_SECONDS = 300L; + + @Resource + private Producer captchaProducer; + + @Resource + private RedisTemplate redisTemplate; + + @Resource + private SysMenuService menuService; + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @Override + public CaptchaResponse generateCaptcha() { + String captchaKey = IdUtil.fastSimpleUUID(); + String captchaText = captchaProducer.createText(); + + // 存入Redis + String redisKey = CAPTCHA_PREFIX + captchaKey; + redisTemplate.opsForValue().set(redisKey, captchaText, CAPTCHA_EXPIRE_SECONDS, TimeUnit.SECONDS); + + // 生成图片 + BufferedImage image = captchaProducer.createImage(captchaText); + String base64Image; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", baos); + base64Image = "data:image/png;base64," + Base64.getEncoder().encodeToString(baos.toByteArray()); + } catch (Exception e) { + log.error("生成验证码图片失败", e); + throw new BusinessException("生成验证码失败"); + } + + log.debug("生成验证码: key={}, text={}", captchaKey, captchaText); + + return CaptchaResponse.builder() + .captchaKey(captchaKey) + .captchaImage(base64Image) + .build(); + } + + @Override + public LoginResponse login(LoginRequest request) { + // 验证验证码 + String redisKey = CAPTCHA_PREFIX + request.getCaptchaKey(); + Object cachedCaptcha = redisTemplate.opsForValue().get(redisKey); + if (cachedCaptcha == null) { + throw new BusinessException(ResultCode.CAPTCHA_EXPIRED); + } + if (!cachedCaptcha.toString().equalsIgnoreCase(request.getCaptcha())) { + throw new BusinessException(ResultCode.CAPTCHA_ERROR); + } + // 删除已使用的验证码 + redisTemplate.delete(redisKey); + + // 查询用户 + SysUser user = getByUsername(request.getUsername()); + if (user == null) { + throw new BusinessException(ResultCode.USER_PASSWORD_ERROR); + } + + // 验证密码 + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + throw new BusinessException(ResultCode.USER_PASSWORD_ERROR); + } + + // 检查用户状态 + if (user.getStatus() != 1) { + throw new BusinessException(ResultCode.USER_DISABLED); + } + + // 更新登录信息 + user.setLastLoginTime(LocalDateTime.now()); + updateById(user); + + // Sa-Token 登录 + StpUtil.login(user.getId()); + String token = StpUtil.getTokenValue(); + + // 构建用户信息 + UserInfoVO userInfo = buildUserInfoVO(user); + + log.info("用户登录成功: userId={}, username={}", user.getId(), user.getUsername()); + + return LoginResponse.builder() + .token(token) + .expires(StpUtil.getTokenTimeout()) + .userInfo(userInfo) + .build(); + } + + @Override + public void logout() { + if (StpUtil.isLogin()) { + log.info("用户登出: userId={}", StpUtil.getLoginId()); + StpUtil.logout(); + } + } + + @Override + public UserInfoVO getCurrentUser() { + if (!StpUtil.isLogin()) { + throw new BusinessException(ResultCode.UNAUTHORIZED); + } + + Long userId = Long.parseLong(StpUtil.getLoginId().toString()); + SysUser user = getById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_EXIST); + } + + return buildUserInfoVO(user); + } + + @Override + public SysUser getByUsername(String username) { + return getOne(new LambdaQueryWrapper() + .eq(SysUser::getUsername, username)); + } + + /** + * 构建用户信息VO + */ + private UserInfoVO buildUserInfoVO(SysUser user) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 获取用户角色对应的菜单权限 + List permissions = getPermissionsByRole(user.getRole()); + + return UserInfoVO.builder() + .id(user.getId()) + .username(user.getUsername()) + .nickname(user.getNickname()) + .avatar(user.getAvatar()) + .email(user.getEmail()) + .phone(user.getPhone()) + .role(user.getRole()) + .permissions(permissions) + .menus(getMenusByRole(user.getRole())) + .createTime(user.getCreatedAt() != null ? user.getCreatedAt().format(formatter) : null) + .lastLoginTime(user.getLastLoginTime() != null ? user.getLastLoginTime().format(formatter) : null) + .build(); + } + + /** + * 根据角色获取权限列表 + */ + private List getPermissionsByRole(String roleCode) { + // 超级管理员返回所有权限 + if ("super_admin".equals(roleCode)) { + return List.of("*"); + } + + // 根据角色编码获取菜单 + List menus = menuService.getMenusByRoleCode(roleCode); + if (menus == null || menus.isEmpty()) { + return List.of(); + } + + // 返回菜单编码列表作为权限 + return menus.stream() + .map(SysMenu::getCode) + .distinct() + .toList(); + } + + /** + * 根据角色获取菜单列表 + */ + private List getMenusByRole(String roleCode) { + // 超级管理员返回所有菜单 + if ("super_admin".equals(roleCode)) { + // 获取所有启用状态的菜单 + return menuService.list(new LambdaQueryWrapper() + .eq(SysMenu::getStatus, 1) + .orderByAsc(SysMenu::getSort)); + } + + // 其他角色根据关联查询 + return menuService.getMenusByRoleCode(roleCode); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceAccountServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceAccountServiceImpl.java new file mode 100644 index 0000000..d12a459 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceAccountServiceImpl.java @@ -0,0 +1,32 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.entity.FinanceAccount; +import com.nanxiislet.admin.mapper.FinanceAccountMapper; +import com.nanxiislet.admin.service.FinanceAccountService; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 财务账户服务实现 + */ +@Service +public class FinanceAccountServiceImpl extends ServiceImpl implements FinanceAccountService { + + @Override + public List listActive() { + return list(new LambdaQueryWrapper() + .eq(FinanceAccount::getStatus, "active") + .orderByDesc(FinanceAccount::getIsDefault) + .orderByAsc(FinanceAccount::getCreatedAt)); + } + + @Override + public FinanceAccount getDefault() { + return getOne(new LambdaQueryWrapper() + .eq(FinanceAccount::getIsDefault, true) + .eq(FinanceAccount::getStatus, "active")); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceBudgetServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceBudgetServiceImpl.java new file mode 100644 index 0000000..aa63ed0 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceBudgetServiceImpl.java @@ -0,0 +1,108 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceBudget; +import com.nanxiislet.admin.mapper.FinanceBudgetMapper; +import com.nanxiislet.admin.service.FinanceBudgetService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 预算管理服务实现 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Service +public class FinanceBudgetServiceImpl extends ServiceImpl implements FinanceBudgetService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(FinanceBudget::getName, query.getKeyword()) + .or().like(FinanceBudget::getDepartmentName, query.getKeyword()) + .or().like(FinanceBudget::getProjectName, query.getKeyword()) + ); + } + + // 默认按年份、创建时间倒序 + wrapper.orderByDesc(FinanceBudget::getYear) + .orderByDesc(FinanceBudget::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public FinanceBudget findBudget(Integer year, String period, Integer quarter, Integer month, Long departmentId, Long projectId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(FinanceBudget::getYear, year) + .eq(FinanceBudget::getPeriod, period); + + if (quarter != null) { + wrapper.eq(FinanceBudget::getQuarter, quarter); + } + if (month != null) { + wrapper.eq(FinanceBudget::getMonth, month); + } + if (departmentId != null) { + wrapper.eq(FinanceBudget::getDepartmentId, departmentId); + } + if (projectId != null) { + wrapper.eq(FinanceBudget::getProjectId, projectId); + } + + return getOne(wrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUsedAmount(Long budgetId, BigDecimal usedAmount) { + FinanceBudget budget = getById(budgetId); + if (budget == null) { + return; + } + + BigDecimal newUsedAmount = budget.getUsedAmount().add(usedAmount); + BigDecimal remainingAmount = budget.getTotalBudget().subtract(newUsedAmount); + + FinanceBudget update = new FinanceBudget(); + update.setId(budgetId); + update.setUsedAmount(newUsedAmount); + update.setRemainingAmount(remainingAmount.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : remainingAmount); + + updateById(update); + calculateUsageRate(budgetId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void calculateUsageRate(Long budgetId) { + FinanceBudget budget = getById(budgetId); + if (budget == null || budget.getTotalBudget().compareTo(BigDecimal.ZERO) == 0) { + return; + } + + BigDecimal usageRate = budget.getUsedAmount() + .divide(budget.getTotalBudget(), 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")) + .setScale(2, RoundingMode.HALF_UP); + + FinanceBudget update = new FinanceBudget(); + update.setId(budgetId); + update.setUsageRate(usageRate); + updateById(update); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceExpenseServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceExpenseServiceImpl.java new file mode 100644 index 0000000..84b46d4 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceExpenseServiceImpl.java @@ -0,0 +1,48 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceExpense; +import com.nanxiislet.admin.mapper.FinanceExpenseMapper; +import com.nanxiislet.admin.service.FinanceExpenseService; +import org.springframework.stereotype.Service; + +import java.util.Date; + +/** + * 支出管理服务实现 + */ +@Service +public class FinanceExpenseServiceImpl extends ServiceImpl implements FinanceExpenseService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(FinanceExpense::getExpenseNo, query.getKeyword()) + .or().like(FinanceExpense::getTitle, query.getKeyword()) + .or().like(FinanceExpense::getPayeeName, query.getKeyword()) + ); + } + + wrapper.orderByDesc(FinanceExpense::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public String generateExpenseNo() { + String prefix = "EX" + DateUtil.format(new Date(), "yyyyMMdd"); + long count = count(new LambdaQueryWrapper() + .likeRight(FinanceExpense::getExpenseNo, prefix)); + return prefix + String.format("%04d", count + 1); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceIncomeServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceIncomeServiceImpl.java new file mode 100644 index 0000000..db4d886 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceIncomeServiceImpl.java @@ -0,0 +1,53 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceIncome; +import com.nanxiislet.admin.mapper.FinanceIncomeMapper; +import com.nanxiislet.admin.service.FinanceIncomeService; +import org.springframework.stereotype.Service; + +import java.util.Date; + +/** + * 收入管理服务实现 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Service +public class FinanceIncomeServiceImpl extends ServiceImpl implements FinanceIncomeService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(FinanceIncome::getIncomeNo, query.getKeyword()) + .or().like(FinanceIncome::getTitle, query.getKeyword()) + .or().like(FinanceIncome::getCustomerName, query.getKeyword()) + ); + } + + // 默认按创建时间倒序 + wrapper.orderByDesc(FinanceIncome::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public String generateIncomeNo() { + String prefix = "IN" + DateUtil.format(new Date(), "yyyyMMdd"); + long count = count(new LambdaQueryWrapper() + .likeRight(FinanceIncome::getIncomeNo, prefix)); + return prefix + String.format("%04d", count + 1); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceInvoiceServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceInvoiceServiceImpl.java new file mode 100644 index 0000000..b122d56 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceInvoiceServiceImpl.java @@ -0,0 +1,81 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.common.result.ResultCode; +import com.nanxiislet.admin.entity.FinanceInvoice; +import com.nanxiislet.admin.mapper.FinanceInvoiceMapper; +import com.nanxiislet.admin.service.FinanceInvoiceService; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +/** + * 发票管理服务实现 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Service +public class FinanceInvoiceServiceImpl extends ServiceImpl implements FinanceInvoiceService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(FinanceInvoice::getTitle, query.getKeyword()) + .or().like(FinanceInvoice::getTaxCode, query.getKeyword()) + ); + } + + // 默认按创建时间倒序 + wrapper.orderByDesc(FinanceInvoice::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public void issueInvoice(Long id) { + FinanceInvoice invoice = getById(id); + if (invoice == null) { + throw new BusinessException(ResultCode.DATA_NOT_EXIST); + } + + if (!"pending".equals(invoice.getStatus())) { + throw new BusinessException("只有待开票状态的发票才能开具"); + } + + FinanceInvoice update = new FinanceInvoice(); + update.setId(id); + update.setStatus("issued"); + update.setIssueTime(LocalDate.now()); + updateById(update); + } + + @Override + public void rejectInvoice(Long id, String reason) { + FinanceInvoice invoice = getById(id); + if (invoice == null) { + throw new BusinessException(ResultCode.DATA_NOT_EXIST); + } + + if (!"pending".equals(invoice.getStatus())) { + throw new BusinessException("只有待开票状态的发票才能驳回"); + } + + FinanceInvoice update = new FinanceInvoice(); + update.setId(id); + update.setStatus("rejected"); + update.setRejectReason(reason); + updateById(update); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceReimbursementServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceReimbursementServiceImpl.java new file mode 100644 index 0000000..9084e04 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceReimbursementServiceImpl.java @@ -0,0 +1,48 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.FinanceReimbursement; +import com.nanxiislet.admin.mapper.FinanceReimbursementMapper; +import com.nanxiislet.admin.service.FinanceReimbursementService; +import org.springframework.stereotype.Service; + +import java.util.Date; + +/** + * 报销管理服务实现 + */ +@Service +public class FinanceReimbursementServiceImpl extends ServiceImpl implements FinanceReimbursementService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(FinanceReimbursement::getReimbursementNo, query.getKeyword()) + .or().like(FinanceReimbursement::getTitle, query.getKeyword()) + .or().like(FinanceReimbursement::getApplicantName, query.getKeyword()) + ); + } + + wrapper.orderByDesc(FinanceReimbursement::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public String generateReimbursementNo() { + String prefix = "RB" + DateUtil.format(new Date(), "yyyyMMdd"); + long count = count(new LambdaQueryWrapper() + .likeRight(FinanceReimbursement::getReimbursementNo, prefix)); + return prefix + String.format("%04d", count + 1); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/FinanceSettlementServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/FinanceSettlementServiceImpl.java new file mode 100644 index 0000000..acaadf8 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/FinanceSettlementServiceImpl.java @@ -0,0 +1,131 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.common.result.ResultCode; +import com.nanxiislet.admin.entity.FinanceSettlement; +import com.nanxiislet.admin.mapper.FinanceSettlementMapper; +import com.nanxiislet.admin.service.FinanceSettlementService; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; + +/** + * 结算管理服务实现 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Service +public class FinanceSettlementServiceImpl extends ServiceImpl implements FinanceSettlementService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(FinanceSettlement::getProjectName, query.getKeyword()) + .or().like(FinanceSettlement::getTalentName, query.getKeyword()) + .or().like(FinanceSettlement::getPeriod, query.getKeyword()) + ); + } + + // 默认按创建时间倒序 + wrapper.orderByDesc(FinanceSettlement::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public void approve(Long id) { + FinanceSettlement settlement = getById(id); + if (settlement == null) { + throw new BusinessException(ResultCode.DATA_NOT_EXIST); + } + + if (!"pending".equals(settlement.getStatus())) { + throw new BusinessException("只有待审核状态的结算单才能审核"); + } + + FinanceSettlement update = new FinanceSettlement(); + update.setId(id); + update.setStatus("paying"); + update.setAuditTime(LocalDate.now()); + updateById(update); + } + + @Override + public void reject(Long id, String reason) { + FinanceSettlement settlement = getById(id); + if (settlement == null) { + throw new BusinessException(ResultCode.DATA_NOT_EXIST); + } + + if (!"pending".equals(settlement.getStatus())) { + throw new BusinessException("只有待审核状态的结算单才能驳回"); + } + + FinanceSettlement update = new FinanceSettlement(); + update.setId(id); + update.setStatus("rejected"); + update.setRemark(reason); + updateById(update); + } + + @Override + public void confirmPayment(Long id) { + FinanceSettlement settlement = getById(id); + if (settlement == null) { + throw new BusinessException(ResultCode.DATA_NOT_EXIST); + } + + if (!"paying".equals(settlement.getStatus())) { + throw new BusinessException("只有待打款状态的结算单才能确认打款"); + } + + FinanceSettlement update = new FinanceSettlement(); + update.setId(id); + update.setStatus("completed"); + update.setPaymentTime(LocalDate.now()); + updateById(update); + } + + @Override + public void calculateAmounts(FinanceSettlement settlement) { + BigDecimal totalAmount = settlement.getTotalAmount(); + if (totalAmount == null) { + totalAmount = BigDecimal.ZERO; + } + + BigDecimal platformFee = settlement.getPlatformFee(); + if (platformFee == null) { + platformFee = BigDecimal.ZERO; + } + + // 应纳税所得额 = 总额 - 平台服务费 + BigDecimal taxableAmount = totalAmount.subtract(platformFee); + settlement.setTaxableAmount(taxableAmount); + + // 计算税金 + BigDecimal taxRate = settlement.getTaxRate(); + if (taxRate == null) { + taxRate = new BigDecimal("0.03"); // 默认3% + } + BigDecimal taxAmount = taxableAmount.multiply(taxRate).setScale(2, RoundingMode.HALF_UP); + settlement.setTaxAmount(taxAmount); + + // 实发金额 = 应纳税所得额 - 税金 + BigDecimal actualAmount = taxableAmount.subtract(taxAmount); + settlement.setActualAmount(actualAmount); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/OnePanelServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/OnePanelServiceImpl.java new file mode 100644 index 0000000..2e8f889 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/OnePanelServiceImpl.java @@ -0,0 +1,2336 @@ +package com.nanxiislet.admin.service.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.nanxiislet.admin.dto.DeployRequest; +import com.nanxiislet.admin.dto.DeployResult; +import com.nanxiislet.admin.entity.PlatformProject; +import com.nanxiislet.admin.entity.PlatformServer; +import com.nanxiislet.admin.entity.PlatformCertificate; +import com.nanxiislet.admin.entity.PlatformDomain; +import com.nanxiislet.admin.service.OnePanelService; +import com.nanxiislet.admin.service.PlatformCertificateService; +import com.nanxiislet.admin.service.PlatformDomainService; +import com.nanxiislet.admin.service.PlatformProjectService; +import com.nanxiislet.admin.service.PlatformServerService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.client.SimpleClientHttpRequestFactory; + +/** + * 1Panel服务实现 + * + * @author NanxiIslet + * @since 2024-01-10 + */ +@Slf4j +@Service +public class OnePanelServiceImpl implements OnePanelService { + + @Resource + private PlatformProjectService projectService; + + @Resource + private PlatformDomainService domainService; + + @Resource + private PlatformCertificateService certificateService; + + @Resource + private PlatformServerService serverService; + + @Resource + private ObjectMapper objectMapper; + + // 普通 API 请求的 RestTemplate + private final RestTemplate restTemplate; + + // 专门用于文件上传的 RestTemplate,具有更长的超时时间 + private final RestTemplate uploadRestTemplate; + + public OnePanelServiceImpl() { + // 普通 API 请求配置:30秒连接超时,2分钟读取超时 + SimpleClientHttpRequestFactory normalFactory = new SimpleClientHttpRequestFactory(); + normalFactory.setConnectTimeout(30 * 1000); // 30秒 + normalFactory.setReadTimeout(2 * 60 * 1000); // 2分钟 + this.restTemplate = new RestTemplate(normalFactory); + + // 文件上传专用配置:5分钟连接超时,30分钟读取超时 + SimpleClientHttpRequestFactory uploadFactory = new SimpleClientHttpRequestFactory(); + uploadFactory.setConnectTimeout(5 * 60 * 1000); // 5分钟 + uploadFactory.setReadTimeout(30 * 60 * 1000); // 30分钟 + this.uploadRestTemplate = new RestTemplate(uploadFactory); + } + + /** + * 生成1Panel Token + * Token = md5('1panel' + API-Key + UnixTimestamp) + */ + private String generateToken(String apiKey, long timestamp) { + String rawToken = "1panel" + apiKey + timestamp; + return DigestUtils.md5DigestAsHex(rawToken.getBytes()); + } + + /** + * 创建HTTP请求头 + */ + private HttpHeaders createHeaders(PlatformServer server) { + long timestamp = System.currentTimeMillis() / 1000; + String token = generateToken(server.getPanelApiKey(), timestamp); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("1Panel-Token", token); + headers.set("1Panel-Timestamp", String.valueOf(timestamp)); + return headers; + } + + /** + * 获取1Panel API基础URL + */ + private String getBaseUrl(PlatformServer server) { + if (StringUtils.hasText(server.getPanelUrl())) { + String baseUrl = server.getPanelUrl(); + // 尝试去除URL中的路径部分(例如安全入口 /super),只保留 协议://IP:端口 + try { + java.net.URI uri = new java.net.URI(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + + if (scheme != null && host != null) { + StringBuilder cleanUrl = new StringBuilder(scheme).append("://").append(host); + if (port != -1) { + cleanUrl.append(":").append(port); + } + return cleanUrl.toString(); + } + } catch (Exception e) { + log.warn("解析Panel URL失败: {}", baseUrl, e); + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + return baseUrl; + } + String protocol = "http"; + String ip = server.getIp(); + Integer port = server.getPanelPort() != null ? server.getPanelPort() : 42588; + return protocol + "://" + ip + ":" + port; + } + + /** + * 发送GET请求到1Panel + */ + private String sendGet(PlatformServer server, String path) { + String url = getBaseUrl(server) + path; + // log.info("1Panel GET Request: {}", url); + HttpHeaders headers = createHeaders(server); + HttpEntity entity = new HttpEntity<>(headers); + + try { + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + return response.getBody(); + } catch (Exception e) { + log.error("1Panel GET请求失败: {}", url, e); + throw new RuntimeException("1Panel API请求失败: " + e.getMessage()); + } + } + + /** + * 发送POST请求到1Panel + */ + private String sendPost(PlatformServer server, String path, Object body) { + String url = getBaseUrl(server) + path; + log.info("1Panel POST Request: {}", url); // 添加日志 + HttpHeaders headers = createHeaders(server); + + try { + String jsonBody = body != null ? objectMapper.writeValueAsString(body) : "{}"; + log.info("1Panel POST Body: {}", jsonBody); // 打印请求体 + HttpEntity entity = new HttpEntity<>(jsonBody, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); + return response.getBody(); + } catch (Exception e) { + log.error("1Panel POST请求失败: {}", url, e); + // 尝试获取更多错误信息,比如是不是返回了HTML + if (e.getMessage() != null && e.getMessage().contains("<")) { + log.error("1Panel API返回了非JSON格式数据。可能是URL错误(包含安全入口?)或认证失败。"); + } + throw new RuntimeException("1Panel API请求失败: " + e.getMessage()); + } + } + + @Override + public DeployResult deployProject(DeployRequest request) { + DeployResult result = DeployResult.success("部署流程开始"); + + try { + // 1. 获取项目信息 + PlatformProject project = projectService.getById(request.getProjectId()); + if (project == null) { + return DeployResult.failed("项目不存在"); + } + + // 2. 检查项目是否绑定域名 + if (!StringUtils.hasText(project.getDomain())) { + return DeployResult.failed("项目未绑定域名,无法部署"); + } + + // 3. 获取服务器信息 + PlatformServer server = serverService.getById(project.getServerId()); + if (server == null) { + return DeployResult.failed("项目未绑定服务器"); + } + + // 4. 检查服务器1Panel配置 + if (!StringUtils.hasText(server.getPanelApiKey())) { + return DeployResult.failed("服务器未配置1Panel API密钥"); + } + + // 更新项目状态为部署中 + project.setStatus("deploying"); + project.setLastDeployTime(LocalDateTime.now()); + project.setLastDeployStatus("deploying"); + projectService.updateById(project); + + // 5. 检查/创建网站 + Long websiteId = checkOrCreateWebsite(server, project, request, result); + if (websiteId == null && !result.getSuccess()) { + updateProjectDeployStatus(project, "failed", result.getMessage()); + return result; + } + result.setWebsiteId(websiteId); + + // 6. 如果启用HTTPS,检查/申请证书 + if (Boolean.TRUE.equals(request.getEnableHttps())) { + Long sslId = checkOrApplyCertificate(server, project, request, result); + result.setSslCertificateId(sslId); + + // 7. 配置HTTPS + if (sslId != null && websiteId != null) { + configureWebsiteHttps(server, websiteId, sslId, result); + } + } else { + result.addSkippedStep("配置HTTPS", "未启用HTTPS"); + } + + // 更新项目状态 + project.setPanelWebsiteId(websiteId); + project.setPanelSslId(result.getSslCertificateId()); + project.setEnableHttps(request.getEnableHttps()); + + result.setSuccess(true); + result.setMessage("部署流程完成"); + updateProjectDeployStatus(project, "success", "部署成功"); + + // 更新对应域名的状态 + if (project.getDomainId() != null) { + updateDomainDeployStatus(project.getDomainId(), websiteId, result.getSslCertificateId(), project, true); + } + + return result; + + } catch (Exception e) { + log.error("部署项目失败", e); + result.setSuccess(false); + result.setMessage("部署失败: " + e.getMessage()); + result.addFailedStep("异常", e.getMessage()); + return result; + } + } + + /** + * 更新项目部署状态 + */ + private void updateProjectDeployStatus(PlatformProject project, String status, String message) { + project.setLastDeployStatus(status); + project.setLastDeployMessage(message); + project.setStatus("success".equals(status) ? "active" : "inactive"); + projectService.updateById(project); + } + + /** + * 更新域名部署状态 + */ + private void updateDomainDeployStatus(Long domainId, Long websiteId, Long sslId, PlatformProject project, boolean deployed) { + try { + PlatformDomain domain = domainService.getById(domainId); + if (domain != null) { + // 更新网站信息 + domain.setPanelWebsiteId(websiteId); + domain.setPanelSslId(sslId); + domain.setDeployStatus(deployed ? "deployed" : "failed"); + domain.setStatus(deployed ? "active" : "pending"); + domain.setLastDeployTime(java.time.LocalDateTime.now()); + domain.setLastDeployMessage(deployed ? "部署成功" : "部署失败"); + + // 更新项目关联信息 + if (project != null) { + domain.setProjectId(project.getId()); + domain.setProjectName(project.getName()); + } + + // 如果有 SSL,更新 SSL 状态 + if (sslId != null) { + domain.setSslStatus("valid"); + domain.setEnableHttps(true); + } + + domainService.updateById(domain); + log.info("域名状态已更新: domainId={}, websiteId={}, projectId={}, deployed={}", + domainId, websiteId, project != null ? project.getId() : null, deployed); + + // 部署成功后,自动检测 DNS 解析状态 + if (deployed) { + try { + domainService.checkDomainDns(domainId); + log.info("DNS检测完成: domainId={}", domainId); + } catch (Exception e) { + log.warn("DNS检测失败: domainId={}", domainId, e); + } + } + } + } catch (Exception e) { + log.error("更新域名状态失败: domainId={}", domainId, e); + } + } + + /** + * 检查或创建网站 + */ + private Long checkOrCreateWebsite(PlatformServer server, PlatformProject project, + DeployRequest request, DeployResult result) { + try { + // 先查找是否已存在该域名的网站 + Map searchParams = new HashMap<>(); + searchParams.put("page", 1); + searchParams.put("pageSize", 100); + searchParams.put("name", project.getDomain()); + searchParams.put("orderBy", "created_at"); + searchParams.put("order", "descending"); + String response = sendPost(server, "/api/v2/websites/search", searchParams); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + String domainName = project.getDomain(); + String expectedAlias = domainName; // 1Panel 实际返回的 alias 就是域名本身 + + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimaryDomain = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (expectedAlias.equals(itemAlias) || domainName.equals(itemPrimaryDomain)) { + Long websiteId = item.get("id").asLong(); + result.addSuccessStep("检查网站", "网站已存在,ID: " + websiteId); + return websiteId; + } + } + } + + // 网站不存在,需要创建 + if (!Boolean.TRUE.equals(request.getCreateIfNotExist())) { + result.setSuccess(false); + result.setMessage("网站不存在,且未设置自动创建"); + result.addFailedStep("检查网站", "网站不存在"); + return null; + } + + // 创建网站 - 按照 1Panel 实际接口格式 + Map createParams = new HashMap<>(); + String alias = domainName; // 1Panel 实际使用域名作为 alias + + // 基础参数 + createParams.put("primaryDomain", ""); // 1Panel 实际请求中为空 + createParams.put("alias", alias); + createParams.put("type", "static"); + createParams.put("appType", "installed"); // 静态网站使用 installed + createParams.put("webSiteGroupId", 1); // 注意大小写 + createParams.put("remark", "项目: " + project.getName()); + createParams.put("otherDomains", ""); + createParams.put("proxy", ""); + createParams.put("IPV6", false); + createParams.put("enableFtp", false); + createParams.put("proxyType", "tcp"); + createParams.put("port", 80); + createParams.put("proxyProtocol", "http://"); + createParams.put("proxyAddress", ""); + createParams.put("runtimeType", "php"); + createParams.put("createDb", false); + createParams.put("dbType", "mysql"); + createParams.put("dbFormat", "utf8mb4"); + + // domains 数组 + List> domains = new ArrayList<>(); + Map domainItem = new HashMap<>(); + domainItem.put("domain", domainName); + domainItem.put("port", 80); + domainItem.put("ssl", false); + domains.add(domainItem); + createParams.put("domains", domains); + + // siteDir 留空,让 1Panel 自动处理 + createParams.put("siteDir", ""); + + // SSL 配置 + Long sslIdToConfig = null; + if (project.getDomainId() != null) { + PlatformDomain domain = domainService.getById(project.getDomainId()); + if (domain != null && domain.getCertificateId() != null) { + PlatformCertificate cert = certificateService.getById(domain.getCertificateId()); + if (cert != null && cert.getPanelSslId() != null) { + sslIdToConfig = cert.getPanelSslId(); + // 在创建时启用 SSL + createParams.put("enableSSL", true); + createParams.put("websiteSSLID", cert.getPanelSslId()); + result.addSuccessStep("SSL配置", "将绑定证书: " + cert.getPrimaryDomain()); + } + } + } + if (sslIdToConfig == null) { + createParams.put("enableSSL", false); + } + + // 先调用 check 接口进行预检 + try { + Map checkParams = new HashMap<>(); + checkParams.put("primaryDomain", project.getDomain()); + checkParams.put("type", "static"); + String checkResponse = sendPost(server, "/api/v2/websites/check", checkParams); + log.info("1Panel Check Response: {}", checkResponse); + JsonNode checkResult = objectMapper.readTree(checkResponse); + if (checkResult.has("code") && checkResult.get("code").asInt() != 200) { + String errMsg = checkResult.has("message") ? checkResult.get("message").asText() : "预检失败"; + result.addFailedStep("预检网站", errMsg); + result.setSuccess(false); + result.setMessage("网站预检失败: " + errMsg); + return null; + } + result.addSuccessStep("预检网站", "域名可用"); + } catch (Exception checkEx) { + log.warn("网站预检接口调用失败,继续尝试创建: {}", checkEx.getMessage()); + } + + // 调用创建接口 + String createResponse = sendPost(server, "/api/v2/websites", createParams); + log.info("1Panel Create Response: {}", createResponse); + JsonNode createResult = objectMapper.readTree(createResponse); + + // 检查创建结果 + if (createResult.has("code") && createResult.get("code").asInt() == 200) { + // 重新查询获取网站ID + Map searchParams2 = new HashMap<>(); + searchParams2.put("page", 1); + searchParams2.put("pageSize", 10); + searchParams2.put("name", alias); + searchParams2.put("orderBy", "created_at"); + searchParams2.put("order", "descending"); + response = sendPost(server, "/api/v2/websites/search", searchParams2); + log.info("1Panel Search Response: {}", response); + jsonNode = objectMapper.readTree(response); + data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + // 用 alias 匹配而不是 primaryDomain + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimaryDomain = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (alias.equals(itemAlias) || domainName.equals(itemPrimaryDomain)) { + Long websiteId = item.get("id").asLong(); + result.addSuccessStep("创建网站", "网站创建成功,ID: " + websiteId); + + // 如果有证书,配置 HTTPS + if (sslIdToConfig != null) { + try { + configureWebsiteHttps(server, websiteId, sslIdToConfig, result); + } catch (Exception e) { + log.warn("配置HTTPS失败,但网站已创建成功: {}", e.getMessage()); + } + } + + return websiteId; + } + } + } + } else { + // 记录详细的错误信息 + String errMsg = createResult.has("message") ? createResult.get("message").asText() : "未知错误"; + log.error("1Panel 创建网站失败: code={}, message={}", + createResult.has("code") ? createResult.get("code").asInt() : "null", errMsg); + result.addFailedStep("创建网站", errMsg); + result.setSuccess(false); + result.setMessage("网站创建失败: " + errMsg); + return null; + } + + result.addFailedStep("创建网站", "网站创建失败"); + result.setSuccess(false); + result.setMessage("网站创建失败"); + return null; + + } catch (Exception e) { + log.error("检查/创建网站失败", e); + result.addFailedStep("检查网站", e.getMessage()); + result.setSuccess(false); + result.setMessage("检查网站失败: " + e.getMessage()); + return null; + } + } + + /** + * 检查或申请证书 + */ + private Long checkOrApplyCertificate(PlatformServer server, PlatformProject project, + DeployRequest request, DeployResult result) { + try { + // 先查找是否已存在该域名的证书 + String response = sendPost(server, "/api/v2/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + String projectDomain = project.getDomain(); + log.info("正在查找证书,项目域名: {}", projectDomain); + + for (JsonNode item : items) { + String primaryDomain = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + Long sslId = item.get("id").asLong(); + log.info("检查证书: primaryDomain={}, sslId={}", primaryDomain, sslId); + + // 1. 精确匹配 + if (projectDomain.equals(primaryDomain)) { + log.info("证书精确匹配成功: {} = {}, sslId={}", projectDomain, primaryDomain, sslId); + result.addSuccessStep("检查SSL证书", "证书已存在(精确匹配),ID: " + sslId); + return sslId; + } + + // 2. 通配符证书匹配 (*.example.com 匹配 test.example.com) + if (primaryDomain.startsWith("*.")) { + String wildcardBase = primaryDomain.substring(2); // example.com + if (projectDomain.endsWith("." + wildcardBase)) { + log.info("证书通配符匹配成功: {} matches {}, sslId={}", projectDomain, primaryDomain, sslId); + result.addSuccessStep("检查SSL证书", "证书已存在(通配符匹配 " + primaryDomain + "),ID: " + sslId); + return sslId; + } + } + } + log.warn("未找到匹配的证书,项目域名: {}", projectDomain); + } + + // 证书不存在,需要申请 + if (request.getAcmeAccountId() == null || request.getDnsAccountId() == null) { + result.addSkippedStep("申请SSL证书", "未配置Acme/DNS账户"); + return null; + } + + // 申请证书 + Map applyParams = new HashMap<>(); + applyParams.put("primaryDomain", project.getDomain()); + applyParams.put("otherDomains", ""); + applyParams.put("provider", "dnsAccount"); + applyParams.put("acmeAccountId", request.getAcmeAccountId()); + applyParams.put("dnsAccountId", request.getDnsAccountId()); + applyParams.put("autoRenew", true); + applyParams.put("keyType", "P256"); + applyParams.put("apply", true); + + sendPost(server, "/api/v2/websites/ssl", applyParams); + + // 等待证书申请完成(最多等待120秒) + for (int i = 0; i < 24; i++) { + Thread.sleep(5000); + + response = sendPost(server, "/api/v2/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + jsonNode = objectMapper.readTree(response); + data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String primaryDomain = item.get("primaryDomain").asText(); + if (project.getDomain().equals(primaryDomain)) { + Long sslId = item.get("id").asLong(); + result.addSuccessStep("申请SSL证书", "证书申请成功,ID: " + sslId); + return sslId; + } + } + } + } + + result.addFailedStep("申请SSL证书", "证书申请超时"); + return null; + + } catch (Exception e) { + log.error("检查/申请证书失败", e); + result.addFailedStep("检查SSL证书", e.getMessage()); + return null; + } + } + + /** + * 配置网站HTTPS + */ + private void configureWebsiteHttps(PlatformServer server, Long websiteId, Long sslId, DeployResult result) { + try { + log.info("开始配置HTTPS: websiteId={}, sslId={}", websiteId, sslId); + + Map httpsParams = new HashMap<>(); + httpsParams.put("websiteId", websiteId); // 1Panel 需要在请求体中包含 websiteId + httpsParams.put("enable", true); + httpsParams.put("type", "existed"); // existed=使用已有证书, select 不是有效值 + httpsParams.put("websiteSSLId", sslId); + httpsParams.put("httpConfig", "HTTPToHTTPS"); + httpsParams.put("SSLProtocol", new String[]{"TLSv1.2", "TLSv1.3"}); + httpsParams.put("algorithm", ""); + + String response = sendPost(server, "/api/v2/websites/" + websiteId + "/https", httpsParams); + log.info("配置HTTPS API响应: {}", response); + + // 检查响应结果 + JsonNode jsonNode = objectMapper.readTree(response); + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.addSuccessStep("配置HTTPS", "HTTPS配置成功"); + log.info("HTTPS配置成功: websiteId={}, sslId={}", websiteId, sslId); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "未知错误"; + result.addFailedStep("配置HTTPS", errMsg); + log.error("HTTPS配置失败: websiteId={}, sslId={}, error={}", websiteId, sslId, errMsg); + } + + } catch (Exception e) { + log.error("配置HTTPS失败", e); + result.addFailedStep("配置HTTPS", e.getMessage()); + } + } + + @Override + public String getWebsites(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + throw new RuntimeException("服务器不存在"); + } + return sendPost(server, "/websites/search", Map.of( + "page", 1, + "pageSize", 100 + )); + } + + @Override + public String getCertificates(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + throw new RuntimeException("服务器不存在"); + } + return sendPost(server, "/api/v2/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 100 + )); + } + + @Override + public Long checkWebsiteExists(Long serverId, String domain) { + if (domain == null || domain.isEmpty()) { + return null; + } + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return null; + } + + try { + Map searchParams = new HashMap<>(); + searchParams.put("page", 1); + searchParams.put("pageSize", 100); + searchParams.put("name", domain); + searchParams.put("orderBy", "created_at"); + searchParams.put("order", "descending"); + + String response = sendPost(server, "/api/v2/websites/search", searchParams); + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimaryDomain = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (domain.equals(itemAlias) || domain.equals(itemPrimaryDomain)) { + return item.get("id").asLong(); + } + } + } + } catch (Exception e) { + log.error("检查网站是否存在失败: {}", e.getMessage()); + } + + return null; + } + + @Override + public Map checkWebsiteStatus(Long serverId, String domain) { + Map result = new HashMap<>(); + result.put("exists", false); + + if (domain == null || domain.isEmpty()) { + return result; + } + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return result; + } + + try { + Map searchParams = new HashMap<>(); + searchParams.put("page", 1); + searchParams.put("pageSize", 100); + searchParams.put("name", domain); + searchParams.put("orderBy", "created_at"); + searchParams.put("order", "descending"); + + String response = sendPost(server, "/api/v2/websites/search", searchParams); + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimaryDomain = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (domain.equals(itemAlias) || domain.equals(itemPrimaryDomain)) { + result.put("exists", true); + result.put("id", item.get("id").asLong()); + result.put("status", item.has("status") ? item.get("status").asText() : ""); + result.put("sslStatus", item.has("sslStatus") ? item.get("sslStatus").asText() : ""); + result.put("protocol", item.has("protocol") ? item.get("protocol").asText() : ""); + result.put("sitePath", item.has("sitePath") ? item.get("sitePath").asText() : ""); + result.put("type", item.has("type") ? item.get("type").asText() : ""); + result.put("remark", item.has("remark") ? item.get("remark").asText() : ""); + result.put("sslExpireDate", item.has("sslExpireDate") ? item.get("sslExpireDate").asText() : ""); + result.put("alias", itemAlias); + result.put("primaryDomain", itemPrimaryDomain); + return result; + } + } + } + } catch (Exception e) { + log.error("检查网站状态失败: {}", e.getMessage()); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public boolean operateWebsite(Long serverId, Long websiteId, String operate) { + if (serverId == null || websiteId == null || operate == null) { + return false; + } + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + Map params = new HashMap<>(); + params.put("id", websiteId); + params.put("operate", operate); + + String response = sendPost(server, "/api/v2/websites/operate", params); + JsonNode jsonNode = objectMapper.readTree(response); + + // 检查响应状态 + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + log.info("网站操作成功: websiteId={}, operate={}", websiteId, operate); + return true; + } else { + log.error("网站操作失败: websiteId={}, operate={}, response={}", websiteId, operate, response); + } + } catch (Exception e) { + log.error("网站操作异常: websiteId={}, operate={}", websiteId, operate, e); + } + + return false; + } + + @Override + public boolean deleteWebsite(Long serverId, Long websiteId) { + if (serverId == null || websiteId == null) { + return false; + } + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + Map params = new HashMap<>(); + params.put("id", websiteId); + params.put("deleteApp", false); + params.put("deleteBackup", true); + params.put("forceDelete", true); + params.put("deleteDB", false); + + String response = sendPost(server, "/api/v2/websites/del", params); + JsonNode jsonNode = objectMapper.readTree(response); + + // 检查响应状态 + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + log.info("网站删除成功: websiteId={}", websiteId); + return true; + } else { + log.error("网站删除失败: websiteId={}, response={}", websiteId, response); + } + } catch (Exception e) { + log.error("网站删除异常: websiteId={}", websiteId, e); + } + + return false; + } + + @Override + public List checkFileBatch(Long serverId, List paths) { + if (serverId == null || paths == null || paths.isEmpty()) { + return new ArrayList<>(); + } + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return new ArrayList<>(); + } + + try { + Map params = new HashMap<>(); + params.put("paths", paths); + + String response = sendPost(server, "/api/v2/files/batch/check", params); + log.info("Batch Check Response: {}", response); // Added Log + JsonNode jsonNode = objectMapper.readTree(response); + + List existingFiles = new ArrayList<>(); + if (jsonNode.has("data") && jsonNode.get("data").isArray()) { + for (JsonNode node : jsonNode.get("data")) { + if (node.isObject() && node.has("path")) { + existingFiles.add(node.get("path").asText()); + } else { + existingFiles.add(node.asText()); + } + } + } + log.info("Parsed Existing Files: {}", existingFiles); // Added Log + return existingFiles; + } catch (Exception e) { + log.error("批量检查文件失败", e); + throw new RuntimeException("检查文件失败: " + e.getMessage()); + } + } + + @Override + public String searchFile(Long serverId, String path) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + throw new RuntimeException("服务器不存在"); + } + + try { + Map params = new HashMap<>(); + params.put("path", path); + params.put("expand", true); + params.put("showHidden", true); + params.put("page", 1); + params.put("pageSize", 100); + params.put("search", ""); + params.put("containSub", false); + params.put("sortBy", "name"); + params.put("sortOrder", "ascending"); + + String response = sendPost(server, "/api/v2/files/search", params); + // 验证响应 + JsonNode jsonNode = objectMapper.readTree(response); + if (jsonNode.has("code") && jsonNode.get("code").asInt() != 200) { + throw new RuntimeException(jsonNode.has("message") ? jsonNode.get("message").asText() : "查询失败"); + } + return response; // 返回原始JSON响应供前端或其他逻辑处理 + } catch (Exception e) { + log.error("查询文件失败", e); + throw new RuntimeException("查询文件失败: " + e.getMessage()); + } + } + + @Override + public boolean deleteFile(Long serverId, String path, boolean isDir) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + Map params = new HashMap<>(); + params.put("path", path); + params.put("isDir", isDir); + params.put("forceDelete", true); + + String response = sendPost(server, "/api/v2/files/del", params); + JsonNode jsonNode = objectMapper.readTree(response); + + return jsonNode.has("code") && jsonNode.get("code").asInt() == 200; + } catch (Exception e) { + log.error("删除文件失败: path={}", path, e); + return false; + } + } + + @Override + public String uploadFileChunk(Long serverId, String filename, String path, int chunkIndex, int chunkCount, org.springframework.web.multipart.MultipartFile fileContent) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + throw new RuntimeException("服务器不存在"); + } + + try { + org.springframework.util.MultiValueMap body = new org.springframework.util.LinkedMultiValueMap<>(); + body.add("path", path); + body.add("overwrite", "true"); // 添加 overwrite 参数,支持覆盖已存在的文件 + + // 封装文件资源 + org.springframework.core.io.Resource fileResource = new org.springframework.core.io.ByteArrayResource(fileContent.getBytes()) { + @Override + public String getFilename() { + return filename; + } + }; + body.add("file", fileResource); + + // 使用直接上传接口替代分片上传 + return sendMultipartPost(server, "/api/v2/files/upload", body); + } catch (Exception e) { + log.error("上传失败: {}", filename, e); + throw new RuntimeException("上传失败: " + e.getMessage()); + } + } + + private String sendMultipartPost(PlatformServer server, String path, org.springframework.util.MultiValueMap body) { + String url = getBaseUrl(server) + path; + HttpHeaders headers = createHeaders(server); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> entity = new HttpEntity<>(body, headers); + try { + log.info("开始上传文件到: {}", url); + long startTime = System.currentTimeMillis(); + + // 使用专门的上传 RestTemplate,具有更长的超时时间 + ResponseEntity response = uploadRestTemplate.exchange(url, HttpMethod.POST, entity, String.class); + + long duration = System.currentTimeMillis() - startTime; + log.info("文件上传完成,耗时: {}ms", duration); + + return response.getBody(); + } catch (Exception e) { + log.error("1Panel Multipart POST请求失败: {}", url, e); + throw new RuntimeException("1Panel API请求失败: " + e.getMessage()); + } + } + + @Override + public Map checkAppInstalled(Long serverId, String key, String name) { + Map result = new HashMap<>(); + result.put("isExist", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("key", key); + params.put("name", name); + + String response = sendPost(server, "/api/v2/apps/installed/check", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result.put("isExist", data.has("isExist") && data.get("isExist").asBoolean()); + result.put("name", data.has("name") ? data.get("name").asText() : ""); + result.put("app", data.has("app") ? data.get("app").asText() : ""); + result.put("version", data.has("version") ? data.get("version").asText() : ""); + result.put("status", data.has("status") ? data.get("status").asText() : ""); + result.put("createdAt", data.has("createdAt") ? data.get("createdAt").asText() : ""); + result.put("lastBackupAt", data.has("lastBackupAt") ? data.get("lastBackupAt").asText() : ""); + result.put("appInstallId", data.has("appInstallId") ? data.get("appInstallId").asLong() : 0); + result.put("containerName", data.has("containerName") ? data.get("containerName").asText() : ""); + result.put("installPath", data.has("installPath") ? data.get("installPath").asText() : ""); + result.put("httpPort", data.has("httpPort") ? data.get("httpPort").asInt() : 0); + result.put("httpsPort", data.has("httpsPort") ? data.get("httpsPort").asInt() : 0); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "检查失败"; + log.error("检查应用安装状态失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("检查应用安装状态异常: key={}, name={}", key, name, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map searchDatabases(Long serverId, String database, int page, int pageSize) { + Map result = new HashMap<>(); + result.put("total", 0); + result.put("items", new ArrayList<>()); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("page", page); + params.put("pageSize", pageSize); + params.put("database", database); + params.put("orderBy", "createdAt"); + params.put("order", "null"); + + String response = sendPost(server, "/api/v2/databases/search", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result.put("total", data.has("total") ? data.get("total").asInt() : 0); + + List> items = new ArrayList<>(); + if (data.has("items") && data.get("items").isArray()) { + for (JsonNode item : data.get("items")) { + Map dbItem = new HashMap<>(); + dbItem.put("id", item.has("id") ? item.get("id").asLong() : 0); + dbItem.put("createdAt", item.has("createdAt") ? item.get("createdAt").asText() : ""); + dbItem.put("name", item.has("name") ? item.get("name").asText() : ""); + dbItem.put("from", item.has("from") ? item.get("from").asText() : ""); + dbItem.put("mysqlName", item.has("mysqlName") ? item.get("mysqlName").asText() : ""); + dbItem.put("format", item.has("format") ? item.get("format").asText() : ""); + dbItem.put("username", item.has("username") ? item.get("username").asText() : ""); + dbItem.put("password", item.has("password") ? item.get("password").asText() : ""); + dbItem.put("permission", item.has("permission") ? item.get("permission").asText() : ""); + dbItem.put("isDelete", item.has("isDelete") && item.get("isDelete").asBoolean()); + dbItem.put("description", item.has("description") ? item.get("description").asText() : ""); + items.add(dbItem); + } + } + result.put("items", items); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "查询失败"; + log.error("查询数据库列表失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("查询数据库列表异常: database={}", database, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map createDatabase(Long serverId, Map params) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String response = sendPost(server, "/api/v2/databases", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + if (jsonNode.has("data")) { + result.put("data", objectMapper.convertValue(jsonNode.get("data"), Map.class)); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "创建失败"; + log.error("创建数据库失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("创建数据库异常", e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public boolean deleteDatabase(Long serverId, Map params) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + String response = sendPost(server, "/api/v2/databases/del", params); + JsonNode jsonNode = objectMapper.readTree(response); + + return jsonNode.has("code") && jsonNode.get("code").asInt() == 200; + } catch (Exception e) { + log.error("删除数据库异常", e); + return false; + } + } + + @Override + public boolean updateDatabaseDescription(Long serverId, Map params) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + String response = sendPost(server, "/api/v2/databases/description/update", params); + JsonNode jsonNode = objectMapper.readTree(response); + + return jsonNode.has("code") && jsonNode.get("code").asInt() == 200; + } catch (Exception e) { + log.error("更新数据库描述异常", e); + return false; + } + } + + @Override + public boolean changeDatabasePassword(Long serverId, Map params) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + String response = sendPost(server, "/api/v2/databases/change/password", params); + JsonNode jsonNode = objectMapper.readTree(response); + + return jsonNode.has("code") && jsonNode.get("code").asInt() == 200; + } catch (Exception e) { + log.error("修改数据库密码异常", e); + return false; + } + } + + @Override + public boolean operateApp(Long serverId, Map params) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return false; + } + + try { + // 构造 1Panel API 需要的参数 + // 接口 /api/v2/apps/installed/op 参数通常需要 installId 和 operate + String response = sendPost(server, "/api/v2/apps/installed/op", params); + JsonNode jsonNode = objectMapper.readTree(response); + + return jsonNode.has("code") && jsonNode.get("code").asInt() == 200; + } catch (Exception e) { + log.error("操作应用失败", e); + return false; + } + } + + @Override + public List getDatabaseFormatOptions(Long serverId, String type, String database, String format) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return new ArrayList<>(); + } + + try { + // 修正:1Panel 接口参数只需要 name (对应数据库类型) + Map params = new HashMap<>(); + params.put("name", type); // 这里 type 应该是 "mysql", "postgresql" 等 + + // 下面的参数不需要发给 1Panel + // params.put("type", type); + // params.put("database", database); + // if (format != null && !format.isEmpty()) { + // params.put("format", format); + // } + + String response = sendPost(server, "/api/v2/databases/format/options", params); + JsonNode jsonNode = objectMapper.readTree(response); + + List options = new ArrayList<>(); + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null && data.isArray()) { + for (JsonNode item : data) { + String itemFormat = item.has("format") ? item.get("format").asText() : ""; + + // 如果指定了 format,则只返回匹配 format 的 collations + if (format != null && !format.isEmpty()) { + if (itemFormat.equalsIgnoreCase(format)) { + JsonNode collations = item.get("collations"); + if (collations != null && collations.isArray()) { + for (JsonNode col : collations) { + options.add(col.asText()); + } + } + break; + } + } else { + // 如果没有指定 format,为了兼容,我们可以返回所有的 collation,但这可能太多了 + // 或者返回 format 列表? + // 目前前端在 loadCollationOptions 时总是会传 createForm.format (默认 utf8mb4) + // 所以这里主要关注 format 匹配的情况。 + // 如果没传,我们可以为了调试方便,把所有 format 打印出来? + // 还是不做处理,返回空,迫使前端传 format。 + } + } + } + } + return options; + } catch (Exception e) { + log.error("获取数据库字符集选项失败", e); + return new ArrayList<>(); + } + } + + @Override + public Map getAppInfo(Long serverId, String appKey) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String response = sendGet(server, "/api/v2/apps/" + appKey); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result = objectMapper.convertValue(data, Map.class); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取应用信息失败"; + log.error("获取应用信息失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("获取应用信息异常: appKey={}", appKey, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map getAppDetail(Long serverId, Long appId, String version) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String response = sendGet(server, "/api/v2/apps/detail/" + appId + "/" + version + "/app"); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result = objectMapper.convertValue(data, Map.class); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取应用详情失败"; + log.error("获取应用详情失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("获取应用详情异常: appId={}, version={}", appId, version, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map installApp(Long serverId, Map params) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String response = sendPost(server, "/api/v2/apps/install", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + if (jsonNode.has("data")) { + result.put("data", objectMapper.convertValue(jsonNode.get("data"), Map.class)); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "安装失败"; + log.error("安装应用失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("安装应用异常", e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map readTaskLog(Long serverId, String taskId, int page, int pageSize) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("id", 0); + params.put("type", "task"); + params.put("name", ""); + params.put("page", page); + params.put("pageSize", pageSize); + params.put("latest", false); + params.put("taskID", taskId); + params.put("taskType", ""); + params.put("taskOperate", ""); + params.put("resourceID", 0); + + String response = sendPost(server, "/api/v2/files/read", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result.put("end", data.has("end") && data.get("end").asBoolean()); + result.put("path", data.has("path") ? data.get("path").asText() : ""); + result.put("total", data.has("total") ? data.get("total").asInt() : 0); + result.put("taskStatus", data.has("taskStatus") ? data.get("taskStatus").asText() : ""); + result.put("totalLines", data.has("totalLines") ? data.get("totalLines").asInt() : 0); + + List lines = new ArrayList<>(); + if (data.has("lines") && data.get("lines").isArray()) { + for (JsonNode line : data.get("lines")) { + lines.add(line.asText()); + } + } + result.put("lines", lines); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "读取日志失败"; + log.error("读取任务日志失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("读取任务日志异常: taskId={}", taskId, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map searchRuntimes(Long serverId, String type, int page, int pageSize) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("type", type); + params.put("page", page); + params.put("pageSize", pageSize); + + String response = sendPost(server, "/api/v2/runtimes/search", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result.put("total", data.has("total") ? data.get("total").asInt() : 0); + + List> items = new ArrayList<>(); + if (data.has("items") && data.get("items").isArray()) { + for (JsonNode item : data.get("items")) { + items.add(objectMapper.convertValue(item, Map.class)); + } + } + result.put("items", items); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "搜索运行时失败"; + log.error("搜索运行时失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("搜索运行时异常: type={}", type, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map syncRuntimes(Long serverId) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String response = sendPost(server, "/api/v2/runtimes/sync", new HashMap<>()); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "同步运行时失败"; + log.error("同步运行时失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("同步运行时异常", e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map operateRuntime(Long serverId, Long id, String operate) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + // Map frontend operate values to 1Panel API values + String panelOperate = operate; + switch (operate) { + case "start": + panelOperate = "up"; + break; + case "stop": + panelOperate = "down"; + break; + case "restart": + panelOperate = "restart"; + break; + } + + Map params = new HashMap<>(); + params.put("ID", id.intValue()); // 1Panel expects uppercase ID + params.put("operate", panelOperate); + + log.info("操作运行时: id={}, operate={}, params={}", id, panelOperate, params); + + String response = sendPost(server, "/api/v2/runtimes/operate", params); + log.info("操作运行时响应: {}", response); + + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "操作运行时失败"; + log.error("操作运行时失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("操作运行时异常: id={}, operate={}", id, operate, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map deleteRuntime(Long serverId, Long id, boolean forceDelete, boolean deleteFolder, String codeDir) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("id", id); + params.put("forceDelete", forceDelete); + + String response = sendPost(server, "/api/v2/runtimes/del", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + + // 如果需要删除关联文件夹 + if (deleteFolder && codeDir != null && !codeDir.isEmpty() && !"/".equals(codeDir)) { + try { + log.info("删除运行时关联目录: {}", codeDir); + Map deleteParams = new HashMap<>(); + deleteParams.put("path", codeDir); + deleteParams.put("isDir", true); + deleteParams.put("forceDelete", true); + + String deleteResponse = sendPost(server, "/api/v2/files/del", deleteParams); + JsonNode deleteNode = objectMapper.readTree(deleteResponse); + + if (deleteNode.has("code") && deleteNode.get("code").asInt() == 200) { + log.info("目录删除成功: {}", codeDir); + result.put("folderDeleted", true); + } else { + String errMsg = deleteNode.has("message") ? deleteNode.get("message").asText() : "删除目录失败"; + log.warn("删除目录失败: {} - {}", codeDir, errMsg); + result.put("folderDeleteError", errMsg); + } + } catch (Exception e) { + log.warn("删除目录异常: {} - {}", codeDir, e.getMessage()); + result.put("folderDeleteError", e.getMessage()); + } + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "删除运行时失败"; + log.error("删除运行时失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("删除运行时异常: id={}", id, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map searchRuntimeApps(Long serverId, String type, int page, int pageSize) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("type", type); + params.put("page", page); + params.put("pageSize", pageSize); + params.put("resource", "remote"); + + String response = sendPost(server, "/api/v2/apps/search", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result.put("total", data.has("total") ? data.get("total").asInt() : 0); + + List> items = new ArrayList<>(); + if (data.has("items") && data.get("items").isArray()) { + for (JsonNode item : data.get("items")) { + items.add(objectMapper.convertValue(item, Map.class)); + } + } + result.put("items", items); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "搜索运行时应用失败"; + log.error("搜索运行时应用失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("搜索运行时应用异常: type={}", type, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map getRuntimeDetail(Long serverId, Long appId, String version) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String path = "/api/v2/apps/detail/" + appId + "/" + version + "/runtime"; + String response = sendGet(server, path); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result = objectMapper.convertValue(data, Map.class); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取运行时版本详情失败"; + log.error("获取运行时版本详情失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("获取运行时版本详情异常: appId={}, version={}", appId, version, e); + result.put("error", e.getMessage()); + } + + return result; + } + + /** + * 辅助方法:发送创建运行时请求 + */ + private Map doCreateRuntime(PlatformServer server, Map params) throws Exception { + Map result = new HashMap<>(); + String response = sendPost(server, "/api/v2/runtimes", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "创建运行时失败"; + result.put("success", false); + result.put("error", errMsg); + } + return result; + } + + @Override + public Map createRuntime(Long serverId, Map params) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + // 第一次尝试创建 + Map createResult = doCreateRuntime(server, params); + if ((Boolean) createResult.getOrDefault("success", false)) { + return createResult; + } + + String error = (String) createResult.get("error"); + // 检查是否为目录已存在错误: "rename ... dest: file exists" + // 示例: rename /opt/1panel/runtime/java/21 /opt/1panel/runtime/java/superJava: file exists + if (error != null && error.contains("rename") && error.contains("file exists")) { + log.info("检测到运行环境目录冲突,尝试清理后重试: {}", error); + + String[] parts = error.split("\\s+"); + String destPath = null; + // 简单的查找逻辑:找到 : 前面的路径 + for (int i = 0; i < parts.length; i++) { + if (parts[i].endsWith(":") && i > 0) { + destPath = parts[i-1]; + break; + } + } + + // 确保路径包含 runtime 关键字以防误删 + if (destPath != null && destPath.contains("/runtime/")) { + log.info("正在清理冲突目录: {}", destPath); + // 尝试删除冲突目录 + Map delResult = this.deleteFile(serverId, destPath, Boolean.TRUE); + if ((Boolean) delResult.getOrDefault("success", false)) { + // 删除成功,重试创建 + log.info("清理成功,重试创建运行时"); + return doCreateRuntime(server, params); + } else { + log.warn("自动清理失败: {}", delResult.get("error")); + } + } + } + + result.put("error", error); + } catch (Exception e) { + log.error("创建运行时异常", e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map listFiles(Long serverId, String path) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("path", path); + params.put("expand", true); + params.put("page", 1); + params.put("pageSize", 100); + params.put("showHidden", true); + params.put("search", ""); + params.put("containSub", false); + params.put("dir", false); + + String response = sendPost(server, "/api/v2/files/search", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + List> items = new ArrayList<>(); + if (data.has("items") && data.get("items").isArray()) { + for (JsonNode item : data.get("items")) { + Map fileItem = new HashMap<>(); + fileItem.put("name", item.has("name") ? item.get("name").asText() : ""); + fileItem.put("isDir", item.has("isDir") && item.get("isDir").asBoolean()); + fileItem.put("size", item.has("size") ? item.get("size").asLong() : 0); + fileItem.put("modTime", item.has("modTime") ? item.get("modTime").asText() : ""); + items.add(fileItem); + } + } + result.put("items", items); + result.put("total", data.has("total") ? data.get("total").asInt() : items.size()); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取文件列表失败"; + log.error("获取文件列表失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("获取文件列表异常: path={}", path, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map mkdir(Long serverId, String path) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + // 解析路径和名称 + // 如果路径以/结尾,先去掉 + String fullPath = path.endsWith("/") && path.length() > 1 ? path.substring(0, path.length() - 1) : path; + int lastSlashIndex = fullPath.lastIndexOf("/"); + + String parentPath; + String name; + + if (lastSlashIndex == -1) { + // 没有斜杠,可能是相对路径或者根目录下文件(不应该出现的情况,path通常是绝对路径) + parentPath = "/"; + name = fullPath; + } else if (lastSlashIndex == 0) { + // 根目录下的目录,例如 /app + parentPath = "/"; + name = fullPath.substring(1); + } else { + // 普通情况,例如 /opt/app + parentPath = fullPath.substring(0, lastSlashIndex); + name = fullPath.substring(lastSlashIndex + 1); + } + + Map params = new HashMap<>(); + params.put("path", fullPath); // 修正:这里使用完整路径,而不是父目录 + params.put("name", name); // 文件夹名称 + params.put("isDir", true); + params.put("mode", 493); // 0755 + params.put("isLink", false); + params.put("isSymlink", true); // 根据您的反馈,将此字段设为true(尽管通常用于软链,但既然您的正确参数里是true,我们就保持一致) + params.put("linkPath", ""); + + String response = sendPost(server, "/api/v2/files", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "创建目录失败"; + log.error("创建目录失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("创建目录异常: path={}", path, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map deleteFile(Long serverId, String path, Boolean isDir) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("path", path); + params.put("isDir", isDir); + params.put("forceDelete", true); // 强制删除,包含非空目录 + + // 使用单个删除接口 /api/v2/files/del + String response = sendPost(server, "/api/v2/files/del", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "删除失败"; + log.error("删除文件失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("删除文件异常: path={}", path, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map createFile(Long serverId, String path) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + // 解析路径和名称 + // 如果路径以/结尾,先去掉 + String fullPath = path.endsWith("/") && path.length() > 1 ? path.substring(0, path.length() - 1) : path; + int lastSlashIndex = fullPath.lastIndexOf("/"); + + String name; + + if (lastSlashIndex == -1) { + name = fullPath; + } else if (lastSlashIndex == 0) { + name = fullPath.substring(1); + } else { + name = fullPath.substring(lastSlashIndex + 1); + } + + Map params = new HashMap<>(); + params.put("path", fullPath); + params.put("name", name); + params.put("isDir", false); // 创建文件 + params.put("mode", 420); // 0644 for files usually, or use user provided example default? + // 用户没有给出mode值,mkdir是493(755)。文件一般是644(420)。 + // 让我们看看用户提供的参数:没有mode。 + // 用户之前mkdir的例子给了mode 493。 + // 对于文件,我们暂用 420 (0644)。 + params.put("isLink", false); + params.put("isSymlink", true); // Follow existing pattern requested by user + params.put("linkPath", ""); + + String response = sendPost(server, "/api/v2/files", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "创建文件失败"; + log.error("创建文件失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("创建文件异常: path={}", path, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map uploadFile(Long serverId, String path, org.springframework.web.multipart.MultipartFile file) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + org.springframework.util.MultiValueMap body = new org.springframework.util.LinkedMultiValueMap<>(); + body.add("path", path); + + // 使用 InputStreamResource 替代 ByteArrayResource,避免大文件内存占用 + body.add("file", new org.springframework.core.io.InputStreamResource(file.getInputStream()) { + @Override + public String getFilename() { + return file.getOriginalFilename(); + } + + @Override + public long contentLength() { + return file.getSize(); + } + }); + + // 获取通用Header(包含认证) + HttpHeaders headers = createHeaders(server); + // 覆盖ContentType + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + String url = getBaseUrl(server) + "/api/v2/files/upload"; + log.info("开始上传文件到: {},文件大小: {} bytes", url, file.getSize()); + long startTime = System.currentTimeMillis(); + + // 使用专门的上传 RestTemplate,具有更长的超时时间 + ResponseEntity response = uploadRestTemplate.postForEntity(url, requestEntity, String.class); + + long duration = System.currentTimeMillis() - startTime; + log.info("文件上传完成,耗时: {}ms", duration); + + JsonNode jsonNode = objectMapper.readTree(response.getBody()); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "上传文件失败"; + log.error("上传文件失败: {}", errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("上传文件异常: path={}", path, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map getContainerLog(Long serverId, String containerName, String composePath) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + if (composePath != null && !composePath.isEmpty()) { + // 优先使用 compose 路径方式获取日志 (适合 1Panel 运行时) + // RestTemplate 会自动处理 URL 编码,不需要手动 Encode,否则会导致双重编码 + String url = "/api/v2/containers/search/log?compose=" + composePath + "&tail=100&follow=false&operateNode=local&since=all"; + + // 使用 sendGet 调用 + String response = sendGet(server, url); + + if (response == null) { + result.put("success", true); + result.put("log", ""); + } else if (response.trim().startsWith("{")) { + // 尝试解析 JSON + JsonNode jsonNode = objectMapper.readTree(response); + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + JsonNode data = jsonNode.get("data"); + result.put("log", data != null ? data.asText() : ""); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取日志失败"; + result.put("error", errMsg); + } + } else { + // 如果不是 JSON,可能直接就是日志内容 + result.put("success", true); + result.put("log", response); + } + } else { + // 原有逻辑:直接根据容器名获取 + Map params = new HashMap<>(); + params.put("name", containerName); + params.put("tail", 200); + + String response = sendPost(server, "/api/v2/containers/log", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + JsonNode data = jsonNode.get("data"); + result.put("log", data != null ? data.asText() : ""); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取日志失败"; + result.put("error", errMsg); + } + } + } catch (Exception e) { + log.error("获取容器日志异常: {}", containerName, e); + result.put("error", e.getMessage()); + } + return result; + } + + + @Override + public Map updateRuntime(Long serverId, Map params) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + // 1Panel的更新接口通常是 POST /api/v2/runtimes/update + String response = sendPost(server, "/api/v2/runtimes/update", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "更新运行时失败"; + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("更新运行时异常", e); + result.put("error", e.getMessage()); + } + + return result; + } + @Override + public java.util.List> getNodeScripts(Long serverId, String codeDir) { + java.util.List> result = new java.util.ArrayList<>(); + PlatformServer server = serverService.getById(serverId); + if (server == null) { + return result; + } + + try { + java.util.Map params = new java.util.HashMap<>(); + params.put("codeDir", codeDir); + + String response = sendPost(server, "/api/v2/runtimes/node/package", params); + log.info("Node Scripts Response: {}", response); + + com.fasterxml.jackson.databind.JsonNode jsonNode = objectMapper.readTree(response); + com.fasterxml.jackson.databind.JsonNode data = null; + + if (jsonNode.isArray()) { + data = jsonNode; + } else if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + data = jsonNode.get("data"); + } + + if (data != null && data.isArray()) { + for (com.fasterxml.jackson.databind.JsonNode item : data) { + java.util.Map script = new java.util.HashMap<>(); + String name = ""; + String scriptContent = ""; + + if (item.isObject()) { + if (item.has("name")) name = item.get("name").asText(); + if (item.has("script")) scriptContent = item.get("script").asText(); + } else if (item.isTextual()) { + name = item.asText(); + scriptContent = name; + } + + script.put("name", name); + script.put("script", scriptContent); + result.add(script); + } + } + } catch (Exception e) { + log.error("获取Node脚本异常", e); + } + return result; + } + + @Override + public Map uploadChunk(Long serverId, String path, String filename, + int chunkIndex, int chunkCount, long fileSize, String uploadId, + org.springframework.web.multipart.MultipartFile chunk) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + // 1Panel chunkupload 接口参数 + org.springframework.util.MultiValueMap body = new org.springframework.util.LinkedMultiValueMap<>(); + + // path 应该是目标目录路径 + body.add("path", path); + + // 封装分片文件 - 使用原始文件名 + org.springframework.core.io.Resource fileResource = new org.springframework.core.io.ByteArrayResource(chunk.getBytes()) { + @Override + public String getFilename() { + return filename; + } + }; + body.add("file", fileResource); + + HttpHeaders headers = createHeaders(server); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + // 1Panel 的分片上传可能使用不同的 URL 或参数 + // 先尝试直接使用 /api/v2/files/upload 接口,因为它可能支持分片 + String url = getBaseUrl(server) + "/api/v2/files/upload"; + log.info("分片上传: {} - 分片 {}/{} (使用标准上传接口)", filename, chunkIndex + 1, chunkCount); + + ResponseEntity response = uploadRestTemplate.postForEntity(url, requestEntity, String.class); + + JsonNode jsonNode = objectMapper.readTree(response.getBody()); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + result.put("chunkIndex", chunkIndex); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "分片上传失败"; + log.error("分片上传失败: {} - 分片 {}/{}, 错误: {}", filename, chunkIndex + 1, chunkCount, errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("分片上传异常: filename={}, chunkIndex={}", filename, chunkIndex, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map mergeChunks(Long serverId, String path, String filename, + int chunkCount, long fileSize, String uploadId) { + Map result = new HashMap<>(); + result.put("success", false); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + Map params = new HashMap<>(); + params.put("path", path); + params.put("filename", filename); + params.put("chunkCount", chunkCount); + params.put("fileSize", fileSize); + params.put("uploadId", uploadId); + + log.info("合并分片: {} - 共 {} 个分片, 文件大小: {} bytes", filename, chunkCount, fileSize); + + String response = sendPost(server, "/api/v2/files/merge", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + result.put("success", true); + log.info("分片合并成功: {}", filename); + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "合并分片失败"; + log.error("合并分片失败: {}, 错误: {}", filename, errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("合并分片异常: filename={}", filename, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public Map getWebsiteNginxConfig(Long serverId, Long websiteId) { + Map result = new HashMap<>(); + + PlatformServer server = serverService.getById(serverId); + if (server == null) { + result.put("error", "服务器不存在"); + return result; + } + + try { + String response = sendGet(server, "/api/v2/websites/" + websiteId + "/config/openresty"); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + JsonNode data = jsonNode.get("data"); + if (data != null) { + result.put("path", data.has("path") ? data.get("path").asText() : ""); + result.put("content", data.has("content") ? data.get("content").asText() : ""); + result.put("name", data.has("name") ? data.get("name").asText() : ""); + } + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "获取Nginx配置失败"; + log.error("获取Nginx配置失败: websiteId={}, error={}", websiteId, errMsg); + result.put("error", errMsg); + } + } catch (Exception e) { + log.error("获取Nginx配置异常: websiteId={}", websiteId, e); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public boolean saveWebsiteNginxConfig(Long serverId, Long websiteId, String content) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + log.error("保存Nginx配置失败: 服务器不存在, serverId={}", serverId); + return false; + } + + try { + Map params = new HashMap<>(); + params.put("id", websiteId); + params.put("content", content); + + String response = sendPost(server, "/api/v2/websites/nginx/update", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + log.info("保存Nginx配置成功: websiteId={}", websiteId); + return true; + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "保存失败"; + log.error("保存Nginx配置失败: websiteId={}, error={}", websiteId, errMsg); + return false; + } + } catch (Exception e) { + log.error("保存Nginx配置异常: websiteId={}", websiteId, e); + return false; + } + } + + @Override + public boolean reloadNginx(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null) { + log.error("重载Nginx失败: 服务器不存在, serverId={}", serverId); + return false; + } + + try { + Map params = new HashMap<>(); + params.put("operate", "restart"); + + String response = sendPost(server, "/api/v2/openresty/operate", params); + JsonNode jsonNode = objectMapper.readTree(response); + + if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) { + log.info("重载Nginx成功: serverId={}", serverId); + return true; + } else { + String errMsg = jsonNode.has("message") ? jsonNode.get("message").asText() : "重载失败"; + log.error("重载Nginx失败: serverId={}, error={}", serverId, errMsg); + return false; + } + } catch (Exception e) { + log.error("重载Nginx异常: serverId={}", serverId, e); + return false; + } + } +} + diff --git a/src/main/java/com/nanxiislet/admin/service/impl/PlatformCertificateServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/PlatformCertificateServiceImpl.java new file mode 100644 index 0000000..255b169 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/PlatformCertificateServiceImpl.java @@ -0,0 +1,665 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.dto.CertificateApplyRequest; +import com.nanxiislet.admin.dto.CertificateApplyResult; +import com.nanxiislet.admin.entity.PlatformCertificate; +import com.nanxiislet.admin.entity.PlatformServer; +import com.nanxiislet.admin.mapper.PlatformCertificateMapper; +import com.nanxiislet.admin.service.PlatformCertificateService; +import com.nanxiislet.admin.service.PlatformServerService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * 证书管理服务实现 + * + * @author NanxiIslet + * @since 2026-01-13 + */ +@Slf4j +@Service +public class PlatformCertificateServiceImpl extends ServiceImpl + implements PlatformCertificateService { + + @Resource + private PlatformServerService serverService; + + @Resource + private ObjectMapper objectMapper; + + private final RestTemplate restTemplate = new RestTemplate(); + + // ==================== 1Panel API 工具方法 ==================== + + private String generateToken(String apiKey, long timestamp) { + String rawToken = "1panel" + apiKey + timestamp; + return DigestUtils.md5DigestAsHex(rawToken.getBytes()); + } + + private String sendGet(PlatformServer server, String path) { + return doRequest1Panel(server, path, HttpMethod.GET, null); + } + + private String sendPost(PlatformServer server, String path, Object body) { + return doRequest1Panel(server, path, HttpMethod.POST, body); + } + + private String doRequest1Panel(PlatformServer server, String path, HttpMethod method, Object body) { + String baseUrl = ""; + String apiPath = server.getPanelUrl(); + String pathPrefix = ""; + + if (StringUtils.hasText(apiPath)) { + if (apiPath.endsWith("/")) apiPath = apiPath.substring(0, apiPath.length() - 1); + baseUrl = apiPath; + // 提取 path prefix (e.g. /super) + try { + java.net.URI uri = new java.net.URI(baseUrl); + pathPrefix = uri.getPath(); + // 重新构建 base (host:port) + baseUrl = uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort(); + } catch (Exception e) { + log.warn("解析panelUrl失败: {}", apiPath); + } + } else { + String ip = server.getIp(); + Integer port = server.getPanelPort() != null ? server.getPanelPort() : 42588; + baseUrl = "http://" + ip + ":" + port; + } + + // 构造尝试列表 + List tryPaths = new ArrayList<>(); + if (StringUtils.hasText(pathPrefix)) { + tryPaths.add(pathPrefix + "/api/v2"); + tryPaths.add(pathPrefix + "/api/v1"); + } + tryPaths.add("/api/v2"); + tryPaths.add("/api/v1"); + + // 去重 + List distinctPaths = tryPaths.stream().distinct().toList(); + + for (String versionPath : distinctPaths) { + String fullUrl = baseUrl + versionPath + path; + + long localTimestamp = System.currentTimeMillis() / 1000; + ResponseEntity responseEntity = executeRequest(fullUrl, method, body, server.getPanelApiKey(), localTimestamp); + String responseBody = responseEntity.getBody(); + + boolean isHtml = responseBody != null && responseBody.trim().startsWith("<"); + + if (responseEntity.getStatusCode().is2xxSuccessful() && !isHtml) { + log.debug("1Panel API 请求成功: {}", fullUrl); + return responseBody; + } + + if (isHtml) { + // 尝试校准时间 + long serverTimestamp = -1; + long responseDate = responseEntity.getHeaders().getDate(); + if (responseDate > 0) serverTimestamp = responseDate / 1000; + else serverTimestamp = localTimestamp - 31536000; + + if (serverTimestamp > 0) { + ResponseEntity retryResponse = executeRequest(fullUrl, method, body, server.getPanelApiKey(), serverTimestamp); + String retryBody = retryResponse.getBody(); + if (retryBody != null && !retryBody.trim().startsWith("<")) { + return retryBody; + } + } + } + } + + log.error("1Panel API 所有路径尝试失败: path={}", path); + return null; + } + + private ResponseEntity executeRequest(String url, HttpMethod method, Object body, String apiKey, long timestamp) { + String token = generateToken(apiKey, timestamp); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("1Panel-Token", token); + headers.set("1Panel-Timestamp", String.valueOf(timestamp)); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + + try { + String jsonBody = body != null ? objectMapper.writeValueAsString(body) : null; + HttpEntity entity = new HttpEntity<>(jsonBody, headers); + return restTemplate.exchange(url, method, entity, String.class); + } catch (Exception e) { + if (e instanceof org.springframework.web.client.HttpStatusCodeException) { + org.springframework.web.client.HttpStatusCodeException se = (org.springframework.web.client.HttpStatusCodeException) e; + return new ResponseEntity<>(se.getResponseBodyAsString(), se.getResponseHeaders(), se.getStatusCode()); + } + log.error("Request 1Panel Error: {} {}", url, e.getMessage()); + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + // ==================== 业务方法 ==================== + + @Override + public Page listPage(Long serverId, BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (serverId != null) { + wrapper.eq(PlatformCertificate::getServerId, serverId); + } + + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.like(PlatformCertificate::getPrimaryDomain, query.getKeyword()); + } + + wrapper.orderByDesc(PlatformCertificate::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public CertificateApplyResult applyCertificate(CertificateApplyRequest request) { + CertificateApplyResult result = CertificateApplyResult.success("证书申请开始"); + + try { + // 1. 获取服务器信息 + PlatformServer server = serverService.getById(request.getServerId()); + if (server == null) { + return CertificateApplyResult.failed("服务器不存在"); + } + + if (!StringUtils.hasText(server.getPanelApiKey())) { + return CertificateApplyResult.failed("服务器未配置1Panel API密钥"); + } + + // 2. 先保存本地记录 + PlatformCertificate certificate = new PlatformCertificate(); + certificate.setServerId(request.getServerId()); + certificate.setServerName(server.getName()); + certificate.setPrimaryDomain(request.getPrimaryDomain()); + certificate.setOtherDomains(request.getOtherDomains()); + certificate.setProvider("dnsAccount"); + certificate.setAcmeAccountId(request.getAcmeAccountId()); + certificate.setDnsAccountId(request.getDnsAccountId()); + certificate.setKeyType(request.getKeyType()); + certificate.setAutoRenew(request.getAutoRenew()); + certificate.setDescription(request.getDescription()); + certificate.setStatus("pending"); + save(certificate); + result.setCertificateId(certificate.getId()); + result.addSuccessStep("创建本地记录", "记录ID: " + certificate.getId()); + + // 3. 调用1Panel申请证书 + Map applyParams = new HashMap<>(); + applyParams.put("primaryDomain", request.getPrimaryDomain()); + // 尝试将otherDomains转为List,如果包含逗号 + if (StrUtil.isNotBlank(request.getOtherDomains())) { + if (request.getOtherDomains().contains(",")) { + applyParams.put("otherDomains", Arrays.asList(request.getOtherDomains().split(","))); + } else { + applyParams.put("otherDomains", request.getOtherDomains()); + } + } + applyParams.put("provider", "dnsAccount"); + applyParams.put("acmeAccountId", request.getAcmeAccountId()); + applyParams.put("dnsAccountId", request.getDnsAccountId()); + applyParams.put("autoRenew", request.getAutoRenew()); + + // 转换 KeyType + String keyType = request.getKeyType(); + if ("EC 256".equals(keyType)) keyType = "P256"; + else if ("EC 384".equals(keyType)) keyType = "P384"; + else if ("RSA 2048".equals(keyType)) keyType = "2048"; + else if ("RSA 4096".equals(keyType)) keyType = "4096"; + applyParams.put("keyType", keyType); + + applyParams.put("pushDirId", 0); // 默认不推送 + applyParams.put("apply", true); + + log.info("申请证书请求参数: {}", objectMapper.writeValueAsString(applyParams)); + + try { + String resp = sendPost(server, "/websites/ssl", applyParams); + if (resp == null) { + throw new RuntimeException("请求1Panel API返回为空,请检查日志"); + } + log.info("申请证书响应: {}", resp); + + // 检查响应中的 code + JsonNode respNode = objectMapper.readTree(resp); + if (respNode.has("code") && respNode.get("code").asInt() != 200) { + String errorMsg = respNode.has("message") ? respNode.get("message").asText() : "未知错误"; + throw new RuntimeException("1Panel返回错误: " + errorMsg); + } + + result.addSuccessStep("提交证书申请", "已提交到1Panel"); + } catch (Exception e) { + log.error("申请证书失败,尝试使用domains列表参数", e); + // 尝试备用参数结构:domains 列表包含主域名和其他域名 + Map retryParams = new HashMap<>(applyParams); + List domains = new ArrayList<>(); + domains.add(request.getPrimaryDomain()); + if (StrUtil.isNotBlank(request.getOtherDomains())) { + domains.addAll(Arrays.asList(request.getOtherDomains().split(","))); + } + retryParams.put("domains", domains); + retryParams.remove("otherDomains"); + + try { + String resp = sendPost(server, "/websites/ssl", retryParams); + if (resp == null) throw new RuntimeException("重试请求返回为空"); + + log.info("重试申请证书响应: {}", resp); + JsonNode respNode = objectMapper.readTree(resp); + if (respNode.has("code") && respNode.get("code").asInt() != 200) { + String errorMsg = respNode.has("message") ? respNode.get("message").asText() : "未知错误"; + throw new RuntimeException("1Panel返回错误(重试): " + errorMsg); + } + + result.addSuccessStep("提交证书申请", "已提交到1Panel(重试)"); + } catch (Exception ex) { + result.addFailedStep("提交证书申请", ex.getMessage()); + certificate.setStatus("error"); + updateById(certificate); + result.setSuccess(false); + result.setMessage("证书申请失败: " + ex.getMessage()); + return result; + } + } + + // 4. 等待并获取证书ID + Long panelSslId = null; + for (int i = 0; i < 30; i++) { // 最多等待150秒 + Thread.sleep(5000); + + String response = sendPost(server, "/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + if (request.getPrimaryDomain().equals(item.get("primaryDomain").asText())) { + panelSslId = item.get("id").asLong(); + + // 更新本地记录 + certificate.setPanelSslId(panelSslId); + certificate.setStatus("valid"); + certificate.setOrganization(item.has("organization") ? item.get("organization").asText() : "Let's Encrypt"); + + if (item.has("startDate")) { + String startDate = item.get("startDate").asText(); + if (startDate.length() >= 10) { + certificate.setStartDate(LocalDate.parse(startDate.substring(0, 10))); + } + } + if (item.has("expireDate")) { + String expireDate = item.get("expireDate").asText(); + if (expireDate.length() >= 10) { + certificate.setExpireDate(LocalDate.parse(expireDate.substring(0, 10))); + } + } + + certificate.setLastSyncTime(LocalDateTime.now()); + updateById(certificate); + + result.setPanelSslId(panelSslId); + result.addSuccessStep("证书申请完成", "1Panel证书ID: " + panelSslId); + break; + } + } + } + + if (panelSslId != null) break; + } + + if (panelSslId == null) { + result.addFailedStep("获取证书", "等待超时"); + certificate.setStatus("error"); + updateById(certificate); + result.setSuccess(false); + result.setMessage("证书申请超时"); + return result; + } + + result.setSuccess(true); + result.setMessage("证书申请成功"); + return result; + + } catch (Exception e) { + log.error("申请证书失败", e); + result.setSuccess(false); + result.setMessage("申请失败: " + e.getMessage()); + result.addFailedStep("异常", e.getMessage()); + return result; + } + } + + @Override + public int syncCertificatesFromPanel(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null || !StringUtils.hasText(server.getPanelApiKey())) { + return 0; + } + + int syncCount = 0; + + try { + String response = sendPost(server, "/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 1000 + )); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + Long panelSslId = item.get("id").asLong(); + String primaryDomain = item.get("primaryDomain").asText(); + + // 查找本地是否已有该证书 + PlatformCertificate cert = getOne(new LambdaQueryWrapper() + .eq(PlatformCertificate::getServerId, serverId) + .eq(PlatformCertificate::getPanelSslId, panelSslId)); + + if (cert == null) { + cert = new PlatformCertificate(); + cert.setServerId(serverId); + cert.setServerName(server.getName()); + cert.setPanelSslId(panelSslId); + } + + // 更新信息 + cert.setPrimaryDomain(primaryDomain); + cert.setOtherDomains(item.has("domains") ? item.get("domains").asText() : null); + cert.setCn(item.has("type") ? item.get("type").asText() : null); // type字段对应CN + cert.setOrganization(item.has("organization") ? item.get("organization").asText() : "Let's Encrypt"); + cert.setProvider(item.has("provider") ? item.get("provider").asText() : "dnsAccount"); + cert.setAutoRenew(item.has("autoRenew") && item.get("autoRenew").asBoolean()); + cert.setDescription(item.has("description") ? item.get("description").asText() : null); + cert.setKeyType(item.has("keyType") ? item.get("keyType").asText() : null); + cert.setStatus("valid"); + + if (item.has("acmeAccountId")) { + cert.setAcmeAccountId(item.get("acmeAccountId").asLong()); + } + if (item.has("dnsAccountId")) { + cert.setDnsAccountId(item.get("dnsAccountId").asLong()); + } + if (item.has("startDate")) { + String startDate = item.get("startDate").asText(); + if (startDate.length() >= 10) { + cert.setStartDate(LocalDate.parse(startDate.substring(0, 10))); + } + } + if (item.has("expireDate")) { + String expireDate = item.get("expireDate").asText(); + if (expireDate.length() >= 10) { + cert.setExpireDate(LocalDate.parse(expireDate.substring(0, 10))); + + // 检查是否过期 + if (cert.getExpireDate().isBefore(LocalDate.now())) { + cert.setStatus("expired"); + } + } + } + + cert.setLastSyncTime(LocalDateTime.now()); + saveOrUpdate(cert); + syncCount++; + } + } + } catch (Exception e) { + log.error("同步证书失败", e); + } + + return syncCount; + } + + @Override + public PlatformCertificate getCertificateDetail(Long id) { + PlatformCertificate cert = getById(id); + if (cert == null || cert.getPanelSslId() == null) { + return cert; + } + + // 从1Panel获取证书内容 + PlatformServer server = serverService.getById(cert.getServerId()); + if (server == null || !StringUtils.hasText(server.getPanelApiKey())) { + return cert; + } + + try { + String response = sendGet(server, "/websites/ssl/" + cert.getPanelSslId()); + if (response == null) { + log.warn("获取证书详情失败: 服务器未返回数据"); + return cert; + } + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null) { + // 获取证书内容 + if (data.has("pem")) { + cert.setCertContent(data.get("pem").asText()); + } + // 获取私钥内容 + if (data.has("privateKey")) { + cert.setKeyContent(data.get("privateKey").asText()); + } + // 获取CN(证书主体名称) + if (data.has("type") && cert.getCn() == null) { + cert.setCn(data.get("type").asText()); + } + // 获取其他字段 + if (data.has("organization") && cert.getOrganization() == null) { + cert.setOrganization(data.get("organization").asText()); + } + } + } catch (Exception e) { + log.error("获取证书详情失败", e); + } + + return cert; + } + + @Override + public boolean deleteCertificate(Long id) { + PlatformCertificate cert = getById(id); + if (cert == null) { + return false; + } + + // 从1Panel删除 + if (cert.getPanelSslId() != null && cert.getServerId() != null) { + PlatformServer server = serverService.getById(cert.getServerId()); + if (server != null && StringUtils.hasText(server.getPanelApiKey())) { + try { + Map params = new HashMap<>(); + List ids = new ArrayList<>(); + ids.add(cert.getPanelSslId()); + params.put("ids", ids); + + log.info("尝试从1Panel删除证书: server={}, path=/websites/ssl/del, params={}", server.getName(), params); + + String resp = sendPost(server, "/websites/ssl/del", params); + log.info("1Panel删除响应: {}", resp); + + // 如果返回可能是404或空,尝试备用路径 + if (resp == null || resp.contains("404")) { + log.info("尝试备用路径 /websites/ssl/delete"); + sendPost(server, "/websites/ssl/delete", params); + } + } catch (Exception e) { + log.error("从1Panel删除证书失败", e); + } + } + } + + // 删除本地记录 + return removeById(id); + } + + @Override + public boolean updateCertificateSettings(Long id, Boolean autoRenew, String description) { + PlatformCertificate cert = getById(id); + if (cert == null) { + return false; + } + + // 更新1Panel + if (cert.getPanelSslId() != null && cert.getServerId() != null) { + PlatformServer server = serverService.getById(cert.getServerId()); + if (server != null && StringUtils.hasText(server.getPanelApiKey())) { + try { + Map params = new HashMap<>(); + params.put("id", cert.getPanelSslId()); + params.put("autoRenew", autoRenew); + params.put("description", description); + sendPost(server, "/websites/ssl/update", params); + } catch (Exception e) { + log.error("更新1Panel证书设置失败", e); + } + } + } + + // 更新本地 + cert.setAutoRenew(autoRenew); + cert.setDescription(description); + return updateById(cert); + } + + @Override + public List> getAcmeAccounts(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null || !StringUtils.hasText(server.getPanelApiKey())) { + return Collections.emptyList(); + } + + try { + String response = sendPost(server, "/websites/acme/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + if (response == null) { + log.warn("获取Acme账户失败: 服务器{}未返回数据", serverId); + return Collections.emptyList(); + } + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + List> result = new ArrayList<>(); + + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + Map account = new HashMap<>(); + account.put("id", item.get("id").asLong()); + account.put("email", item.get("email").asText()); + account.put("type", item.has("type") ? item.get("type").asText() : "letsencrypt"); + result.add(account); + } + } + return result; + } catch (Exception e) { + log.error("获取Acme账户失败", e); + return Collections.emptyList(); + } + } + + @Override + public List> getDnsAccounts(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null || !StringUtils.hasText(server.getPanelApiKey())) { + return Collections.emptyList(); + } + + try { + String response = sendPost(server, "/websites/dns/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + if (response == null) { + log.warn("获取DNS账户失败: 服务器{}未返回数据", serverId); + return Collections.emptyList(); + } + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + List> result = new ArrayList<>(); + + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + Map account = new HashMap<>(); + account.put("id", item.get("id").asLong()); + account.put("name", item.get("name").asText()); + account.put("type", item.has("type") ? item.get("type").asText() : ""); + result.add(account); + } + } + return result; + } catch (Exception e) { + log.error("获取DNS账户失败", e); + return Collections.emptyList(); + } + } + + @Override + public List> getWebsites(Long serverId) { + PlatformServer server = serverService.getById(serverId); + if (server == null || !StringUtils.hasText(server.getPanelApiKey())) { + return Collections.emptyList(); + } + + try { + String response = sendGet(server, "/websites/list"); + + if (response == null) { + log.warn("获取网站列表失败: 服务器{}未返回数据", serverId); + return Collections.emptyList(); + } + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + List> result = new ArrayList<>(); + + if (data != null && data.isArray()) { + for (JsonNode item : data) { + Map website = new HashMap<>(); + website.put("id", item.get("id").asLong()); + website.put("primaryDomain", item.get("primaryDomain").asText()); + website.put("alias", item.has("alias") ? item.get("alias").asText() : ""); + result.add(website); + } + } + return result; + } catch (Exception e) { + log.error("获取网站列表失败", e); + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/PlatformDomainServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/PlatformDomainServiceImpl.java new file mode 100644 index 0000000..51c6e6c --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/PlatformDomainServiceImpl.java @@ -0,0 +1,1064 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanxiislet.admin.common.base.BasePageQuery; +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.mapper.PlatformDomainMapper; +import com.nanxiislet.admin.service.PlatformDomainService; +import com.nanxiislet.admin.service.PlatformCertificateService; +import com.nanxiislet.admin.entity.PlatformCertificate; +import com.nanxiislet.admin.service.PlatformServerService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.net.InetAddress; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; + +import com.nanxiislet.admin.dto.DomainQueryDTO; + +/** + * 域名管理服务实现 + * + * @author NanxiIslet + * @since 2026-01-13 + */ +@Slf4j +@Service +public class PlatformDomainServiceImpl extends ServiceImpl implements PlatformDomainService { + + @Resource + private PlatformServerService serverService; + + @Resource + private PlatformCertificateService certificateService; + + @Resource + private ObjectMapper objectMapper; + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public Page listPage(DomainQueryDTO query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.like(PlatformDomain::getDomain, query.getKeyword()); + } + + if (query.getServerId() != null) { + wrapper.eq(PlatformDomain::getServerId, query.getServerId()); + } + + if (StrUtil.isNotBlank(query.getStatus())) { + wrapper.eq(PlatformDomain::getStatus, query.getStatus()); + } + + if (StrUtil.isNotBlank(query.getSslStatus())) { + wrapper.eq(PlatformDomain::getSslStatus, query.getSslStatus()); + } + + wrapper.orderByDesc(PlatformDomain::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public DomainDeployResult deployDomain(DomainDeployRequest request) { + DomainDeployResult result = DomainDeployResult.success("部署流程开始"); + + try { + // 1. 获取域名信息 + PlatformDomain domain = getById(request.getDomainId()); + if (domain == null) { + return DomainDeployResult.failed("域名不存在"); + } + + // 2. 获取服务器信息 + if (domain.getServerId() == null) { + return DomainDeployResult.failed("域名未绑定服务器"); + } + + PlatformServer server = serverService.getById(domain.getServerId()); + if (server == null) { + return DomainDeployResult.failed("服务器不存在"); + } + + // 3. 检查服务器1Panel配置 + if (!StringUtils.hasText(server.getPanelApiKey())) { + return DomainDeployResult.failed("服务器未配置1Panel API密钥"); + } + + // 更新域名状态为部署中 + domain.setDeployStatus("deploying"); + domain.setLastDeployTime(LocalDateTime.now()); + updateById(domain); + + // 4. 检查/创建网站 + Long websiteId = checkOrCreateWebsite(server, domain, request, result); + if (websiteId == null && !result.getSuccess()) { + updateDomainDeployStatus(domain, "failed", result.getMessage()); + return result; + } + result.setWebsiteId(websiteId); + domain.setPanelWebsiteId(websiteId); + + // 5. 如果启用HTTPS,检查/申请证书 + if (Boolean.TRUE.equals(request.getEnableHttps())) { + Long sslId = checkOrApplyCertificate(server, domain, request, result); + result.setSslCertificateId(sslId); + domain.setPanelSslId(sslId); + + // 6. 配置HTTPS + if (sslId != null && websiteId != null) { + configureWebsiteHttps(server, websiteId, sslId, result); + domain.setEnableHttps(true); + domain.setSslStatus("valid"); + } + } else { + result.addSkippedStep("配置HTTPS", "未启用HTTPS"); + } + + // 更新域名信息 + domain.setStatus("active"); + domain.setDeployStatus("deployed"); + domain.setLastDeployMessage("部署成功"); + updateById(domain); + + result.setSuccess(true); + result.setMessage("部署流程完成"); + return result; + + } catch (Exception e) { + log.error("部署域名失败", e); + result.setSuccess(false); + result.setMessage("部署失败: " + e.getMessage()); + result.addFailedStep("异常", e.getMessage()); + return result; + } + } + + /** + * 更新域名部署状态 + */ + private void updateDomainDeployStatus(PlatformDomain domain, String status, String message) { + domain.setDeployStatus(status); + domain.setLastDeployMessage(message); + updateById(domain); + } + + /** + * 生成1Panel Token + */ + private String generateToken(String apiKey, long timestamp) { + String rawToken = "1panel" + apiKey + timestamp; + return DigestUtils.md5DigestAsHex(rawToken.getBytes()); + } + + /** + * 创建HTTP请求头 + */ + private HttpHeaders createHeaders(PlatformServer server) { + long timestamp = System.currentTimeMillis() / 1000; + String token = generateToken(server.getPanelApiKey(), timestamp); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("1Panel-Token", token); + headers.set("1Panel-Timestamp", String.valueOf(timestamp)); + return headers; + } + + /** + * 获取1Panel API基础URL + */ + private String getBaseUrl(PlatformServer server) { + if (StringUtils.hasText(server.getPanelUrl())) { + String baseUrl = server.getPanelUrl(); + + // 尝试去除URL中的路径部分(例如安全入口 /super),只保留 协议://IP:端口 + try { + java.net.URI uri = new java.net.URI(baseUrl); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + + if (scheme != null && host != null) { + StringBuilder cleanUrl = new StringBuilder(scheme).append("://").append(host); + if (port != -1) { + cleanUrl.append(":").append(port); + } + return cleanUrl.toString(); + } + } catch (Exception e) { + log.warn("解析Panel URL失败: {}", baseUrl, e); + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + return baseUrl; + } + String ip = server.getIp(); + Integer port = server.getPanelPort() != null ? server.getPanelPort() : 42588; + return "http://" + ip + ":" + port; + } + + /** + * 发送POST请求到1Panel + */ + private String sendPost(PlatformServer server, String path, Object body) { + String url = getBaseUrl(server) + path; + HttpHeaders headers = createHeaders(server); + + try { + String jsonBody = body != null ? objectMapper.writeValueAsString(body) : "{}"; + HttpEntity entity = new HttpEntity<>(jsonBody, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); + return response.getBody(); + } catch (Exception e) { + log.error("1Panel POST请求失败: {}", url, e); + throw new RuntimeException("1Panel API请求失败: " + e.getMessage()); + } + } + + /** + * 检查或创建网站 + */ + private Long checkOrCreateWebsite(PlatformServer server, PlatformDomain domain, + DomainDeployRequest request, DomainDeployResult result) { + try { + // 先查找是否已存在该域名的网站 (使用 v2 接口) + String response = sendPost(server, "/api/v2/websites/search", Map.of( + "page", 1, + "pageSize", 100, + "name", domain.getDomain() + )); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + // v2 接口返回 alias 为域名,primaryDomain可能为空? 检查两者 + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimary = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (domain.getDomain().equals(itemAlias) || domain.getDomain().equals(itemPrimary)) { + Long websiteId = item.get("id").asLong(); + result.addSuccessStep("检查网站", "网站已存在,ID: " + websiteId); + return websiteId; + } + } + } + + // 网站不存在,需要创建 + if (!Boolean.TRUE.equals(request.getCreateIfNotExist())) { + result.setSuccess(false); + result.setMessage("网站不存在,且未设置自动创建"); + result.addFailedStep("检查网站", "网站不存在"); + return null; + } + + // 调用 check 接口 + sendPost(server, "/api/v2/websites/check", new HashMap<>()); + + // 构建创建参数 + Map createParams = new HashMap<>(); + createParams.put("primaryDomain", ""); + createParams.put("alias", domain.getDomain()); + createParams.put("webSiteGroupId", 1); + createParams.put("remark", StringUtils.hasText(domain.getDescription()) ? domain.getDescription() : "域名: " + domain.getDomain()); + createParams.put("otherDomains", ""); + createParams.put("IPV6", false); + createParams.put("enableFtp", false); + createParams.put("ftpUser", ""); + createParams.put("ftpPassword", ""); + + // 数据库默认参数 + createParams.put("createDb", false); + createParams.put("dbType", "mysql"); + createParams.put("dbFormat", "utf8mb4"); + + // 域名列表 + List> domains = new ArrayList<>(); + Map domainItem = new HashMap<>(); + domainItem.put("domain", domain.getDomain()); + domainItem.put("host", domain.getDomain()); + domainItem.put("port", domain.getPort() != null ? domain.getPort() : 80); + domainItem.put("ssl", false); + domains.add(domainItem); + createParams.put("domains", domains); + + // AppInstall 默认参数 + Map appInstall = new HashMap<>(); + appInstall.put("appId", 0); + appInstall.put("name", ""); + appInstall.put("params", new HashMap<>()); + createParams.put("appinstall", appInstall); + + // 根据是否是反向代理设置类型 + if (StringUtils.hasText(domain.getProxyPass())) { + createParams.put("type", "proxy"); + createParams.put("appType", "installed"); + createParams.put("proxy", domain.getProxyPass()); + createParams.put("proxyType", "tcp"); + + String proxyPass = domain.getProxyPass(); + if (proxyPass.contains("://")) { + String[] parts = proxyPass.split("://"); + createParams.put("proxyProtocol", parts[0] + "://"); + createParams.put("proxyAddress", parts[1]); + } else { + createParams.put("proxyProtocol", "http://"); + createParams.put("proxyAddress", proxyPass); + } + + // 运行环境类型,反代时似乎也需要传,参考用户提供的 json + createParams.put("runtimeType", "php"); + } else { + createParams.put("type", "static"); + createParams.put("appType", "installed"); + createParams.put("runtimeType", "php"); // 静态网站默认 + + String sitePath = domain.getSitePath(); + if (!StringUtils.hasText(sitePath)) { + // sitePath = "/opt/1panel/www/sites/" + domain.getDomain() + "/index"; + sitePath = ""; // 留空让面板自动生成 + } + createParams.put("siteDir", sitePath); + } + + // Root port parameter (important for some 1Panel versions/types) + if (domain.getPort() != null) { + createParams.put("port", domain.getPort()); + } else { + createParams.put("port", 80); + } + + // SSL 配置 (如果在创建时就启用) + // 如果 domain 中已经关联了 certificateId, 我们尝试在创建时就绑定 + if (domain.getCertificateId() != null) { + PlatformCertificate cert = certificateService.getById(domain.getCertificateId()); + if (cert != null && cert.getPanelSslId() != null) { + createParams.put("enableSSL", true); + createParams.put("websiteSSLID", cert.getPanelSslId()); + + if (cert.getAcmeAccountId() != null) { + createParams.put("acmeAccountID", cert.getAcmeAccountId()); + } + + result.addSuccessStep("SSL配置", "将绑定证书: " + cert.getPrimaryDomain()); + } else { + createParams.put("enableSSL", false); + } + } else { + createParams.put("enableSSL", false); + } + + String createResponse = sendPost(server, "/api/v2/websites", createParams); + JsonNode createResult = objectMapper.readTree(createResponse); + + if (createResult.has("code") && createResult.get("code").asInt() == 200) { + // 重新查询获取网站ID (增加重试机制,防止数据延迟) + for (int i = 0; i < 3; i++) { + try { + if (i > 0) Thread.sleep(1000); // 等待1秒 + + response = sendPost(server, "/api/v2/websites/search", Map.of( + "page", 1, + "pageSize", 20, + "name", domain.getDomain(), + "orderBy", "created_at", + "order", "descending" + )); + jsonNode = objectMapper.readTree(response); + data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimary = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (domain.getDomain().equals(itemAlias) || domain.getDomain().equals(itemPrimary)) { + Long websiteId = item.get("id").asLong(); + result.addSuccessStep("创建网站", "网站创建成功,ID: " + websiteId); + return websiteId; + } + } + } + } catch (Exception e) { + log.warn("查询新创建网站失败(第{}次): {}", i+1, e.getMessage()); + } + } + // 如果循环结束还没找到,说明虽然创建接口返回成功,但查不到 + log.error("网站创建接口返回成功,但无法查询到该网站: {}", domain.getDomain()); + } + + result.addFailedStep("创建网站", "网站创建失败: " + (createResult.has("message") ? createResult.get("message").asText() : "未知错误")); + result.setSuccess(false); + result.setMessage("网站创建失败"); + return null; + + } catch (Exception e) { + log.error("检查/创建网站失败", e); + result.addFailedStep("检查网站", e.getMessage()); + result.setSuccess(false); + result.setMessage("检查网站失败: " + e.getMessage()); + return null; + } + } + + /** + * 检查或申请证书 + */ + private Long checkOrApplyCertificate(PlatformServer server, PlatformDomain domain, + DomainDeployRequest request, DomainDeployResult result) { + try { + // 先查找是否已存在该域名的证书 + String response = sendPost(server, "/api/v1/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String primaryDomain = item.get("primaryDomain").asText(); + if (domain.getDomain().equals(primaryDomain)) { + Long sslId = item.get("id").asLong(); + // 获取过期时间 + if (item.has("expireDate")) { + String expireDate = item.get("expireDate").asText(); + if (expireDate.length() >= 10) { + domain.setSslExpireDate(LocalDate.parse(expireDate.substring(0, 10))); + } + } + result.addSuccessStep("检查SSL证书", "证书已存在,ID: " + sslId); + return sslId; + } + } + } + + // 证书不存在,需要申请 + if (request.getAcmeAccountId() == null || request.getDnsAccountId() == null) { + result.addSkippedStep("申请SSL证书", "未配置Acme/DNS账户"); + return null; + } + + // 申请证书 + Map applyParams = new HashMap<>(); + applyParams.put("primaryDomain", domain.getDomain()); + applyParams.put("otherDomains", ""); + applyParams.put("provider", "dnsAccount"); + applyParams.put("acmeAccountId", request.getAcmeAccountId()); + applyParams.put("dnsAccountId", request.getDnsAccountId()); + applyParams.put("autoRenew", true); + applyParams.put("keyType", "P256"); + applyParams.put("apply", true); + + sendPost(server, "/api/v1/websites/ssl", applyParams); + + // 等待证书申请完成(最多等待120秒) + for (int i = 0; i < 24; i++) { + Thread.sleep(5000); + + response = sendPost(server, "/api/v1/websites/ssl/search", Map.of( + "page", 1, + "pageSize", 100 + )); + + jsonNode = objectMapper.readTree(response); + data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + String primaryDomain = item.get("primaryDomain").asText(); + if (domain.getDomain().equals(primaryDomain)) { + Long sslId = item.get("id").asLong(); + result.addSuccessStep("申请SSL证书", "证书申请成功,ID: " + sslId); + return sslId; + } + } + } + } + + result.addFailedStep("申请SSL证书", "证书申请超时"); + return null; + + } catch (Exception e) { + log.error("检查/申请证书失败", e); + result.addFailedStep("检查SSL证书", e.getMessage()); + return null; + } + } + + /** + * 配置网站HTTPS + */ + private void configureWebsiteHttps(PlatformServer server, Long websiteId, Long sslId, DomainDeployResult result) { + try { + Map httpsParams = new HashMap<>(); + httpsParams.put("enable", true); + httpsParams.put("type", "select"); + httpsParams.put("websiteSSLId", sslId); + httpsParams.put("httpConfig", "HTTPToHTTPS"); + httpsParams.put("SSLProtocol", new String[]{"TLSv1.2", "TLSv1.3"}); + + sendPost(server, "/api/v1/websites/" + websiteId + "/https", httpsParams); + result.addSuccessStep("配置HTTPS", "HTTPS配置成功"); + + } catch (Exception e) { + log.error("配置HTTPS失败", e); + result.addFailedStep("配置HTTPS", e.getMessage()); + } + } + + @Override + public Map checkDomainDns(Long domainId) { + Map result = new HashMap<>(); + result.put("resolved", false); + + PlatformDomain domain = getById(domainId); + if (domain == null) { + result.put("error", "域名不存在"); + return result; + } + + try { + // 尝试解析域名 + InetAddress[] addresses = InetAddress.getAllByName(domain.getDomain()); + if (addresses != null && addresses.length > 0) { + result.put("resolved", true); + result.put("records", java.util.Arrays.stream(addresses) + .map(InetAddress::getHostAddress) + .toList()); + + // 更新域名DNS状态 + domain.setDnsStatus("resolved"); + updateById(domain); + } else { + domain.setDnsStatus("unresolved"); + updateById(domain); + } + } catch (Exception e) { + log.warn("DNS解析失败: {}", domain.getDomain(), e); + domain.setDnsStatus("unresolved"); + updateById(domain); + result.put("error", e.getMessage()); + } + + return result; + } + + @Override + public PlatformDomain syncDomainFromPanel(Long domainId) { + PlatformDomain domain = getById(domainId); + if (domain == null || domain.getServerId() == null) { + return domain; + } + + PlatformServer server = serverService.getById(domain.getServerId()); + if (server == null || !StringUtils.hasText(server.getPanelApiKey())) { + return domain; + } + + try { + // 查询1Panel中的网站信息 + String response = sendPost(server, "/api/v1/websites/search", Map.of( + "page", 1, + "pageSize", 100, + "name", domain.getDomain() + )); + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + if (data != null && data.has("items")) { + JsonNode items = data.get("items"); + for (JsonNode item : items) { + if (domain.getDomain().equals(item.get("primaryDomain").asText())) { + domain.setPanelWebsiteId(item.get("id").asLong()); + domain.setDeployStatus("deployed"); + domain.setStatus("active"); + + // 检查SSL状态 + if (item.has("sslStatus") && "enable".equals(item.get("sslStatus").asText())) { + domain.setEnableHttps(true); + domain.setSslStatus("valid"); + } + + updateById(domain); + break; + } + } + } + } catch (Exception e) { + log.error("同步域名信息失败", e); + } + + return domain; + } + + @Override + public DomainStatsDTO getDomainStats() { + DomainStatsDTO stats = new DomainStatsDTO(); + + // 总计 + stats.setTotal(count()); + + // 正常 + stats.setActive(count(new LambdaQueryWrapper() + .eq(PlatformDomain::getStatus, "active"))); + + // 待配置 + stats.setPending(count(new LambdaQueryWrapper() + .eq(PlatformDomain::getStatus, "pending"))); + + // SSL即将过期(30天内) + LocalDate expiringDate = LocalDate.now().plusDays(30); + stats.setSslExpiring(count(new LambdaQueryWrapper() + .eq(PlatformDomain::getSslStatus, "expiring") + .or() + .le(PlatformDomain::getSslExpireDate, expiringDate) + .gt(PlatformDomain::getSslExpireDate, LocalDate.now()))); + + // 已部署 + stats.setDeployed(count(new LambdaQueryWrapper() + .eq(PlatformDomain::getDeployStatus, "deployed"))); + + return stats; + } + + @Override + public PlatformDomain getByDomain(String domain) { + return getOne(new LambdaQueryWrapper() + .eq(PlatformDomain::getDomain, domain)); + } + + + + @Override + public int syncDomainsFromCertificates(Long serverId) { + // 获取服务器信息 + PlatformServer server = serverService.getById(serverId); + if (server == null) { + log.warn("服务器不存在: {}", serverId); + return 0; + } + + // 获取该服务器的所有证书 + var certList = certificateService.list( + new LambdaQueryWrapper() + .eq(com.nanxiislet.admin.entity.PlatformCertificate::getServerId, serverId) + ); + + int syncCount = 0; + + for (var cert : certList) { + String primaryDomain = cert.getPrimaryDomain(); + if (StrUtil.isBlank(primaryDomain)) continue; + + // 检查主域名是否已存在(包含已删除) + PlatformDomain existingDomain = baseMapper.findByDomainIncludeDeleted(primaryDomain); + if (existingDomain == null) { + // 创建新域名记录 + PlatformDomain newDomain = new PlatformDomain(); + newDomain.setDomain(primaryDomain); + newDomain.setServerId(serverId); + newDomain.setServerName(server.getName()); + newDomain.setServerIp(server.getIp()); + newDomain.setStatus("active"); + newDomain.setDnsStatus("resolved"); + newDomain.setSslStatus("valid"); + newDomain.setEnableHttps(true); + newDomain.setSslExpireDate(cert.getExpireDate()); + newDomain.setPort(443); + newDomain.setDescription("从证书同步: " + primaryDomain); + save(newDomain); + syncCount++; + log.info("从证书同步域名(新增): {}", primaryDomain); + } else { + // 如果已删除,恢复它 + if (existingDomain.getDeleted() != null && existingDomain.getDeleted() == 1) { + baseMapper.restoreById(existingDomain.getId()); + existingDomain.setDeleted(0); + log.info("从证书同步域名(恢复): {}", primaryDomain); + } + + // 更新关联的服务器信息,确保归属正确 + existingDomain.setServerId(serverId); + existingDomain.setServerName(server.getName()); + existingDomain.setServerIp(server.getIp()); + + // 更新SSL信息 + existingDomain.setSslExpireDate(cert.getExpireDate()); + existingDomain.setSslStatus("valid"); + existingDomain.setEnableHttps(true); // 确保开启HTTPS + + updateById(existingDomain); + syncCount++; + log.info("从证书同步域名(更新): {}", primaryDomain); + } + + // 处理其他域名 + String otherDomains = cert.getOtherDomains(); + if (StrUtil.isNotBlank(otherDomains)) { + for (String otherDomain : otherDomains.split(",")) { + otherDomain = otherDomain.trim(); + if (StrUtil.isBlank(otherDomain)) continue; + + PlatformDomain existingOther = baseMapper.findByDomainIncludeDeleted(otherDomain); + if (existingOther == null) { + PlatformDomain newDomain = new PlatformDomain(); + newDomain.setDomain(otherDomain); + newDomain.setServerId(serverId); + newDomain.setServerName(server.getName()); + newDomain.setServerIp(server.getIp()); + newDomain.setStatus("active"); + newDomain.setDnsStatus("resolved"); + newDomain.setSslStatus("valid"); + newDomain.setEnableHttps(true); + newDomain.setSslExpireDate(cert.getExpireDate()); + newDomain.setPort(443); + newDomain.setDescription("从证书同步: " + otherDomain); + save(newDomain); + syncCount++; + log.info("从证书同步域名(其他-新增): {}", otherDomain); + } else { + // 如果已删除,恢复它 + if (existingOther.getDeleted() != null && existingOther.getDeleted() == 1) { + baseMapper.restoreById(existingOther.getId()); + existingOther.setDeleted(0); + log.info("从证书同步域名(其他-恢复): {}", otherDomain); + } + + existingOther.setServerId(serverId); + existingOther.setSslExpireDate(cert.getExpireDate()); + existingOther.setSslStatus("valid"); + existingOther.setEnableHttps(true); + updateById(existingOther); + syncCount++; + log.info("从证书同步域名(其他-更新): {}", otherDomain); + } + } + } + } + + return syncCount; + } + + @Override + public void undeployDomain(Long domainId) { + PlatformDomain domain = getById(domainId); + if (domain == null) { + throw new RuntimeException("域名不存在"); + } + + if (domain.getServerId() == null) { + throw new RuntimeException("域名未绑定服务器"); + } + + PlatformServer server = serverService.getById(domain.getServerId()); + if (server == null) { + throw new RuntimeException("服务器已删除"); + } + + Long websiteId = domain.getPanelWebsiteId(); + // 如果本地没有ID,尝试去搜索一下,防止确实存在但没关联 + if (websiteId == null) { + try { + String response = sendPost(server, "/api/v2/websites/search", Map.of( + "page", 1, "pageSize", 10, "name", domain.getDomain() + )); + JsonNode jsonNode = objectMapper.readTree(response); + if (jsonNode.has("data") && jsonNode.get("data").has("items")) { + for (JsonNode item : jsonNode.get("data").get("items")) { + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + String itemPrimary = item.has("primaryDomain") ? item.get("primaryDomain").asText() : ""; + if (domain.getDomain().equals(itemAlias) || domain.getDomain().equals(itemPrimary)) { + websiteId = item.get("id").asLong(); + break; + } + } + } + } catch (Exception e) { + log.warn("搜索网站失败", e); + } + } + + if (websiteId != null) { + try { + // 调用删除接口 + Map payload = new HashMap<>(); + payload.put("id", websiteId); + payload.put("deleteApp", false); + payload.put("deleteBackup", true); + payload.put("forceDelete", true); + payload.put("deleteDB", false); + + String res = sendPost(server, "/api/v2/websites/del", payload); + JsonNode resNode = objectMapper.readTree(res); + if (resNode.has("code") && resNode.get("code").asInt() != 200) { + throw new RuntimeException("删除失败: " + (resNode.has("message") ? resNode.get("message").asText() : "未知错误")); + } + } catch (Exception e) { + log.error("在1Panel删除网站失败", e); + throw new RuntimeException("删除1Panel网站失败: " + e.getMessage()); + } + } else { + // log.info("本地和面板均未找到网站ID,仅清除本地状态"); + } + + // 更新本地状态 + domain.setDeployStatus("not_deployed"); + domain.setRuntimeDeployStatus("not_deployed"); + domain.setPanelWebsiteId(null); + domain.setLastDeployMessage("已删除部署"); + // 保留 proxyPass 配置,以便用户重新部署 + + updateById(domain); + } + + @Override + public DomainDeployResult deployRuntime(Long domainId) { + DomainDeployResult result = DomainDeployResult.success("开始部署运行环境"); + + try { + // 1. 获取域名信息 + PlatformDomain domain = getById(domainId); + if (domain == null) { + return DomainDeployResult.failed("域名不存在"); + } + + // 2. 检查运行环境配置 + if (domain.getRuntimeId() == null) { + return DomainDeployResult.failed("未配置运行环境"); + } + if (!StringUtils.hasText(domain.getRuntimeType())) { + return DomainDeployResult.failed("未配置运行环境类型"); + } + + // 3. 获取服务器信息 + if (domain.getServerId() == null) { + return DomainDeployResult.failed("域名未绑定服务器"); + } + + PlatformServer server = serverService.getById(domain.getServerId()); + if (server == null) { + return DomainDeployResult.failed("服务器不存在"); + } + + if (!StringUtils.hasText(server.getPanelApiKey())) { + return DomainDeployResult.failed("服务器未配置1Panel API密钥"); + } + + // 4. 更新域名状态为部署中 + domain.setRuntimeDeployStatus("deploying"); + updateById(domain); + + // 5. 构建创建运行环境网站的请求参数 + Map createParams = new HashMap<>(); + createParams.put("primaryDomain", ""); + createParams.put("type", "runtime"); + createParams.put("alias", domain.getDomain()); + createParams.put("remark", StringUtils.hasText(domain.getDescription()) ? domain.getDescription() : "运行环境: " + domain.getRuntimeName()); + createParams.put("appType", "installed"); + createParams.put("webSiteGroupId", 1); + createParams.put("otherDomains", ""); + createParams.put("proxy", ""); + createParams.put("runtimeID", domain.getRuntimeId().intValue()); + createParams.put("runtimeType", domain.getRuntimeType()); + createParams.put("IPV6", false); + createParams.put("enableFtp", false); + createParams.put("ftpUser", ""); + createParams.put("ftpPassword", ""); + createParams.put("proxyType", "tcp"); + + // 关键修正:对于 runtime 类型,顶层 port 参数是后端应用监听的内部端口 + // 我们从 proxyPass (http://127.0.0.1:8080) 中解析出 8080 + int internalPort = 8080; // 默认回退值 + if (StringUtils.hasText(domain.getProxyPass())) { + try { + String proxyPass = domain.getProxyPass(); + if (proxyPass.contains("://")) { + java.net.URI uri = new java.net.URI(proxyPass); + if (uri.getPort() != -1) { + internalPort = uri.getPort(); + } + } else if (proxyPass.contains(":")) { + // 处理没有协议头的情况 (e.g. 127.0.0.1:8080) + String[] parts = proxyPass.split(":"); + internalPort = Integer.parseInt(parts[parts.length - 1]); + } + } catch (Exception e) { + log.warn("解析内部端口失败: {}, 使用默认端口 {}", domain.getProxyPass(), internalPort); + } + } + createParams.put("port", internalPort); + + createParams.put("proxyProtocol", "http://"); + createParams.put("proxyAddress", ""); + createParams.put("siteDir", ""); + createParams.put("streamPorts", ""); + createParams.put("udp", false); + createParams.put("name", ""); + createParams.put("algorithm", ""); + createParams.put("servers", new ArrayList<>()); + + // AppInstall 参数 + Map appInstall = new HashMap<>(); + appInstall.put("appId", 0); + appInstall.put("name", ""); + appInstall.put("appDetailId", 0); + appInstall.put("params", new HashMap<>()); + appInstall.put("version", ""); + appInstall.put("appkey", ""); + appInstall.put("advanced", false); + appInstall.put("cpuQuota", 0); + appInstall.put("memoryLimit", 0); + appInstall.put("memoryUnit", "MB"); + appInstall.put("containerName", ""); + appInstall.put("allowPort", false); + appInstall.put("format", "utf8mb4"); + appInstall.put("collation", ""); + createParams.put("appinstall", appInstall); + + // 数据库参数 + createParams.put("createDb", false); + createParams.put("dbName", ""); + createParams.put("dbPassword", ""); + createParams.put("dbFormat", "utf8mb4"); + createParams.put("dbUser", ""); + createParams.put("dbType", "mysql"); + createParams.put("dbHost", ""); + + // 域名列表 + List> domains = new ArrayList<>(); + Map domainItem = new HashMap<>(); + domainItem.put("domain", domain.getDomain()); + domainItem.put("host", domain.getDomain()); + domainItem.put("port", domain.getPort() != null ? domain.getPort() : 80); + domainItem.put("ssl", false); + domains.add(domainItem); + createParams.put("domains", domains); + + // 6. 处理 SSL 配置 + if (Boolean.TRUE.equals(domain.getEnableHttps()) && domain.getCertificateId() != null) { + PlatformCertificate cert = certificateService.getById(domain.getCertificateId()); + if (cert != null && cert.getPanelSslId() != null) { + createParams.put("enableSSL", true); + createParams.put("websiteSSLID", cert.getPanelSslId().intValue()); + if (cert.getAcmeAccountId() != null) { + createParams.put("acmeAccountID", cert.getAcmeAccountId().intValue()); + } + result.addSuccessStep("SSL配置", "将绑定证书: " + cert.getPrimaryDomain()); + } else { + createParams.put("enableSSL", false); + createParams.put("websiteSSLID", 0); + createParams.put("acmeAccountID", 0); + } + } else { + createParams.put("enableSSL", false); + createParams.put("websiteSSLID", 0); + createParams.put("acmeAccountID", 0); + } + + // 7. 生成 taskID + createParams.put("taskID", java.util.UUID.randomUUID().toString()); + + // 8. 调用 1Panel API 创建网站 + log.info("部署运行环境请求参数: {}", objectMapper.writeValueAsString(createParams)); + String createResponse = sendPost(server, "/api/v2/websites", createParams); + JsonNode createResult = objectMapper.readTree(createResponse); + + if (createResult.has("code") && createResult.get("code").asInt() == 200) { + result.addSuccessStep("创建运行环境网站", "请求已提交"); + + // 等待并查询网站ID + Thread.sleep(2000); + String searchResponse = sendPost(server, "/api/v2/websites/search", Map.of( + "page", 1, + "pageSize", 20, + "name", domain.getDomain(), + "orderBy", "created_at", + "order", "descending" + )); + JsonNode searchResult = objectMapper.readTree(searchResponse); + JsonNode data = searchResult.get("data"); + if (data != null && data.has("items")) { + for (JsonNode item : data.get("items")) { + String itemAlias = item.has("alias") ? item.get("alias").asText() : ""; + if (domain.getDomain().equals(itemAlias)) { + Long websiteId = item.get("id").asLong(); + domain.setPanelWebsiteId(websiteId); + result.setWebsiteId(websiteId); + result.addSuccessStep("获取网站ID", "网站ID: " + websiteId); + break; + } + } + } + + // 更新域名状态 + domain.setRuntimeDeployStatus("deployed"); + domain.setDeployStatus("deployed"); + domain.setStatus("active"); + domain.setLastDeployTime(java.time.LocalDateTime.now()); + domain.setLastDeployMessage("运行环境部署成功"); + updateById(domain); + + result.setSuccess(true); + result.setMessage("运行环境部署成功"); + } else { + String errorMsg = createResult.has("message") ? createResult.get("message").asText() : "未知错误"; + result.addFailedStep("创建运行环境网站", errorMsg); + result.setSuccess(false); + result.setMessage("部署失败: " + errorMsg); + + domain.setRuntimeDeployStatus("failed"); + domain.setLastDeployMessage("部署失败: " + errorMsg); + updateById(domain); + } + + return result; + + } catch (Exception e) { + log.error("部署运行环境失败", e); + result.setSuccess(false); + result.setMessage("部署失败: " + e.getMessage()); + result.addFailedStep("异常", e.getMessage()); + + // 更新状态为失败 + try { + PlatformDomain domain = getById(domainId); + if (domain != null) { + domain.setRuntimeDeployStatus("failed"); + domain.setLastDeployMessage("部署异常: " + e.getMessage()); + updateById(domain); + } + } catch (Exception ignored) {} + + return result; + } + } + +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/PlatformProjectServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/PlatformProjectServiceImpl.java new file mode 100644 index 0000000..bb26c23 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/PlatformProjectServiceImpl.java @@ -0,0 +1,37 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.entity.PlatformProject; +import com.nanxiislet.admin.mapper.PlatformProjectMapper; +import com.nanxiislet.admin.service.PlatformProjectService; +import org.springframework.stereotype.Service; + +/** + * 项目管理服务实现 + */ +@Service +public class PlatformProjectServiceImpl extends ServiceImpl implements PlatformProjectService { + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(PlatformProject::getName, query.getKeyword()) + .or().like(PlatformProject::getCode, query.getKeyword()) + ); + } + + wrapper.orderByAsc(PlatformProject::getSort) + .orderByDesc(PlatformProject::getCreatedAt); + + return page(page, wrapper); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/PlatformServerServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/PlatformServerServiceImpl.java new file mode 100644 index 0000000..8a901e4 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/PlatformServerServiceImpl.java @@ -0,0 +1,419 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanxiislet.admin.common.base.BasePageQuery; +import com.nanxiislet.admin.dto.ServerInfoDto; +import com.nanxiislet.admin.entity.PlatformServer; +import com.nanxiislet.admin.mapper.PlatformServerMapper; +import com.nanxiislet.admin.service.PlatformServerService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * 服务器管理服务实现 + */ +@Slf4j +@Service +public class PlatformServerServiceImpl extends ServiceImpl implements PlatformServerService { + + @Resource + private ObjectMapper objectMapper; + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + public Page listPage(BasePageQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(PlatformServer::getName, query.getKeyword()) + .or().like(PlatformServer::getIp, query.getKeyword()) + ); + } + + wrapper.orderByDesc(PlatformServer::getCreatedAt); + + return page(page, wrapper); + } + + @Override + public ServerInfoDto getServerStatus(Long id) { + ServerInfoDto dto = new ServerInfoDto(); + PlatformServer server = getById(id); + if (server == null) { + return dto; + } + + // 1. 基础信息转换 + dto = convertToDto(server); + + // 2. 只有配置了 Panel信 息才去获取实时状态 + if (StrUtil.isBlank(server.getPanelUrl()) || StrUtil.isBlank(server.getPanelApiKey())) { + dto.setStatus("offline"); + return dto; + } + + try { + // 根据 Swagger 文档修正:使用 GET /dashboard/current/:ioOption/:netOption + // 而不是 POST + String response = sendGet1Panel(server, "/dashboard/current/all/all"); + + if (response == null) { + // 请求失败 + return dto; + } + + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode data = jsonNode.get("data"); + + if (data != null) { + dto.setStatus("online"); + fillRealtimeData(dto, data); + + // 补充系统信息:务必尝试 /hosts/info 以获取准确的 OS 和内核版本 + // /dashboard/current 可能不包含详细 OS 信息 + try { + String hostInfoRes = sendGet1Panel(server, "/hosts/info"); + if (hostInfoRes != null) { + JsonNode hostData = objectMapper.readTree(hostInfoRes).get("data"); + if (hostData != null) { + if (hostData.has("os")) dto.setOs(hostData.get("os").asText()); + else if (hostData.has("platform")) dto.setOs(hostData.get("platform").asText()); + } + } + } catch (Exception ignore) {} + + // 更新DB状态 + updateServerStatus(server, dto); + } + } catch (Exception e) { + log.error("解析1Panel状态失败: {}", e.getMessage()); + } + + return dto; + } + + private double round2(double value) { + return Math.round(value * 100.0) / 100.0; + } + + private void fillRealtimeData(ServerInfoDto dto, JsonNode data) { + // CPU + if (data.has("cpuUsedPercent")) dto.getCpu().setUsage(round2(data.get("cpuUsedPercent").asDouble())); + else if (data.has("cpu")) dto.getCpu().setUsage(round2(data.get("cpu").asDouble())); + else if (data.has("cpuPercent")) { // 有些版本是 cpuPercent (array) + JsonNode cp = data.get("cpuPercent"); + if (cp.isArray() && cp.size() > 0) dto.getCpu().setUsage(round2(cp.get(0).asDouble())); + } + + if (data.has("cpuTotal")) dto.getCpu().setCores(data.get("cpuTotal").asInt()); + else if (data.has("cpuCores")) dto.getCpu().setCores(data.get("cpuCores").asInt()); + + // Memory + if (data.has("memoryUsed")) { + double usedBytes = data.get("memoryUsed").asDouble(); + dto.getMemory().setUsed(round2(usedBytes / (1024 * 1024 * 1024))); + } + if (data.has("memoryTotal")) { + double totalBytes = data.get("memoryTotal").asDouble(); + dto.getMemory().setTotal(round2(totalBytes / (1024 * 1024 * 1024))); + } + // 计算内存使用率 + if (dto.getMemory().getTotal() > 0.0) { + dto.getMemory().setUsage(round2(dto.getMemory().getUsed() / dto.getMemory().getTotal() * 100)); + } + + // Disk (优先从 diskData 数组中汇总,旧版本可能是 disk) + boolean diskParsed = false; + JsonNode diskNode = data.has("diskData") ? data.get("diskData") : (data.has("disk") ? data.get("disk") : null); + + if (diskNode != null && diskNode.isArray()) { + double totalDisk = 0; + double usedDisk = 0; + for (JsonNode d : diskNode) { + if (d.has("total")) totalDisk += d.get("total").asDouble(); + if (d.has("used")) usedDisk += d.get("used").asDouble(); + } + if (totalDisk > 0) { + dto.getDisk().setTotal(round2(totalDisk / (1024 * 1024 * 1024))); + dto.getDisk().setUsed(round2(usedDisk / (1024 * 1024 * 1024))); + diskParsed = true; + } + } + + // 如果数组解析失败,尝试直接字段 + if (!diskParsed) { + if (data.has("diskUsed")) { + double usedBytes = data.get("diskUsed").asDouble(); + dto.getDisk().setUsed(round2(usedBytes / (1024 * 1024 * 1024))); + } + if (data.has("diskTotal")) { + double totalBytes = data.get("diskTotal").asDouble(); + dto.getDisk().setTotal(round2(totalBytes / (1024 * 1024 * 1024))); + } + } + // 计算磁盘使用率 + if (dto.getDisk().getTotal() > 0.0) { + dto.getDisk().setUsage(round2(dto.getDisk().getUsed() / dto.getDisk().getTotal() * 100)); + } + + // OS + if (data.has("os")) dto.setOs(data.get("os").asText()); + else if (data.has("platform")) dto.setOs(data.get("platform").asText()); + } + + /** + * 将实体转换为DTO + */ + private ServerInfoDto convertToDto(PlatformServer server) { + List tags = new ArrayList<>(); + if (StringUtils.hasText(server.getTags())) { + try { + tags = objectMapper.readValue(server.getTags(), new TypeReference>() {}); + } catch (Exception e) { + log.warn("解析服务器标签失败: {}", server.getTags()); + } + } + + return ServerInfoDto.builder() + .id(server.getId()) + .name(server.getName()) + .ip(server.getIp()) + .internalIp(server.getInternalIp()) + .port(server.getPort()) + .type(server.getType()) + .status(server.getStatus()) + .os(server.getOs()) + .tags(tags) + + .panelUrl(server.getPanelUrl()) + .panelPort(server.getPanelPort()) + .description(server.getDescription()) + .createdAt(server.getCreatedAt()) + .updatedAt(server.getUpdatedAt()) + .cpu(ServerInfoDto.CpuInfo.builder().cores(server.getCpuCores() != null ? server.getCpuCores() : 0).usage(0.0).build()) + .memory(ServerInfoDto.MemoryInfo.builder().total(server.getMemoryTotal() != null ? server.getMemoryTotal().doubleValue() : 0.0).used(0.0).usage(0.0).build()) + .disk(ServerInfoDto.DiskInfo.builder().total(server.getDiskTotal() != null ? server.getDiskTotal().doubleValue() : 0.0).used(0.0).usage(0.0).build()) + .build(); + } + + /** + * 更新数据库中的服务器状态 + */ + private void updateServerStatus(PlatformServer server, ServerInfoDto dto) { + PlatformServer update = new PlatformServer(); + update.setId(server.getId()); + update.setStatus(dto.getStatus()); + update.setOs(dto.getOs()); + update.setCpuCores(dto.getCpu().getCores()); + update.setMemoryTotal(dto.getMemory().getTotal().intValue()); + update.setDiskTotal(dto.getDisk().getTotal().intValue()); + updateById(update); + } + + /** + * 生成1Panel Token + */ + private String generateToken(String apiKey, long timestamp) { + String rawToken = "1panel" + apiKey + timestamp; + return DigestUtils.md5DigestAsHex(rawToken.getBytes()); + } + + private String sendPost1Panel(PlatformServer server, String path, Object body) { + return doRequest1Panel(server, path, HttpMethod.POST, body); + } + + private String sendGet1Panel(PlatformServer server, String path) { + return doRequest1Panel(server, path, HttpMethod.GET, null); + } + + /** + * 使用 IP 和端口构建 Base URL + */ + private String buildBaseUrlFromIpAndPort(PlatformServer server) { + String protocol = "http"; + String ip = server.getIp(); + Integer port = server.getPanelPort() != null ? server.getPanelPort() : 42588; + return protocol + "://" + ip + ":" + port; + } + + private String doRequest1Panel(PlatformServer server, String path, HttpMethod method, Object body) { + String baseUrl = ""; + String apiPath = server.getPanelUrl(); + String pathPrefix = ""; + + if (StrUtil.isNotBlank(apiPath)) { + if (apiPath.endsWith("/")) apiPath = apiPath.substring(0, apiPath.length() - 1); + + // 如果用户输入的 URL 不包含协议前缀,自动添加 https:// + if (!apiPath.startsWith("http://") && !apiPath.startsWith("https://")) { + apiPath = "https://" + apiPath; + } + + baseUrl = apiPath; + // 提取 path prefix (e.g. /super) + try { + java.net.URI uri = new java.net.URI(baseUrl); + pathPrefix = uri.getPath(); + String scheme = uri.getScheme(); + String host = uri.getHost(); + int port = uri.getPort(); + + // 确保 scheme 和 host 有效 + if (scheme == null || host == null) { + log.warn("Panel URL 解析无效,scheme={}, host={}, 原始URL: {}", scheme, host, apiPath); + // 回退到使用 IP 和端口 + baseUrl = buildBaseUrlFromIpAndPort(server); + } else if (port > 0) { + // URL 中明确指定了端口 + baseUrl = scheme + "://" + host + ":" + port; + } else if (server.getPanelPort() != null && server.getPanelPort() > 0) { + // URL 中没有端口,但用户配置了独立的 panelPort + baseUrl = scheme + "://" + host + ":" + server.getPanelPort(); + } else { + // 使用默认端口(不附加端口号,依赖协议默认端口) + baseUrl = scheme + "://" + host; + } + } catch (Exception e) { + log.warn("解析Panel URL失败: {}", apiPath, e); + // 回退到使用 IP 和端口 + baseUrl = buildBaseUrlFromIpAndPort(server); + } + } else { + baseUrl = buildBaseUrlFromIpAndPort(server); + } + + + + // 构造尝试列表: + // 1. 带前缀 (如果配置了): /super/api/v2 + // 2. 带前缀 v1: /super/api/v1 + // 3. 不带前缀 (标准): /api/v2 <-- 新增 + // 4. 不带前缀 v1: /api/v1 <-- 新增 + + List tryPaths = new ArrayList<>(); + if (StrUtil.isNotBlank(pathPrefix)) { + tryPaths.add(pathPrefix + "/api/v2"); + tryPaths.add(pathPrefix + "/api/v1"); + } + tryPaths.add("/api/v2"); + tryPaths.add("/api/v1"); + + // 去重 + List distinctPaths = tryPaths.stream().distinct().toList(); + + for (String versionPath : distinctPaths) { + String fullUrl = baseUrl + versionPath + path; + + long localTimestamp = System.currentTimeMillis() / 1000; + + ResponseEntity responseEntity = executeRequest(fullUrl, method, body, server.getPanelApiKey(), localTimestamp); + String responseBody = responseEntity.getBody(); + + + boolean isHtml = responseBody != null && responseBody.trim().startsWith("<"); + + if (responseEntity.getStatusCode().is2xxSuccessful() && !isHtml) { + return responseBody; + } + + if (isHtml) { + + // 尝试校准时间 + long serverTimestamp = -1; + long responseDate = responseEntity.getHeaders().getDate(); + if (responseDate > 0) serverTimestamp = responseDate / 1000; + else serverTimestamp = localTimestamp - 31536000; + + if (serverTimestamp > 0) { + ResponseEntity retryResponse = executeRequest(fullUrl, method, body, server.getPanelApiKey(), serverTimestamp); + String retryBody = retryResponse.getBody(); + if (retryBody != null && !retryBody.trim().startsWith("<")) { + return retryBody; + } + } + } + } + return null; + } + + private ResponseEntity executeRequest(String url, HttpMethod method, Object body, String apiKey, long timestamp) { + String token = generateToken(apiKey, timestamp); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("1Panel-Token", token); + headers.set("1Panel-Timestamp", String.valueOf(timestamp)); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + + HttpEntity entity = new HttpEntity<>(body, headers); + try { + return restTemplate.exchange(url, method, entity, String.class); + } catch (Exception e) { + if (e instanceof org.springframework.web.client.HttpStatusCodeException) { + org.springframework.web.client.HttpStatusCodeException se = (org.springframework.web.client.HttpStatusCodeException) e; + return new ResponseEntity<>(se.getResponseBodyAsString(), se.getResponseHeaders(), se.getStatusCode()); + } + log.error("Request 1Panel Error: {} {}", url, e.getMessage()); + return new ResponseEntity<>(null, org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Resource + private com.nanxiislet.admin.mapper.PlatformProjectMapper projectMapper; + + @Override + public List getBindingProjects(Long serverId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(com.nanxiislet.admin.entity.PlatformProject::getServerId, serverId); + wrapper.select(com.nanxiislet.admin.entity.PlatformProject::getName); + + List projects = projectMapper.selectList(wrapper); + List projectNames = new ArrayList<>(); + for (com.nanxiislet.admin.entity.PlatformProject project : projects) { + projectNames.add(project.getName()); + } + return projectNames; + } + + @Resource + private com.nanxiislet.admin.mapper.PlatformDomainMapper domainMapper; + + @Resource + private com.nanxiislet.admin.mapper.PlatformCertificateMapper certificateMapper; + + @Override + public void deleteBindingDomains(Long serverId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(com.nanxiislet.admin.entity.PlatformDomain::getServerId, serverId); + domainMapper.delete(wrapper); + log.info("已删除服务器 {} 绑定的所有域名", serverId); + } + + @Override + public void deleteBindingCertificates(Long serverId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(com.nanxiislet.admin.entity.PlatformCertificate::getServerId, serverId); + certificateMapper.delete(wrapper); + log.info("已删除服务器 {} 绑定的所有证书", serverId); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysApprovalInstanceServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysApprovalInstanceServiceImpl.java new file mode 100644 index 0000000..2c394e5 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysApprovalInstanceServiceImpl.java @@ -0,0 +1,259 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.dto.approval.ApprovalInstanceVO; +import com.nanxiislet.admin.entity.SysApprovalInstance; +import com.nanxiislet.admin.entity.SysApprovalNode; +import com.nanxiislet.admin.entity.SysApprovalRecord; +import com.nanxiislet.admin.entity.SysApprovalTemplate; +import com.nanxiislet.admin.entity.SysUser; +import com.nanxiislet.admin.mapper.SysApprovalInstanceMapper; +import com.nanxiislet.admin.mapper.SysApprovalNodeMapper; +import com.nanxiislet.admin.mapper.SysApprovalRecordMapper; +import com.nanxiislet.admin.mapper.SysApprovalTemplateMapper; +import com.nanxiislet.admin.mapper.SysUserMapper; +import com.nanxiislet.admin.service.SysApprovalInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 审批实例服务实现 + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Slf4j +@Service +public class SysApprovalInstanceServiceImpl extends ServiceImpl + implements SysApprovalInstanceService { + + @Resource + private SysApprovalTemplateMapper templateMapper; + + @Resource + private SysApprovalNodeMapper nodeMapper; + + @Resource + private SysApprovalRecordMapper recordMapper; + + @Resource + private SysUserMapper userMapper; + + @Override + public IPage listPage(Integer page, Integer pageSize, String scenario, String status, String keyword) { + Page pageParam = new Page<>(page, pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.hasText(scenario)) { + wrapper.eq(SysApprovalInstance::getScenario, scenario); + } + if (StringUtils.hasText(status)) { + wrapper.eq(SysApprovalInstance::getStatus, status); + } + if (StringUtils.hasText(keyword)) { + wrapper.and(w -> w + .like(SysApprovalInstance::getBusinessTitle, keyword) + .or() + .like(SysApprovalInstance::getInitiatorName, keyword)); + } + wrapper.orderByDesc(SysApprovalInstance::getCreatedAt); + + IPage result = this.page(pageParam, wrapper); + + // 转换为VO + Page voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); + voPage.setRecords(result.getRecords().stream() + .map(this::toVO) + .collect(Collectors.toList())); + + return voPage; + } + + @Override + public ApprovalInstanceVO getDetail(Long id) { + SysApprovalInstance instance = this.getById(id); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + return toVO(instance); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(String businessType, Long businessId, String businessTitle, Long initiatorId) { + // 查找匹配的模板 + SysApprovalTemplate template = templateMapper.selectOne(new LambdaQueryWrapper() + .eq(SysApprovalTemplate::getScenario, businessType) + .eq(SysApprovalTemplate::getEnabled, 1) + .last("LIMIT 1")); + + if (template == null) { + throw new BusinessException("未找到适用的审批流程模板"); + } + + // 获取发起人信息 + SysUser user = userMapper.selectById(initiatorId); + + // 获取第一个节点 + SysApprovalNode firstNode = nodeMapper.selectOne(new LambdaQueryWrapper() + .eq(SysApprovalNode::getTemplateId, template.getId()) + .orderByAsc(SysApprovalNode::getSort) + .last("LIMIT 1")); + + // 创建实例 + SysApprovalInstance instance = new SysApprovalInstance(); + instance.setTemplateId(template.getId()); + instance.setTemplateName(template.getName()); + instance.setScenario(businessType); + instance.setBusinessType(businessType); + instance.setBusinessId(businessId); + instance.setBusinessTitle(businessTitle); + instance.setInitiatorId(initiatorId); + instance.setInitiatorName(user != null ? user.getNickname() : "未知用户"); + instance.setInitiatorAvatar(user != null ? user.getAvatar() : null); + instance.setStatus("pending"); + if (firstNode != null) { + instance.setCurrentNodeId(firstNode.getId()); + instance.setCurrentNodeName(firstNode.getName()); + } + + this.save(instance); + + log.info("创建审批实例: id={}, businessType={}, businessId={}", instance.getId(), businessType, businessId); + return instance.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void submit(Long id) { + SysApprovalInstance instance = this.getById(id); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + if (!"pending".equals(instance.getStatus())) { + throw new BusinessException("当前状态不允许提交"); + } + + instance.setStatus("in_progress"); + instance.setSubmittedAt(LocalDateTime.now()); + this.updateById(instance); + + log.info("提交审批: id={}", id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approve(Long id, Long nodeId, Long approverId, String action, String comment) { + SysApprovalInstance instance = this.getById(id); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + if (!"in_progress".equals(instance.getStatus())) { + throw new BusinessException("当前状态不允许审批"); + } + + // 获取审批人信息 + SysUser approver = userMapper.selectById(approverId); + + // 创建审批记录 + SysApprovalRecord record = new SysApprovalRecord(); + record.setInstanceId(id); + record.setNodeId(nodeId); + record.setNodeName(instance.getCurrentNodeName()); + record.setApproverId(approverId); + record.setApproverName(approver != null ? approver.getNickname() : "未知用户"); + record.setApproverAvatar(approver != null ? approver.getAvatar() : null); + record.setAction(action); + record.setComment(comment); + record.setOperatedAt(LocalDateTime.now()); + recordMapper.insert(record); + + // 根据操作更新实例状态 + if ("reject".equals(action)) { + instance.setStatus("rejected"); + instance.setCompletedAt(LocalDateTime.now()); + } else if ("approve".equals(action)) { + // 查找下一个节点 + SysApprovalNode currentNode = nodeMapper.selectById(nodeId); + SysApprovalNode nextNode = nodeMapper.selectOne(new LambdaQueryWrapper() + .eq(SysApprovalNode::getTemplateId, instance.getTemplateId()) + .gt(SysApprovalNode::getSort, currentNode != null ? currentNode.getSort() : 0) + .orderByAsc(SysApprovalNode::getSort) + .last("LIMIT 1")); + + if (nextNode != null) { + instance.setCurrentNodeId(nextNode.getId()); + instance.setCurrentNodeName(nextNode.getName()); + } else { + // 没有下一个节点,审批完成 + instance.setStatus("approved"); + instance.setCompletedAt(LocalDateTime.now()); + } + } + + this.updateById(instance); + + log.info("审批操作: id={}, action={}", id, action); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void withdraw(Long id) { + SysApprovalInstance instance = this.getById(id); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + if (!"in_progress".equals(instance.getStatus())) { + throw new BusinessException("当前状态不允许撤回"); + } + + instance.setStatus("withdrawn"); + instance.setCompletedAt(LocalDateTime.now()); + this.updateById(instance); + + log.info("撤回审批: id={}", id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancel(Long id) { + SysApprovalInstance instance = this.getById(id); + if (instance == null) { + throw new BusinessException("审批实例不存在"); + } + + instance.setStatus("cancelled"); + instance.setCompletedAt(LocalDateTime.now()); + this.updateById(instance); + + log.info("取消审批: id={}", id); + } + + /** + * 转换为VO + */ + private ApprovalInstanceVO toVO(SysApprovalInstance instance) { + ApprovalInstanceVO vo = new ApprovalInstanceVO(); + BeanUtils.copyProperties(instance, vo); + + // 查询审批记录 + List records = recordMapper.selectList(new LambdaQueryWrapper() + .eq(SysApprovalRecord::getInstanceId, instance.getId()) + .orderByAsc(SysApprovalRecord::getOperatedAt)); + vo.setRecords(records); + + return vo; + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysApprovalTemplateServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysApprovalTemplateServiceImpl.java new file mode 100644 index 0000000..be15b31 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysApprovalTemplateServiceImpl.java @@ -0,0 +1,228 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +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.entity.SysApprovalInstance; +import com.nanxiislet.admin.entity.SysApprovalNode; +import com.nanxiislet.admin.entity.SysApprovalTemplate; +import com.nanxiislet.admin.mapper.SysApprovalInstanceMapper; +import com.nanxiislet.admin.mapper.SysApprovalNodeMapper; +import com.nanxiislet.admin.mapper.SysApprovalTemplateMapper; +import com.nanxiislet.admin.service.SysApprovalTemplateService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 审批流程模板服务实现 + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Slf4j +@Service +public class SysApprovalTemplateServiceImpl extends ServiceImpl + implements SysApprovalTemplateService { + + @Resource + private SysApprovalNodeMapper nodeMapper; + + @Resource + private SysApprovalInstanceMapper instanceMapper; + + @Override + public ApprovalStats getStats() { + ApprovalStats stats = new ApprovalStats(); + + // 统计模板 + stats.setTotalTemplates(this.count()); + stats.setEnabledTemplates(this.count(new LambdaQueryWrapper() + .eq(SysApprovalTemplate::getEnabled, 1))); + + // 统计实例 + stats.setTotalInstances(instanceMapper.selectCount(null)); + stats.setPendingInstances(instanceMapper.selectCount(new LambdaQueryWrapper() + .eq(SysApprovalInstance::getStatus, "pending"))); + stats.setInProgressInstances(instanceMapper.selectCount(new LambdaQueryWrapper() + .eq(SysApprovalInstance::getStatus, "in_progress"))); + stats.setApprovedInstances(instanceMapper.selectCount(new LambdaQueryWrapper() + .eq(SysApprovalInstance::getStatus, "approved"))); + stats.setRejectedInstances(instanceMapper.selectCount(new LambdaQueryWrapper() + .eq(SysApprovalInstance::getStatus, "rejected"))); + + return stats; + } + + @Override + public IPage listPage(Integer page, Integer pageSize, String scenario, Boolean enabled) { + Page pageParam = new Page<>(page, pageSize); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.hasText(scenario)) { + wrapper.eq(SysApprovalTemplate::getScenario, scenario); + } + if (enabled != null) { + wrapper.eq(SysApprovalTemplate::getEnabled, enabled ? 1 : 0); + } + wrapper.orderByDesc(SysApprovalTemplate::getUpdatedAt); + + IPage result = this.page(pageParam, wrapper); + + // 转换为VO并填充节点 + Page voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal()); + voPage.setRecords(result.getRecords().stream() + .map(this::toVO) + .collect(Collectors.toList())); + + return voPage; + } + + @Override + public List listAll(String scenario) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.hasText(scenario)) { + wrapper.eq(SysApprovalTemplate::getScenario, scenario); + } + wrapper.orderByDesc(SysApprovalTemplate::getUpdatedAt); + + return this.list(wrapper).stream() + .map(this::toVO) + .collect(Collectors.toList()); + } + + @Override + public ApprovalTemplateVO getDetail(Long id) { + SysApprovalTemplate template = this.getById(id); + if (template == null) { + throw new BusinessException("模板不存在"); + } + return toVO(template); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(ApprovalTemplateRequest request) { + // 创建模板 + SysApprovalTemplate template = new SysApprovalTemplate(); + template.setName(request.getName()); + template.setDescription(request.getDescription()); + template.setScenario(request.getScenario()); + template.setEnabled(request.getEnabled() != null && request.getEnabled() ? 1 : 0); + this.save(template); + + // 创建节点 + if (request.getNodes() != null && !request.getNodes().isEmpty()) { + int sort = 1; + for (SysApprovalNode node : request.getNodes()) { + node.setId(null); + node.setTemplateId(template.getId()); + node.setSort(sort++); + nodeMapper.insert(node); + } + } + + log.info("创建审批模板: id={}, name={}", template.getId(), template.getName()); + return template.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void update(ApprovalTemplateRequest request) { + if (request.getId() == null) { + throw new BusinessException("模板ID不能为空"); + } + + SysApprovalTemplate template = this.getById(request.getId()); + if (template == null) { + throw new BusinessException("模板不存在"); + } + + // 更新模板 + template.setName(request.getName()); + template.setDescription(request.getDescription()); + template.setScenario(request.getScenario()); + if (request.getEnabled() != null) { + template.setEnabled(request.getEnabled() ? 1 : 0); + } + this.updateById(template); + + // 删除旧节点 + nodeMapper.delete(new LambdaQueryWrapper() + .eq(SysApprovalNode::getTemplateId, template.getId())); + + // 创建新节点 + if (request.getNodes() != null && !request.getNodes().isEmpty()) { + int sort = 1; + for (SysApprovalNode node : request.getNodes()) { + node.setId(null); + node.setTemplateId(template.getId()); + node.setSort(sort++); + nodeMapper.insert(node); + } + } + + log.info("更新审批模板: id={}, name={}", template.getId(), template.getName()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + // 检查是否有使用中的实例 + long instanceCount = instanceMapper.selectCount(new LambdaQueryWrapper() + .eq(SysApprovalInstance::getTemplateId, id) + .in(SysApprovalInstance::getStatus, "pending", "in_progress")); + if (instanceCount > 0) { + throw new BusinessException("存在未完成的审批实例,无法删除"); + } + + // 删除节点 + nodeMapper.delete(new LambdaQueryWrapper() + .eq(SysApprovalNode::getTemplateId, id)); + + // 删除模板 + this.removeById(id); + + log.info("删除审批模板: id={}", id); + } + + @Override + public void toggle(Long id) { + SysApprovalTemplate template = this.getById(id); + if (template == null) { + throw new BusinessException("模板不存在"); + } + + template.setEnabled(template.getEnabled() == 1 ? 0 : 1); + this.updateById(template); + + log.info("切换审批模板状态: id={}, enabled={}", id, template.getEnabled()); + } + + /** + * 转换为VO + */ + private ApprovalTemplateVO toVO(SysApprovalTemplate template) { + ApprovalTemplateVO vo = new ApprovalTemplateVO(); + BeanUtils.copyProperties(template, vo); + + // 查询节点 + List nodes = nodeMapper.selectList(new LambdaQueryWrapper() + .eq(SysApprovalNode::getTemplateId, template.getId()) + .orderByAsc(SysApprovalNode::getSort)); + vo.setNodes(nodes); + + return vo; + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysDeptServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysDeptServiceImpl.java new file mode 100644 index 0000000..f7aaf3f --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysDeptServiceImpl.java @@ -0,0 +1,139 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.entity.SysDept; +import com.nanxiislet.admin.mapper.SysDeptMapper; +import com.nanxiislet.admin.service.SysDeptService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 部门服务实现 + * + * @author NanxiIslet + * @since 2026-01-09 + */ +@Slf4j +@Service +public class SysDeptServiceImpl extends ServiceImpl implements SysDeptService { + + @Override + public List listTree() { + // 获取所有部门 + List allDepts = this.list(new LambdaQueryWrapper() + .eq(SysDept::getStatus, 1) + .orderByAsc(SysDept::getSort) + .orderByAsc(SysDept::getId)); + + // 构建树形结构 + return buildTree(allDepts); + } + + @Override + public List listAll() { + return this.list(new LambdaQueryWrapper() + .eq(SysDept::getStatus, 1) + .orderByAsc(SysDept::getSort) + .orderByAsc(SysDept::getId)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(SysDept dept) { + // 检查父部门是否存在 + if (dept.getParentId() != null && dept.getParentId() > 0) { + SysDept parent = this.getById(dept.getParentId()); + if (parent == null) { + throw new BusinessException("父部门不存在"); + } + } else { + dept.setParentId(0L); + } + + // 检查部门编码是否重复 + if (dept.getCode() != null) { + long count = this.count(new LambdaQueryWrapper() + .eq(SysDept::getCode, dept.getCode())); + if (count > 0) { + throw new BusinessException("部门编码已存在"); + } + } + + this.save(dept); + log.info("创建部门: id={}, name={}", dept.getId(), dept.getName()); + return dept.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDept(SysDept dept) { + SysDept existing = this.getById(dept.getId()); + if (existing == null) { + throw new BusinessException("部门不存在"); + } + + // 检查是否将自己设为父部门 + if (dept.getParentId() != null && dept.getParentId().equals(dept.getId())) { + throw new BusinessException("不能将自己设为父部门"); + } + + // 检查部门编码是否重复 + if (dept.getCode() != null) { + long count = this.count(new LambdaQueryWrapper() + .eq(SysDept::getCode, dept.getCode()) + .ne(SysDept::getId, dept.getId())); + if (count > 0) { + throw new BusinessException("部门编码已存在"); + } + } + + this.updateById(dept); + log.info("更新部门: id={}, name={}", dept.getId(), dept.getName()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDept(Long id) { + // 检查是否有子部门 + long childCount = this.count(new LambdaQueryWrapper() + .eq(SysDept::getParentId, id)); + if (childCount > 0) { + throw new BusinessException("存在子部门,无法删除"); + } + + this.removeById(id); + log.info("删除部门: id={}", id); + } + + @Override + public List getDeptUserIds(Long deptId) { + // TODO: 实现获取部门用户ID列表 + // 这里需要关联用户表查询 + return new ArrayList<>(); + } + + /** + * 构建树形结构 + */ + private List buildTree(List list) { + // 按父ID分组 + Map> parentMap = list.stream() + .collect(Collectors.groupingBy(SysDept::getParentId)); + + // 为每个节点设置子节点 + list.forEach(dept -> dept.setChildren(parentMap.get(dept.getId()))); + + // 返回顶级节点 + return list.stream() + .filter(dept -> dept.getParentId() == null || dept.getParentId() == 0) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysDictItemServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysDictItemServiceImpl.java new file mode 100644 index 0000000..dbfa754 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysDictItemServiceImpl.java @@ -0,0 +1,101 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.entity.SysDict; +import com.nanxiislet.admin.entity.SysDictItem; +import com.nanxiislet.admin.mapper.SysDictItemMapper; +import com.nanxiislet.admin.mapper.SysDictMapper; +import com.nanxiislet.admin.service.SysDictItemService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 字典项 Service 实现 + * + * @author NanxiIslet + * @since 2026-01-08 + */ +@Slf4j +@Service +public class SysDictItemServiceImpl extends ServiceImpl implements SysDictItemService { + + @Resource + private SysDictMapper dictMapper; + + @Override + public List listByDictId(Long dictId) { + // 管理后台查询所有字典项(包括禁用的) + return list(new LambdaQueryWrapper() + .eq(SysDictItem::getDictId, dictId) + .orderByAsc(SysDictItem::getSort)); + } + + @Override + public List listByDictCode(String dictCode) { + // 直接使用 Mapper 查询,避免循环依赖 + SysDict dict = dictMapper.selectOne(new LambdaQueryWrapper() + .eq(SysDict::getCode, dictCode)); + if (dict == null) { + throw new BusinessException("字典不存在: " + dictCode); + } + return listByDictId(dict.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(SysDictItem item) { + // 检查字典是否存在 + SysDict dict = dictMapper.selectById(item.getDictId()); + if (dict == null) { + throw new BusinessException("字典不存在"); + } + // 检查值是否存在 + SysDictItem existing = getOne(new LambdaQueryWrapper() + .eq(SysDictItem::getDictId, item.getDictId()) + .eq(SysDictItem::getValue, item.getValue())); + if (existing != null) { + throw new BusinessException("字典项值已存在"); + } + item.setStatus(1); + if (item.getSort() == null) { + item.setSort(0); + } + if (item.getIsDefault() == null) { + item.setIsDefault(0); + } + save(item); + return item.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateItem(SysDictItem item) { + // 检查值是否存在 + SysDictItem existing = getOne(new LambdaQueryWrapper() + .eq(SysDictItem::getDictId, item.getDictId()) + .eq(SysDictItem::getValue, item.getValue())); + if (existing != null && !existing.getId().equals(item.getId())) { + throw new BusinessException("字典项值已存在"); + } + updateById(item); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteItem(Long id) { + removeById(id); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByDictId(Long dictId) { + remove(new LambdaQueryWrapper() + .eq(SysDictItem::getDictId, dictId)); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysDictServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysDictServiceImpl.java new file mode 100644 index 0000000..317741d --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysDictServiceImpl.java @@ -0,0 +1,82 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.entity.SysDict; +import com.nanxiislet.admin.mapper.SysDictMapper; +import com.nanxiislet.admin.service.SysDictItemService; +import com.nanxiislet.admin.service.SysDictService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +/** + * 字典类型 Service 实现 + * + * @author NanxiIslet + * @since 2026-01-08 + */ +@Slf4j +@Service +public class SysDictServiceImpl extends ServiceImpl implements SysDictService { + + @Resource + private SysDictItemService dictItemService; + + @Override + public IPage page(Integer page, Integer pageSize, String name, String code) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.hasText(name)) { + wrapper.like(SysDict::getName, name); + } + if (StringUtils.hasText(code)) { + wrapper.like(SysDict::getCode, code); + } + wrapper.orderByDesc(SysDict::getCreatedAt); + return page(new Page<>(page, pageSize), wrapper); + } + + @Override + public SysDict getByCode(String code) { + return getOne(new LambdaQueryWrapper() + .eq(SysDict::getCode, code)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(SysDict dict) { + // 检查编码是否存在 + SysDict existing = getByCode(dict.getCode()); + if (existing != null) { + throw new BusinessException("字典编码已存在"); + } + dict.setStatus(1); + save(dict); + return dict.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDict(SysDict dict) { + // 检查编码是否存在 + SysDict existing = getByCode(dict.getCode()); + if (existing != null && !existing.getId().equals(dict.getId())) { + throw new BusinessException("字典编码已存在"); + } + updateById(dict); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDict(Long id) { + // 删除字典项 + dictItemService.deleteByDictId(id); + // 删除字典 + removeById(id); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysMenuServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..eb763a7 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,102 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.entity.SysMenu; +import com.nanxiislet.admin.entity.SysRoleMenu; +import com.nanxiislet.admin.mapper.SysMenuMapper; +import com.nanxiislet.admin.mapper.SysRoleMenuMapper; +import com.nanxiislet.admin.service.SysMenuService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + * 菜单服务实现 + * + * @author NanxiIslet + * @since 2024-01-08 + */ +@Slf4j +@Service +public class SysMenuServiceImpl extends ServiceImpl implements SysMenuService { + + @Resource + private SysRoleMenuMapper roleMenuMapper; + + @Override + public List listTree() { + // 获取所有菜单并按sort排序 + return list(new LambdaQueryWrapper() + .eq(SysMenu::getStatus, 1) + .orderByAsc(SysMenu::getSort)); + } + + @Override + public List listAll() { + return list(new LambdaQueryWrapper() + .orderByAsc(SysMenu::getParentId) + .orderByAsc(SysMenu::getSort)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(SysMenu menu) { + // 检查编码是否存在 + SysMenu existing = getOne(new LambdaQueryWrapper() + .eq(SysMenu::getCode, menu.getCode())); + if (existing != null) { + throw new BusinessException("菜单编码已存在"); + } + menu.setStatus(1); + if (menu.getParentId() == null) { + menu.setParentId(0L); + } + save(menu); + return menu.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateMenu(SysMenu menu) { + // 检查编码是否存在 + SysMenu existing = getOne(new LambdaQueryWrapper() + .eq(SysMenu::getCode, menu.getCode())); + if (existing != null && !existing.getId().equals(menu.getId())) { + throw new BusinessException("菜单编码已存在"); + } + updateById(menu); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteMenu(Long id) { + // 检查是否有子菜单 + long childCount = count(new LambdaQueryWrapper() + .eq(SysMenu::getParentId, id)); + if (childCount > 0) { + throw new BusinessException("该菜单下有子菜单,无法删除"); + } + + // 删除角色菜单关联 + roleMenuMapper.delete(new LambdaQueryWrapper() + .eq(SysRoleMenu::getMenuId, id)); + + // 删除菜单 + removeById(id); + } + + @Override + public List getMenusByRoleCode(String roleCode) { + return baseMapper.selectMenusByRoleCode(roleCode); + } + + @Override + public List getMenusByRoleId(Long roleId) { + return baseMapper.selectMenusByRoleId(roleId); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/SysRoleServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..6744700 --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,122 @@ +package com.nanxiislet.admin.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.entity.SysRole; +import com.nanxiislet.admin.entity.SysRoleMenu; +import com.nanxiislet.admin.mapper.SysRoleMapper; +import com.nanxiislet.admin.mapper.SysRoleMenuMapper; +import com.nanxiislet.admin.service.SysRoleService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 角色服务实现 + * + * @author NanxiIslet + * @since 2024-01-08 + */ +@Slf4j +@Service +public class SysRoleServiceImpl extends ServiceImpl implements SysRoleService { + + @Resource + private SysRoleMenuMapper roleMenuMapper; + + @Override + public IPage listPage(Integer page, Integer pageSize, String keyword) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(keyword)) { + wrapper.like(SysRole::getName, keyword) + .or().like(SysRole::getCode, keyword); + } + wrapper.orderByAsc(SysRole::getSort); + return page(new Page<>(page, pageSize), wrapper); + } + + @Override + public List listAll() { + return list(new LambdaQueryWrapper() + .eq(SysRole::getStatus, 1) + .orderByAsc(SysRole::getSort)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long create(SysRole role) { + // 检查编码是否存在 + if (getByCode(role.getCode()) != null) { + throw new BusinessException("角色编码已存在"); + } + role.setStatus(1); + save(role); + return role.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateRole(SysRole role) { + // 检查编码是否存在 + SysRole existing = getByCode(role.getCode()); + if (existing != null && !existing.getId().equals(role.getId())) { + throw new BusinessException("角色编码已存在"); + } + updateById(role); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteRole(Long id) { + // 删除角色菜单关联 + roleMenuMapper.delete(new LambdaQueryWrapper() + .eq(SysRoleMenu::getRoleId, id)); + // 删除角色 + removeById(id); + } + + @Override + public SysRole getByCode(String code) { + return getOne(new LambdaQueryWrapper() + .eq(SysRole::getCode, code)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void assignMenus(Long roleId, List menuIds) { + // 先删除原有关联 + roleMenuMapper.delete(new LambdaQueryWrapper() + .eq(SysRoleMenu::getRoleId, roleId)); + + // 添加新关联 + if (menuIds != null && !menuIds.isEmpty()) { + List roleMenus = menuIds.stream() + .map(menuId -> { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(roleId); + rm.setMenuId(menuId); + return rm; + }) + .collect(Collectors.toList()); + roleMenus.forEach(roleMenuMapper::insert); + } + } + + @Override + public List getRoleMenuIds(Long roleId) { + List roleMenus = roleMenuMapper.selectList( + new LambdaQueryWrapper() + .eq(SysRoleMenu::getRoleId, roleId)); + return roleMenus.stream() + .map(SysRoleMenu::getMenuId) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/nanxiislet/admin/service/impl/UserServiceImpl.java b/src/main/java/com/nanxiislet/admin/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..b49462b --- /dev/null +++ b/src/main/java/com/nanxiislet/admin/service/impl/UserServiceImpl.java @@ -0,0 +1,145 @@ +package com.nanxiislet.admin.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.nanxiislet.admin.common.exception.BusinessException; +import com.nanxiislet.admin.common.result.ResultCode; +import com.nanxiislet.admin.dto.system.UserQuery; +import com.nanxiislet.admin.entity.SysUser; +import com.nanxiislet.admin.mapper.SysUserMapper; +import com.nanxiislet.admin.service.UserService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +/** + * 用户管理服务实现 + * + * @author NanxiIslet + * @since 2024-01-06 + */ +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @Override + public Page listPage(UserQuery query) { + Page page = new Page<>(query.getPage(), query.getPageSize()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 关键词搜索 + if (StrUtil.isNotBlank(query.getKeyword())) { + wrapper.and(w -> w + .like(SysUser::getUsername, query.getKeyword()) + .or().like(SysUser::getNickname, query.getKeyword()) + .or().like(SysUser::getPhone, query.getKeyword()) + .or().like(SysUser::getEmail, query.getKeyword()) + ); + } + + // 部门筛选 + if (query.getDeptId() != null) { + wrapper.eq(SysUser::getDeptId, query.getDeptId()); + } + + // 角色筛选 + if (StrUtil.isNotBlank(query.getRole())) { + wrapper.eq(SysUser::getRole, query.getRole()); + } + + // 状态筛选 + if (query.getStatus() != null) { + wrapper.eq(SysUser::getStatus, query.getStatus()); + } + + // 默认按创建时间倒序 + wrapper.orderByDesc(SysUser::getCreatedAt); + + Page result = page(page, wrapper); + + // 清除密码字段 + result.getRecords().forEach(user -> user.setPassword(null)); + + return result; + } + + @Override + public void createUser(SysUser user) { + // 检查用户名是否存在 + if (isUsernameExists(user.getUsername(), null)) { + throw new BusinessException(ResultCode.USER_EXISTS); + } + + // 加密密码 + if (StrUtil.isNotBlank(user.getPassword())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } else { + // 默认密码 + user.setPassword(passwordEncoder.encode("123456")); + } + + // 默认状态 + if (user.getStatus() == null) { + user.setStatus(1); + } + + save(user); + } + + @Override + public void updateUser(SysUser user) { + // 检查用户名是否存在 + if (isUsernameExists(user.getUsername(), user.getId())) { + throw new BusinessException(ResultCode.USER_EXISTS); + } + + // 不更新密码 + user.setPassword(null); + + updateById(user); + } + + @Override + public void resetPassword(Long userId, String newPassword) { + SysUser user = getById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_EXIST); + } + + SysUser updateUser = new SysUser(); + updateUser.setId(userId); + updateUser.setPassword(passwordEncoder.encode(newPassword)); + updateById(updateUser); + } + + @Override + public void changePassword(Long userId, String oldPassword, String newPassword) { + SysUser user = getById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_EXIST); + } + + // 验证旧密码 + if (!passwordEncoder.matches(oldPassword, user.getPassword())) { + throw new BusinessException("原密码错误"); + } + + SysUser updateUser = new SysUser(); + updateUser.setId(userId); + updateUser.setPassword(passwordEncoder.encode(newPassword)); + updateById(updateUser); + } + + @Override + public boolean isUsernameExists(String username, Long excludeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysUser::getUsername, username); + if (excludeId != null) { + wrapper.ne(SysUser::getId, excludeId); + } + return count(wrapper) > 0; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..5ad4d37 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,174 @@ +# Spring Boot 配置 +server: + port: 8080 + servlet: + context-path: /api + +spring: + application: + name: nanxiislet-admin + profiles: + active: dev + + # 数据库配置 + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://47.109.57.58:3306/nanxiislet?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: nanxiislet + password: YNTfc4GHRF7A267f + druid: + initial-size: 5 + min-idle: 5 + max-active: 20 + max-wait: 60000 + # 缩短空闲检测间隔,从60秒改为30秒 + time-between-eviction-runs-millis: 30000 + # 缩短最小空闲时间,防止连接在MySQL端超时后仍被池持有 + min-evictable-idle-time-millis: 300000 + max-evictable-idle-time-millis: 1800000 + validation-query: SELECT 1 + test-while-idle: true + # 开启借用连接时的检测,确保获取的连接是有效的(虽然稍微损耗性能,但能彻底解决EOF问题) + test-on-borrow: true + test-on-return: false + # 保持连接活跃 + keep-alive: true + filter: + stat: + enabled: true + slow-sql-millis: 1000 + log-slow-sql: true + wall: + enabled: true + slf4j: + enabled: true + + # Redis配置 + data: + redis: + host: 47.109.57.58 + port: 6379 + password: redis_YAB3QN + database: 0 + timeout: 10s + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + max-wait: -1ms + shutdown-timeout: 100ms + cluster: + refresh: + adaptive: true + period: 60s + + # SQL初始化配置 - 首次运行后请改为 never + sql: + init: + mode: never # 已初始化完成,改为never避免每次启动重置数据 + schema-locations: classpath:schema.sql + continue-on-error: true + + # Jackson配置 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: Asia/Shanghai + serialization: + write-dates-as-timestamps: false + default-property-inclusion: non_null + + # 文件上传配置 - 支持大文件上传 + servlet: + multipart: + enabled: true + max-file-size: 500MB + max-request-size: 500MB + file-size-threshold: 10MB # 超过 10MB 时写入临时文件 + + # 异步请求超时 - 30分钟 + mvc: + async: + request-timeout: 1800000 + +# MyBatis-Plus 配置 +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + type-aliases-package: com.nanxiislet.admin.entity + global-config: + db-config: + id-type: auto + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + banner: false + configuration: + map-underscore-to-camel-case: true + cache-enabled: false + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl + +# Sa-Token 配置 +sa-token: + token-name: Authorization + token-prefix: Bearer + timeout: 86400 + active-timeout: 3600 + is-concurrent: true + is-share: false + token-style: uuid + is-log: true + +# Knife4j 配置 +springdoc: + api-docs: + path: /v3/api-docs + enabled: true + swagger-ui: + path: /swagger-ui.html + enabled: true +knife4j: + enable: true + setting: + language: zh_cn + +# 自定义配置 +nanxiislet: + # 数据库初始化配置 + db: + init: true # 首次启动设为true,初始化完成后改为false + + # JWT配置 + jwt: + secret: NanxiIsletAdminSecretKey20240106ABCDEFGHIJ1234567890 + expiration: 86400000 + refresh-expiration: 604800000 + + # 验证码配置 + captcha: + enabled: true + expire: 300 + + # 文件上传配置 + upload: + path: ./uploads + allowed-types: + - image/jpeg + - image/png + - image/gif + - application/pdf + - application/zip + - application/x-zip-compressed + max-size: 104857600 + +# 日志配置 +logging: + level: + root: info + com.nanxiislet: debug + com.baomidou.mybatisplus: debug + file: + name: logs/nanxiislet-admin.log + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" diff --git a/src/main/resources/db/V2__user_role_menu.sql b/src/main/resources/db/V2__user_role_menu.sql new file mode 100644 index 0000000..b05f9be --- /dev/null +++ b/src/main/resources/db/V2__user_role_menu.sql @@ -0,0 +1,132 @@ +-- ================================== +-- 用户角色管理模块 - 数据库增量脚本 +-- 创建日期: 2026-01-08 +-- ================================== + +USE `nanxiislet`; + +-- ================================== +-- 角色表 +-- ================================== +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `code` VARCHAR(50) NOT NULL COMMENT '角色编码', + `name` VARCHAR(100) NOT NULL COMMENT '角色名称', + `description` VARCHAR(500) DEFAULT NULL COMMENT '角色描述', + `sort` INT DEFAULT 0 COMMENT '排序', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'; + +-- ================================== +-- 菜单表 +-- ================================== +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `parent_id` BIGINT DEFAULT 0 COMMENT '父菜单ID', + `name` VARCHAR(100) NOT NULL COMMENT '菜单名称', + `code` VARCHAR(100) NOT NULL COMMENT '菜单编码/Key', + `type` VARCHAR(20) DEFAULT 'menu' COMMENT '菜单类型 directory-目录 menu-菜单 button-按钮', + `path` VARCHAR(255) DEFAULT NULL COMMENT '路由路径', + `component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径', + `icon` VARCHAR(100) DEFAULT NULL COMMENT '图标', + `permission` VARCHAR(100) DEFAULT NULL COMMENT '权限标识', + `sort` INT DEFAULT 0 COMMENT '排序', + `hidden` TINYINT DEFAULT 0 COMMENT '是否隐藏 0-显示 1-隐藏', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_parent_id` (`parent_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表'; + +-- ================================== +-- 角色菜单关联表 +-- ================================== +DROP TABLE IF EXISTS `sys_role_menu`; +CREATE TABLE `sys_role_menu` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `role_id` BIGINT NOT NULL COMMENT '角色ID', + `menu_id` BIGINT NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`), + KEY `idx_role_id` (`role_id`), + KEY `idx_menu_id` (`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单关联表'; + +-- ================================== +-- 初始化角色数据 +-- ================================== +INSERT INTO `sys_role` (`code`, `name`, `description`, `sort`) VALUES +('super_admin', '超级管理员', '拥有系统所有权限', 1), +('admin', '管理员', '系统管理员,可管理大部分功能', 2), +('finance', '财务', '财务人员,只能访问财务相关功能', 3), +('customer_service', '客服', '客服人员,只能访问客服相关功能', 4); + +-- ================================== +-- 初始化菜单数据 +-- ================================== +-- 财务管理 +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(0, '财务管理', 'finance', 'directory', '/finance', 'PayCircleOutlined', 1); + +SET @finance_id = LAST_INSERT_ID(); + +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(@finance_id, '财务总览', 'finance-overview', 'menu', '/finance/overview', 'DashboardOutlined', 1), +(@finance_id, '收入管理', 'income', 'menu', '/finance/income', 'RiseOutlined', 2), +(@finance_id, '支出管理', 'expense', 'menu', '/finance/expense', 'FallOutlined', 3), +(@finance_id, '报销管理', 'reimbursement', 'menu', '/finance/reimbursement', 'AuditOutlined', 4), +(@finance_id, '结算管理', 'settlement', 'menu', '/finance/settlement', 'TransactionOutlined', 5), +(@finance_id, '发票管理', 'invoice', 'menu', '/finance/invoice', 'FileTextOutlined', 6), +(@finance_id, '账户管理', 'finance-accounts', 'menu', '/finance/accounts', 'BankOutlined', 7), +(@finance_id, '预算管理', 'budget', 'menu', '/finance/budget', 'FundOutlined', 8), +(@finance_id, '财务报表', 'finance-reports', 'menu', '/finance/reports', 'BarChartOutlined', 9); + +-- 平台管理 +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(0, '平台管理', 'platform', 'directory', '/platform', 'AppstoreOutlined', 2); + +SET @platform_id = LAST_INSERT_ID(); + +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(@platform_id, '项目管理', 'platform-projects', 'menu', '/platform/projects', 'ProjectOutlined', 1), +(@platform_id, '服务器管理', 'platform-servers', 'menu', '/platform/servers', 'CloudServerOutlined', 2), +(@platform_id, '域名管理', 'platform-domains', 'menu', '/platform/domains', 'GlobalOutlined', 3), +(@platform_id, '证书管理', 'platform-certificates', 'menu', '/platform/certificates', 'SafetyCertificateOutlined', 4); + +-- 系统管理 +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(0, '系统管理', 'system', 'directory', '/system', 'SettingOutlined', 3); + +SET @system_id = LAST_INSERT_ID(); + +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(@system_id, '用户管理', 'system-users', 'menu', '/system/users', 'UserOutlined', 1), +(@system_id, '角色管理', 'system-roles', 'menu', '/system/roles', 'TeamOutlined', 2), +(@system_id, '菜单管理', 'system-menus', 'menu', '/system/menus', 'MenuOutlined', 3), +(@system_id, '字典管理', 'system-dict', 'menu', '/system/dict', 'BookOutlined', 4), +(@system_id, '审批流程', 'system-approval', 'menu', '/system/approval', 'ApartmentOutlined', 5), +(@system_id, '审批实例', 'system-approval-instances', 'menu', '/system/approval/instances', 'AuditOutlined', 6); + +-- ================================== +-- 为超级管理员分配所有菜单 +-- ================================== +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT 1, id FROM `sys_menu`; + +-- 更新现有用户的角色为admin +UPDATE `sys_user` SET `role` = 'super_admin' WHERE `username` = 'admin'; diff --git a/src/main/resources/db/V3__dict.sql b/src/main/resources/db/V3__dict.sql new file mode 100644 index 0000000..760610a --- /dev/null +++ b/src/main/resources/db/V3__dict.sql @@ -0,0 +1,76 @@ +-- ================================== +-- 字典管理模块 - 数据库增量脚本 +-- 创建日期: 2026-01-08 +-- ================================== + +USE `nanxiislet`; + +-- ================================== +-- 字典类型表 +-- ================================== +DROP TABLE IF EXISTS `sys_dict`; +CREATE TABLE `sys_dict` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '字典名称', + `code` VARCHAR(100) NOT NULL COMMENT '字典编码', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典类型表'; + +-- ================================== +-- 字典项表 +-- ================================== +DROP TABLE IF EXISTS `sys_dict_item`; +CREATE TABLE `sys_dict_item` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `dict_id` BIGINT NOT NULL COMMENT '字典ID', + `label` VARCHAR(100) NOT NULL COMMENT '字典项标签', + `value` VARCHAR(100) NOT NULL COMMENT '字典项值', + `sort` INT DEFAULT 0 COMMENT '排序', + `is_default` TINYINT DEFAULT 0 COMMENT '是否默认 0-否 1-是', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_dict_id` (`dict_id`), + UNIQUE KEY `uk_dict_value` (`dict_id`, `value`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='字典项表'; + +-- ================================== +-- 初始化字典数据 +-- ================================== +INSERT INTO `sys_dict` (`name`, `code`, `remark`) VALUES +('性别', 'gender', '用户性别'), +('状态', 'status', '通用状态'), +('审批状态', 'approval_status', '审批流程状态'); + +-- 性别字典项 +SET @gender_id = (SELECT id FROM sys_dict WHERE code = 'gender'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `is_default`) VALUES +(@gender_id, '男', '1', 1, 1), +(@gender_id, '女', '2', 2, 0), +(@gender_id, '未知', '0', 3, 0); + +-- 状态字典项 +SET @status_id = (SELECT id FROM sys_dict WHERE code = 'status'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `is_default`) VALUES +(@status_id, '正常', '1', 1, 1), +(@status_id, '禁用', '0', 2, 0); + +-- 审批状态字典项 +SET @approval_id = (SELECT id FROM sys_dict WHERE code = 'approval_status'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `is_default`) VALUES +(@approval_id, '待审批', 'pending', 1, 1), +(@approval_id, '已通过', 'approved', 2, 0), +(@approval_id, '已驳回', 'rejected', 3, 0); diff --git a/src/main/resources/db/V4__approval.sql b/src/main/resources/db/V4__approval.sql new file mode 100644 index 0000000..0235348 --- /dev/null +++ b/src/main/resources/db/V4__approval.sql @@ -0,0 +1,163 @@ +-- ================================== +-- 审批流程管理表 +-- 创建日期: 2026-01-09 +-- ================================== + +-- ================================== +-- 部门管理表 +-- ================================== +DROP TABLE IF EXISTS `sys_dept`; +CREATE TABLE `sys_dept` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `parent_id` BIGINT DEFAULT 0 COMMENT '父部门ID,0表示顶级部门', + `name` VARCHAR(100) NOT NULL COMMENT '部门名称', + `code` VARCHAR(50) DEFAULT NULL COMMENT '部门编码', + `leader_id` BIGINT DEFAULT NULL COMMENT '部门负责人ID', + `leader_name` VARCHAR(50) DEFAULT NULL COMMENT '部门负责人名称', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `sort` INT DEFAULT 0 COMMENT '排序', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_code` (`code`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门管理表'; + +-- 初始化部门数据 +INSERT INTO `sys_dept` (`parent_id`, `name`, `code`, `sort`, `status`) VALUES +(0, '南溪屿科技', 'ROOT', 0, 1), +(1, '技术部', 'TECH', 1, 1), +(1, '产品部', 'PRODUCT', 2, 1), +(1, '财务部', 'FINANCE', 3, 1), +(1, '人力资源部', 'HR', 4, 1), +(2, '前端开发组', 'TECH-FE', 1, 1), +(2, '后端开发组', 'TECH-BE', 2, 1), +(2, '运维组', 'TECH-OPS', 3, 1); + +-- ================================== +-- 审批流程模板表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_template`; +CREATE TABLE `sys_approval_template` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '模板名称', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `scenario` VARCHAR(50) NOT NULL COMMENT '适用场景: project_publish/withdrawal/contract/certification/content/expense_reimbursement/payment_request/purchase_request/budget_adjustment/invoice_apply', + `enabled` TINYINT DEFAULT 1 COMMENT '是否启用 0-禁用 1-启用', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_scenario` (`scenario`), + KEY `idx_enabled` (`enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程模板表'; + +-- ================================== +-- 审批流程节点表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_node`; +CREATE TABLE `sys_approval_node` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` BIGINT NOT NULL COMMENT '模板ID', + `name` VARCHAR(100) NOT NULL COMMENT '节点名称', + `approver_type` VARCHAR(20) NOT NULL DEFAULT 'specified' COMMENT '审批人类型: specified-指定人员 role-按角色 department-按部门 superior-上级领导 self_select-发起人自选', + `approver_ids` JSON DEFAULT NULL COMMENT '审批人ID列表(JSON数组)', + `approver_role` VARCHAR(50) DEFAULT NULL COMMENT '审批角色编码', + `dept_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `dept_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `approval_mode` VARCHAR(10) DEFAULT 'or' COMMENT '审批方式: and-会签 or-或签', + `timeout_hours` INT DEFAULT NULL COMMENT '超时时间(小时)', + `timeout_action` VARCHAR(20) DEFAULT NULL COMMENT '超时操作: skip-跳过 reject-驳回', + `sort` INT DEFAULT 0 COMMENT '节点顺序', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`), + KEY `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程节点表'; + +-- ================================== +-- 审批实例表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_instance`; +CREATE TABLE `sys_approval_instance` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` BIGINT NOT NULL COMMENT '模板ID', + `template_name` VARCHAR(100) DEFAULT NULL COMMENT '模板名称', + `scenario` VARCHAR(50) NOT NULL COMMENT '适用场景', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型', + `business_id` BIGINT NOT NULL COMMENT '业务ID', + `business_title` VARCHAR(200) DEFAULT NULL COMMENT '业务标题', + `initiator_id` BIGINT NOT NULL COMMENT '发起人ID', + `initiator_name` VARCHAR(50) DEFAULT NULL COMMENT '发起人名称', + `initiator_avatar` VARCHAR(255) DEFAULT NULL COMMENT '发起人头像', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending-待提交 in_progress-审批中 approved-已通过 rejected-已拒绝 withdrawn-已撤回 cancelled-已取消', + `current_node_id` BIGINT DEFAULT NULL COMMENT '当前节点ID', + `current_node_name` VARCHAR(100) DEFAULT NULL COMMENT '当前节点名称', + `submitted_at` DATETIME DEFAULT NULL COMMENT '提交时间', + `completed_at` DATETIME DEFAULT NULL COMMENT '完成时间', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_initiator_id` (`initiator_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批实例表'; + +-- ================================== +-- 审批记录表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_record`; +CREATE TABLE `sys_approval_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `instance_id` BIGINT NOT NULL COMMENT '实例ID', + `node_id` BIGINT NOT NULL COMMENT '节点ID', + `node_name` VARCHAR(100) DEFAULT NULL COMMENT '节点名称', + `approver_id` BIGINT NOT NULL COMMENT '审批人ID', + `approver_name` VARCHAR(50) DEFAULT NULL COMMENT '审批人名称', + `approver_avatar` VARCHAR(255) DEFAULT NULL COMMENT '审批人头像', + `action` VARCHAR(20) NOT NULL COMMENT '操作: approve-通过 reject-驳回 transfer-转交 return-退回', + `comment` VARCHAR(500) DEFAULT NULL COMMENT '审批意见', + `operated_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + PRIMARY KEY (`id`), + KEY `idx_instance_id` (`instance_id`), + KEY `idx_approver_id` (`approver_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表'; + +-- ================================== +-- 初始化审批流程模板示例数据 +-- ================================== +INSERT INTO `sys_approval_template` (`name`, `description`, `scenario`, `enabled`) VALUES +('费用报销审批', '员工费用报销审批流程', 'expense_reimbursement', 1), +('付款申请审批', '付款申请审批流程', 'payment_request', 1), +('项目发布审批', '项目发布前的审批流程', 'project_publish', 1); + +-- 为费用报销添加审批节点 +SET @expense_template_id = (SELECT id FROM sys_approval_template WHERE scenario = 'expense_reimbursement' LIMIT 1); +INSERT INTO `sys_approval_node` (`template_id`, `name`, `approver_type`, `approver_role`, `approval_mode`, `sort`) VALUES +(@expense_template_id, '部门经理审批', 'role', 'manager', 'or', 1), +(@expense_template_id, '财务审核', 'role', 'finance', 'or', 2), +(@expense_template_id, '总经理审批', 'role', 'admin', 'or', 3); + +-- 为付款申请添加审批节点 +SET @payment_template_id = (SELECT id FROM sys_approval_template WHERE scenario = 'payment_request' LIMIT 1); +INSERT INTO `sys_approval_node` (`template_id`, `name`, `approver_type`, `approver_role`, `approval_mode`, `sort`) VALUES +(@payment_template_id, '财务审核', 'role', 'finance', 'or', 1), +(@payment_template_id, '总经理审批', 'role', 'admin', 'or', 2); + +-- 为项目发布添加审批节点 +SET @project_template_id = (SELECT id FROM sys_approval_template WHERE scenario = 'project_publish' LIMIT 1); +INSERT INTO `sys_approval_node` (`template_id`, `name`, `approver_type`, `approver_role`, `approval_mode`, `sort`) VALUES +(@project_template_id, '项目经理审核', 'role', 'manager', 'or', 1), +(@project_template_id, '技术总监审批', 'role', 'admin', 'or', 2); diff --git a/src/main/resources/db/V5__dept_menu.sql b/src/main/resources/db/V5__dept_menu.sql new file mode 100644 index 0000000..0a7f0e1 --- /dev/null +++ b/src/main/resources/db/V5__dept_menu.sql @@ -0,0 +1,23 @@ +-- ================================== +-- 添加部门管理菜单 +-- 创建日期: 2026-01-09 +-- ================================== + +USE `nanxiislet`; + +-- 获取系统管理菜单ID +SET @system_id = (SELECT id FROM sys_menu WHERE code = 'system'); + +-- 添加部门管理菜单(在角色管理之后) +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(@system_id, '部门管理', 'system-dept', 'menu', '/system/dept', 'ClusterOutlined', 3); + +-- 更新其他菜单排序 +UPDATE `sys_menu` SET `sort` = 4 WHERE `code` = 'system-menus'; +UPDATE `sys_menu` SET `sort` = 5 WHERE `code` = 'system-dict'; +UPDATE `sys_menu` SET `sort` = 6 WHERE `code` = 'system-approval'; +UPDATE `sys_menu` SET `sort` = 7 WHERE `code` = 'system-approval-instances'; + +-- 为超级管理员分配新菜单 +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT 1, id FROM `sys_menu` WHERE `code` = 'system-dept'; diff --git a/src/main/resources/db/V6__domain_enhance.sql b/src/main/resources/db/V6__domain_enhance.sql new file mode 100644 index 0000000..9549092 --- /dev/null +++ b/src/main/resources/db/V6__domain_enhance.sql @@ -0,0 +1,20 @@ +-- ================================== +-- 域名表增强 - 增加 1Panel 相关字段 +-- 创建日期: 2026-01-13 +-- ================================== + +USE `nanxiislet`; + +-- 添加 1Panel 相关字段 +ALTER TABLE `platform_domain` +ADD COLUMN `panel_website_id` BIGINT DEFAULT NULL COMMENT '1Panel网站ID' AFTER `force_https`, +ADD COLUMN `panel_ssl_id` BIGINT DEFAULT NULL COMMENT '1Panel证书ID' AFTER `panel_website_id`, +ADD COLUMN `site_path` VARCHAR(255) DEFAULT NULL COMMENT '网站目录路径' AFTER `panel_ssl_id`, +ADD COLUMN `alias` VARCHAR(100) DEFAULT NULL COMMENT '网站别名' AFTER `site_path`, +ADD COLUMN `deploy_status` VARCHAR(20) DEFAULT 'not_deployed' COMMENT '部署状态 not_deployed/deploying/deployed/failed' AFTER `alias`, +ADD COLUMN `last_deploy_time` DATETIME DEFAULT NULL COMMENT '最后部署时间' AFTER `deploy_status`, +ADD COLUMN `last_deploy_message` VARCHAR(500) DEFAULT NULL COMMENT '最后部署消息' AFTER `last_deploy_time`; + +-- 添加索引 +ALTER TABLE `platform_domain` ADD INDEX `idx_deploy_status` (`deploy_status`); +ALTER TABLE `platform_domain` ADD INDEX `idx_panel_website_id` (`panel_website_id`); diff --git a/src/main/resources/db/V7__certificate.sql b/src/main/resources/db/V7__certificate.sql new file mode 100644 index 0000000..8718ec2 --- /dev/null +++ b/src/main/resources/db/V7__certificate.sql @@ -0,0 +1,47 @@ +-- ============================================ +-- 证书管理表 +-- 创建日期: 2026-01-13 +-- ============================================ + +USE nanxiislet; + +-- 平台证书表 +DROP TABLE IF EXISTS `platform_certificate`; +CREATE TABLE `platform_certificate` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `server_id` BIGINT NOT NULL COMMENT '关联服务器ID', + `server_name` VARCHAR(100) DEFAULT NULL COMMENT '服务器名称', + `panel_ssl_id` BIGINT DEFAULT NULL COMMENT '1Panel证书ID', + `primary_domain` VARCHAR(200) NOT NULL COMMENT '主域名', + `other_domains` VARCHAR(500) DEFAULT NULL COMMENT '其他域名(逗号分隔)', + `cn` VARCHAR(100) DEFAULT NULL COMMENT '证书主体名称', + `organization` VARCHAR(200) DEFAULT NULL COMMENT '颁发组织', + `provider` VARCHAR(50) DEFAULT 'dnsAccount' COMMENT '验证方式 dnsAccount/httpManual', + `acme_account_id` BIGINT DEFAULT NULL COMMENT 'Acme账户ID', + `acme_account_email` VARCHAR(200) DEFAULT NULL COMMENT 'Acme账户邮箱', + `dns_account_id` BIGINT DEFAULT NULL COMMENT 'DNS账户ID', + `dns_account_name` VARCHAR(100) DEFAULT NULL COMMENT 'DNS账户名称', + `dns_account_type` VARCHAR(50) DEFAULT NULL COMMENT 'DNS账户类型', + `key_type` VARCHAR(50) DEFAULT 'P256' COMMENT '密钥算法 P256/P384/RSA2048/RSA4096', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/valid/expired/error', + `auto_renew` TINYINT(1) DEFAULT 1 COMMENT '自动续签', + `start_date` DATE DEFAULT NULL COMMENT '生效时间', + `expire_date` DATE DEFAULT NULL COMMENT '过期时间', + `cert_content` TEXT DEFAULT NULL COMMENT '证书内容', + `key_content` TEXT DEFAULT NULL COMMENT '私钥内容', + `description` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `last_sync_time` DATETIME DEFAULT NULL COMMENT '最后同步时间', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_server_id` (`server_id`), + KEY `idx_panel_ssl_id` (`panel_ssl_id`), + KEY `idx_primary_domain` (`primary_domain`), + KEY `idx_status` (`status`), + KEY `idx_expire_date` (`expire_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台证书表'; + +SELECT '证书表创建完成!' AS Result; diff --git a/src/main/resources/db/V8__add_runtime_to_domain.sql b/src/main/resources/db/V8__add_runtime_to_domain.sql new file mode 100644 index 0000000..ef7c7ff --- /dev/null +++ b/src/main/resources/db/V8__add_runtime_to_domain.sql @@ -0,0 +1,12 @@ +-- ================================== +-- 域名表增强 - 增加运行环境相关字段 +-- 创建日期: 2026-01-19 +-- ================================== + +USE `nanxiislet`; + +-- 添加运行环境相关字段 +ALTER TABLE `platform_domain` +ADD COLUMN `runtime_id` BIGINT DEFAULT NULL COMMENT '关联运行环境ID', +ADD COLUMN `runtime_server_id` BIGINT DEFAULT NULL COMMENT '运行环境所属服务器ID', +ADD COLUMN `runtime_name` VARCHAR(100) DEFAULT NULL COMMENT '运行环境名称'; diff --git a/src/main/resources/db/fix_approval_tables.sql b/src/main/resources/db/fix_approval_tables.sql new file mode 100644 index 0000000..c8812e6 --- /dev/null +++ b/src/main/resources/db/fix_approval_tables.sql @@ -0,0 +1,179 @@ +-- ================================== +-- 紧急修复:创建部门和审批相关表 +-- 在数据库中执行此脚本 +-- ================================== + +USE `nanxiislet`; + +-- ================================== +-- 部门管理表 +-- ================================== +CREATE TABLE IF NOT EXISTS `sys_dept` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `parent_id` BIGINT DEFAULT 0 COMMENT '父部门ID,0表示顶级部门', + `name` VARCHAR(100) NOT NULL COMMENT '部门名称', + `code` VARCHAR(50) DEFAULT NULL COMMENT '部门编码', + `leader_id` BIGINT DEFAULT NULL COMMENT '部门负责人ID', + `leader_name` VARCHAR(50) DEFAULT NULL COMMENT '部门负责人名称', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `sort` INT DEFAULT 0 COMMENT '排序', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_code` (`code`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门管理表'; + +-- 初始化部门数据(忽略重复) +INSERT IGNORE INTO `sys_dept` (`id`, `parent_id`, `name`, `code`, `sort`, `status`) VALUES +(1, 0, '南溪屿科技', 'ROOT', 0, 1), +(2, 1, '技术部', 'TECH', 1, 1), +(3, 1, '产品部', 'PRODUCT', 2, 1), +(4, 1, '财务部', 'FINANCE', 3, 1), +(5, 1, '人力资源部', 'HR', 4, 1), +(6, 2, '前端开发组', 'TECH-FE', 1, 1), +(7, 2, '后端开发组', 'TECH-BE', 2, 1), +(8, 2, '运维组', 'TECH-OPS', 3, 1); + +-- ================================== +-- 审批流程模板表 +-- ================================== +CREATE TABLE IF NOT EXISTS `sys_approval_template` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '模板名称', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `scenario` VARCHAR(50) NOT NULL COMMENT '适用场景', + `enabled` TINYINT DEFAULT 1 COMMENT '是否启用 0-禁用 1-启用', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_scenario` (`scenario`), + KEY `idx_enabled` (`enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程模板表'; + +-- ================================== +-- 审批流程节点表 +-- ================================== +CREATE TABLE IF NOT EXISTS `sys_approval_node` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` BIGINT NOT NULL COMMENT '模板ID', + `name` VARCHAR(100) NOT NULL COMMENT '节点名称', + `approver_type` VARCHAR(20) NOT NULL DEFAULT 'specified' COMMENT '审批人类型', + `approver_ids` JSON DEFAULT NULL COMMENT '审批人ID列表', + `approver_role` VARCHAR(50) DEFAULT NULL COMMENT '审批角色编码', + `dept_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `dept_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `approval_mode` VARCHAR(10) DEFAULT 'or' COMMENT '审批方式: and-会签 or-或签', + `timeout_hours` INT DEFAULT NULL COMMENT '超时时间(小时)', + `timeout_action` VARCHAR(20) DEFAULT NULL COMMENT '超时操作', + `sort` INT DEFAULT 0 COMMENT '节点顺序', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程节点表'; + +-- ================================== +-- 审批实例表 +-- ================================== +CREATE TABLE IF NOT EXISTS `sys_approval_instance` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` BIGINT NOT NULL COMMENT '模板ID', + `template_name` VARCHAR(100) DEFAULT NULL COMMENT '模板名称', + `scenario` VARCHAR(50) NOT NULL COMMENT '适用场景', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型', + `business_id` BIGINT NOT NULL COMMENT '业务ID', + `business_title` VARCHAR(200) DEFAULT NULL COMMENT '业务标题', + `initiator_id` BIGINT NOT NULL COMMENT '发起人ID', + `initiator_name` VARCHAR(50) DEFAULT NULL COMMENT '发起人名称', + `initiator_avatar` VARCHAR(255) DEFAULT NULL COMMENT '发起人头像', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态', + `current_node_id` BIGINT DEFAULT NULL COMMENT '当前节点ID', + `current_node_name` VARCHAR(100) DEFAULT NULL COMMENT '当前节点名称', + `submitted_at` DATETIME DEFAULT NULL COMMENT '提交时间', + `completed_at` DATETIME DEFAULT NULL COMMENT '完成时间', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批实例表'; + +-- ================================== +-- 审批记录表 +-- ================================== +CREATE TABLE IF NOT EXISTS `sys_approval_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `instance_id` BIGINT NOT NULL COMMENT '实例ID', + `node_id` BIGINT NOT NULL COMMENT '节点ID', + `node_name` VARCHAR(100) DEFAULT NULL COMMENT '节点名称', + `approver_id` BIGINT NOT NULL COMMENT '审批人ID', + `approver_name` VARCHAR(50) DEFAULT NULL COMMENT '审批人名称', + `approver_avatar` VARCHAR(255) DEFAULT NULL COMMENT '审批人头像', + `action` VARCHAR(20) NOT NULL COMMENT '操作', + `comment` VARCHAR(500) DEFAULT NULL COMMENT '审批意见', + `operated_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + PRIMARY KEY (`id`), + KEY `idx_instance_id` (`instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表'; + +-- 初始化审批流程模板示例数据(忽略重复) +INSERT IGNORE INTO `sys_approval_template` (`name`, `description`, `scenario`, `enabled`) VALUES +('费用报销审批', '员工费用报销审批流程', 'expense_reimbursement', 1), +('付款申请审批', '付款申请审批流程', 'payment_request', 1), +('项目发布审批', '项目发布前的审批流程', 'project_publish', 1); + +-- 添加部门管理菜单(如果不存在) +SET @system_id = (SELECT id FROM sys_menu WHERE code = 'system' LIMIT 1); +INSERT IGNORE INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `icon`, `sort`) VALUES +(@system_id, '部门管理', 'system-dept', 'menu', '/system/dept', 'ClusterOutlined', 3); + +-- 为超级管理员分配菜单 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT 1, id FROM `sys_menu` WHERE `code` = 'system-dept'; + +-- 为用户表添加部门字段(使用存储过程检查列是否存在) +SET @dbname = DATABASE(); +SET @tablename = 'sys_user'; + +-- 检查并添加 dept_id 列 +SET @columnname = 'dept_id'; +SET @preparedStatement = (SELECT IF( + EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname + ), + 'SELECT 1', + CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` BIGINT DEFAULT NULL COMMENT ''部门ID'' AFTER `role`') +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- 检查并添加 dept_name 列 +SET @columnname = 'dept_name'; +SET @preparedStatement = (SELECT IF( + EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname + ), + 'SELECT 1', + CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(100) DEFAULT NULL COMMENT ''部门名称'' AFTER `dept_id`') +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +SELECT 'SQL执行完成!请重启后端服务。' AS message; diff --git a/src/main/resources/db/fix_project_table.sql b/src/main/resources/db/fix_project_table.sql new file mode 100644 index 0000000..346020b --- /dev/null +++ b/src/main/resources/db/fix_project_table.sql @@ -0,0 +1,22 @@ +-- ============================================ +-- 修复 platform_project 表结构 +-- 添加缺失的列(忽略已存在的列错误) +-- ============================================ + +USE nanxiislet; + +-- 添加缺失的字段(如列已存在会报错,可忽略) +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS short_name VARCHAR(50) COMMENT '项目简称'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS project_group VARCHAR(50) DEFAULT 'default' COMMENT '项目分组'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS logo VARCHAR(50) COMMENT 'Logo文字'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS color VARCHAR(50) COMMENT '主题色'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS domain VARCHAR(200) COMMENT '绑定域名'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS remark VARCHAR(500) COMMENT '备注'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS enable_https TINYINT(1) DEFAULT 0 COMMENT '是否启用HTTPS'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS panel_website_id BIGINT COMMENT '1Panel网站ID'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS panel_ssl_id BIGINT COMMENT '1Panel证书ID'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS last_deploy_time DATETIME COMMENT '最后部署时间'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS last_deploy_status VARCHAR(50) COMMENT '最后部署状态'; +ALTER TABLE platform_project ADD COLUMN IF NOT EXISTS last_deploy_message VARCHAR(500) COMMENT '最后部署消息'; + +SELECT 'platform_project 表修复完成' AS Result; diff --git a/src/main/resources/db/init.sql b/src/main/resources/db/init.sql new file mode 100644 index 0000000..da4076b --- /dev/null +++ b/src/main/resources/db/init.sql @@ -0,0 +1,418 @@ +-- ================================== +-- Nanxiislet Admin 数据库初始化脚本 +-- 创建日期: 2026-01-06 +-- ================================== + +-- 创建数据库(如果不存在) +CREATE DATABASE IF NOT EXISTS `nanxiislet` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE `nanxiislet`; + +-- ================================== +-- 系统用户表 +-- ================================== +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码', + `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', + `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', + `role` VARCHAR(50) DEFAULT 'user' COMMENT '角色编码', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间', + `last_login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_phone` (`phone`), + KEY `idx_email` (`email`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户表'; + +-- ================================== +-- 平台服务器表 +-- ================================== +DROP TABLE IF EXISTS `platform_server`; +CREATE TABLE `platform_server` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '服务器名称', + `ip` VARCHAR(50) NOT NULL COMMENT '公网IP', + `internal_ip` VARCHAR(50) DEFAULT NULL COMMENT '内网IP', + `port` INT DEFAULT 22 COMMENT 'SSH端口', + `type` VARCHAR(20) DEFAULT 'cloud' COMMENT '服务器类型 physical/virtual/cloud', + `status` VARCHAR(20) DEFAULT 'offline' COMMENT '状态 online/offline/warning/maintenance', + `os` VARCHAR(100) DEFAULT NULL COMMENT '操作系统', + `cpu_cores` INT DEFAULT NULL COMMENT 'CPU核心数', + `memory_total` INT DEFAULT NULL COMMENT '内存大小(GB)', + `disk_total` INT DEFAULT NULL COMMENT '磁盘大小(GB)', + `tags` JSON DEFAULT NULL COMMENT '标签', + `ssh_user` VARCHAR(50) DEFAULT 'root' COMMENT 'SSH用户名', + `ssh_port` INT DEFAULT 22 COMMENT 'SSH端口', + `panel_url` VARCHAR(255) DEFAULT NULL COMMENT '1Panel面板地址', + `panel_port` INT DEFAULT NULL COMMENT '1Panel面板端口', + `panel_api_key` VARCHAR(255) DEFAULT NULL COMMENT '1Panel API密钥', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_ip` (`ip`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台服务器表'; + +-- ================================== +-- 平台项目表 +-- ================================== +DROP TABLE IF EXISTS `platform_project`; +CREATE TABLE `platform_project` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '项目名称', + `code` VARCHAR(50) DEFAULT NULL COMMENT '项目编码', + `type` VARCHAR(50) DEFAULT NULL COMMENT '项目类型', + `url` VARCHAR(255) DEFAULT NULL COMMENT '访问地址', + `domain_id` BIGINT DEFAULT NULL COMMENT '关联域名ID', + `server_id` BIGINT DEFAULT NULL COMMENT '关联服务器ID', + `server_name` VARCHAR(100) DEFAULT NULL COMMENT '服务器名称', + `deploy_path` VARCHAR(255) DEFAULT NULL COMMENT '部署路径', + `version` VARCHAR(50) DEFAULT NULL COMMENT '当前版本', + `status` VARCHAR(20) DEFAULT 'inactive' COMMENT '状态 active/inactive/deploying', + `icon` VARCHAR(255) DEFAULT NULL COMMENT '图标', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `sort` INT DEFAULT 0 COMMENT '排序', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_code` (`code`), + KEY `idx_server_id` (`server_id`), + KEY `idx_domain_id` (`domain_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台项目表'; + +-- ================================== +-- 平台域名表 +-- ================================== +DROP TABLE IF EXISTS `platform_domain`; +CREATE TABLE `platform_domain` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `domain` VARCHAR(255) NOT NULL COMMENT '域名', + `project_id` BIGINT DEFAULT NULL COMMENT '关联项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '关联项目名称', + `server_id` BIGINT DEFAULT NULL COMMENT '关联服务器ID', + `server_name` VARCHAR(100) DEFAULT NULL COMMENT '关联服务器名称', + `server_ip` VARCHAR(50) DEFAULT NULL COMMENT '服务器IP', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 active/pending/expired/error', + `dns_status` VARCHAR(20) DEFAULT 'checking' COMMENT 'DNS状态 resolved/unresolved/checking', + `dns_records` JSON DEFAULT NULL COMMENT 'DNS记录', + `ssl_status` VARCHAR(20) DEFAULT 'none' COMMENT 'SSL状态 valid/expiring/expired/none', + `ssl_expire_date` DATE DEFAULT NULL COMMENT 'SSL过期时间', + `certificate_id` VARCHAR(100) DEFAULT NULL COMMENT '证书ID', + `certificate_name` VARCHAR(100) DEFAULT NULL COMMENT '证书名称', + `nginx_config_path` VARCHAR(255) DEFAULT NULL COMMENT 'Nginx配置路径', + `proxy_pass` VARCHAR(255) DEFAULT NULL COMMENT '代理地址', + `port` INT DEFAULT 80 COMMENT '端口', + `enable_https` TINYINT DEFAULT 0 COMMENT '是否启用HTTPS', + `force_https` TINYINT DEFAULT 0 COMMENT '是否强制HTTPS', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_domain` (`domain`), + KEY `idx_project_id` (`project_id`), + KEY `idx_server_id` (`server_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台域名表'; + +-- ================================== +-- 财务账户表 +-- ================================== +DROP TABLE IF EXISTS `finance_account`; +CREATE TABLE `finance_account` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '账户名称', + `type` VARCHAR(20) DEFAULT 'corporate' COMMENT '账户类型 corporate-对公账户 merchant-商户号', + `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '开户银行', + `bank_branch` VARCHAR(100) DEFAULT NULL COMMENT '开户支行', + `account_no` VARCHAR(50) DEFAULT NULL COMMENT '银行账号', + `merchant_id` VARCHAR(100) DEFAULT NULL COMMENT '商户ID', + `merchant_platform` VARCHAR(20) DEFAULT NULL COMMENT '商户平台 wechat/alipay/unionpay/other', + `app_id` VARCHAR(100) DEFAULT NULL COMMENT 'AppID', + `balance` DECIMAL(15,2) DEFAULT 0.00 COMMENT '当前余额', + `status` VARCHAR(20) DEFAULT 'active' COMMENT '状态 active-正常 inactive-禁用', + `is_default` TINYINT DEFAULT 0 COMMENT '是否默认账户', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_type` (`type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务账户表'; + +-- ================================== +-- 财务收入表 +-- ================================== +DROP TABLE IF EXISTS `finance_income`; +CREATE TABLE `finance_income` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `income_no` VARCHAR(50) NOT NULL COMMENT '收入编号', + `type` VARCHAR(30) DEFAULT 'project' COMMENT '收入类型 project/service_fee/consulting/commission/other', + `title` VARCHAR(200) NOT NULL COMMENT '收入名称', + `customer_id` BIGINT DEFAULT NULL COMMENT '客户ID', + `customer_name` VARCHAR(100) DEFAULT NULL COMMENT '客户名称', + `customer_contact` VARCHAR(100) DEFAULT NULL COMMENT '客户联系方式', + `project_id` BIGINT DEFAULT NULL COMMENT '关联项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '关联项目名称', + `contract_no` VARCHAR(50) DEFAULT NULL COMMENT '合同编号', + `total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '总金额', + `received_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '已收金额', + `pending_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '待收金额', + `account_id` BIGINT DEFAULT NULL COMMENT '收款账户ID', + `account_name` VARCHAR(100) DEFAULT NULL COMMENT '收款账户名称', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/partial/received/overdue', + `expected_date` DATE DEFAULT NULL COMMENT '预计收款日期', + `actual_date` DATE DEFAULT NULL COMMENT '实际收款日期', + `invoice_required` TINYINT DEFAULT 0 COMMENT '是否需要发票', + `invoice_issued` TINYINT DEFAULT 0 COMMENT '发票是否已开', + `invoice_no` VARCHAR(50) DEFAULT NULL COMMENT '发票号', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人名称', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_income_no` (`income_no`), + KEY `idx_type` (`type`), + KEY `idx_customer_id` (`customer_id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_status` (`status`), + KEY `idx_expected_date` (`expected_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务收入表'; + +-- ================================== +-- 财务支出表 +-- ================================== +DROP TABLE IF EXISTS `finance_expense`; +CREATE TABLE `finance_expense` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `expense_no` VARCHAR(50) NOT NULL COMMENT '支出编号', + `type` VARCHAR(30) DEFAULT 'other' COMMENT '支出类型 salary/office/rent/travel/marketing/equipment/service/tax/social_insurance/other', + `title` VARCHAR(200) NOT NULL COMMENT '支出名称', + `payee_name` VARCHAR(100) DEFAULT NULL COMMENT '收款方名称', + `payee_account` VARCHAR(50) DEFAULT NULL COMMENT '收款方账号', + `payee_bank_name` VARCHAR(100) DEFAULT NULL COMMENT '收款方开户行', + `amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '金额', + `project_id` BIGINT DEFAULT NULL COMMENT '关联项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '关联项目名称', + `department_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `department_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `account_id` BIGINT DEFAULT NULL COMMENT '付款账户ID', + `account_name` VARCHAR(100) DEFAULT NULL COMMENT '付款账户名称', + `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态 draft/pending/approved/paid/rejected', + `approval_id` BIGINT DEFAULT NULL COMMENT '审批流程ID', + `approval_status` VARCHAR(20) DEFAULT NULL COMMENT '审批状态', + `attachments` JSON DEFAULT NULL COMMENT '附件路径', + `payment_date` DATE DEFAULT NULL COMMENT '付款日期', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人名称', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_expense_no` (`expense_no`), + KEY `idx_type` (`type`), + KEY `idx_project_id` (`project_id`), + KEY `idx_department_id` (`department_id`), + KEY `idx_status` (`status`), + KEY `idx_payment_date` (`payment_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务支出表'; + +-- ================================== +-- 财务预算表 +-- ================================== +DROP TABLE IF EXISTS `finance_budget`; +CREATE TABLE `finance_budget` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '预算名称', + `period` VARCHAR(20) DEFAULT 'monthly' COMMENT '预算周期 monthly/quarterly/yearly', + `year` INT NOT NULL COMMENT '年份', + `quarter` INT DEFAULT NULL COMMENT '季度 1-4', + `month` INT DEFAULT NULL COMMENT '月份 1-12', + `department_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `department_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `project_id` BIGINT DEFAULT NULL COMMENT '项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '项目名称', + `items` JSON DEFAULT NULL COMMENT '预算明细', + `total_budget` DECIMAL(15,2) DEFAULT 0.00 COMMENT '总预算', + `used_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '已使用金额', + `remaining_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '剩余金额', + `usage_rate` DECIMAL(5,2) DEFAULT 0.00 COMMENT '使用率 0-100', + `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态 draft/active/completed/cancelled', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人名称', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_year` (`year`), + KEY `idx_period` (`period`), + KEY `idx_department_id` (`department_id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务预算表'; + +-- ================================== +-- 财务报销表 +-- ================================== +DROP TABLE IF EXISTS `finance_reimbursement`; +CREATE TABLE `finance_reimbursement` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `reimbursement_no` VARCHAR(50) NOT NULL COMMENT '报销单号', + `type` VARCHAR(30) DEFAULT 'other' COMMENT '报销类型 travel/meal/transport/communication/office/other', + `title` VARCHAR(200) NOT NULL COMMENT '报销标题', + `applicant_id` BIGINT DEFAULT NULL COMMENT '申请人ID', + `applicant_name` VARCHAR(50) DEFAULT NULL COMMENT '申请人名称', + `department_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `department_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '总金额', + `items` JSON DEFAULT NULL COMMENT '报销明细', + `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态 draft/pending/approved/paid/rejected', + `approval_id` BIGINT DEFAULT NULL COMMENT '审批流程ID', + `approval_status` VARCHAR(20) DEFAULT NULL COMMENT '审批状态', + `current_approver` VARCHAR(50) DEFAULT NULL COMMENT '当前审批人', + `bank_account_name` VARCHAR(100) DEFAULT NULL COMMENT '收款人银行户名', + `bank_account_no` VARCHAR(50) DEFAULT NULL COMMENT '收款人银行账号', + `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '收款人开户行', + `payment_account_id` BIGINT DEFAULT NULL COMMENT '付款账户ID', + `payment_date` DATE DEFAULT NULL COMMENT '付款日期', + `payment_remark` VARCHAR(500) DEFAULT NULL COMMENT '付款备注', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_reimbursement_no` (`reimbursement_no`), + KEY `idx_type` (`type`), + KEY `idx_applicant_id` (`applicant_id`), + KEY `idx_department_id` (`department_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务报销表'; + +-- ================================== +-- 财务发票表 +-- ================================== +DROP TABLE IF EXISTS `finance_invoice`; +CREATE TABLE `finance_invoice` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `settlement_id` BIGINT DEFAULT NULL COMMENT '关联结算单ID', + `type` VARCHAR(20) DEFAULT 'vat_normal' COMMENT '发票类型 vat_special/vat_normal/personal', + `title` VARCHAR(200) NOT NULL COMMENT '发票抬头', + `tax_code` VARCHAR(50) DEFAULT NULL COMMENT '税号', + `amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '金额', + `file_url` VARCHAR(255) DEFAULT NULL COMMENT '电子发票文件URL', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/issued/rejected', + `submit_time` DATE DEFAULT NULL COMMENT '提交时间', + `issue_time` DATE DEFAULT NULL COMMENT '开票时间', + `reject_reason` VARCHAR(500) DEFAULT NULL COMMENT '驳回原因', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_settlement_id` (`settlement_id`), + KEY `idx_type` (`type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务发票表'; + +-- ================================== +-- 财务结算表 +-- ================================== +DROP TABLE IF EXISTS `finance_settlement`; +CREATE TABLE `finance_settlement` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `project_id` BIGINT DEFAULT NULL COMMENT '项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '项目名称', + `talent_id` BIGINT DEFAULT NULL COMMENT '人才ID', + `talent_name` VARCHAR(50) DEFAULT NULL COMMENT '人才名称', + `period` VARCHAR(20) DEFAULT NULL COMMENT '结算周期/月份', + `total_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '项目总额', + `platform_fee` DECIMAL(15,2) DEFAULT 0.00 COMMENT '平台服务费', + `taxable_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '应纳税所得额', + `tax_rate` DECIMAL(5,4) DEFAULT 0.0000 COMMENT '税率', + `tax_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '扣税金额', + `actual_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '实发金额', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/paying/completed/rejected', + `invoice_status` VARCHAR(20) DEFAULT 'none' COMMENT '发票状态 none/pending/received', + `bank_account_name` VARCHAR(100) DEFAULT NULL COMMENT '银行户名', + `bank_account_no` VARCHAR(50) DEFAULT NULL COMMENT '银行账号', + `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '开户银行', + `audit_time` DATE DEFAULT NULL COMMENT '审核时间', + `payment_time` DATE DEFAULT NULL COMMENT '打款时间', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_talent_id` (`talent_id`), + KEY `idx_period` (`period`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务结算表'; + +-- ================================== +-- 初始化管理员账户 +-- 密码: admin123 (BCrypt加密) +-- ================================== +INSERT INTO `sys_user` (`username`, `password`, `nickname`, `role`, `status`, `remark`) +VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '超级管理员', 'super_admin', 1, '系统初始管理员'); + +-- ================================== +-- 初始化示例数据 +-- ================================== + +-- 服务器示例数据 +INSERT INTO `platform_server` (`name`, `ip`, `internal_ip`, `port`, `type`, `status`, `os`, `cpu_cores`, `memory_total`, `disk_total`, `ssh_user`, `ssh_port`, `panel_port`, `panel_api_key`, `description`) +VALUES +('生产服务器', '47.109.57.58', NULL, 22, 'cloud', 'online', 'CentOS 7.9', 4, 8, 100, 'root', 22, 42588, 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', '南溪小岛主生产服务器,部署1Panel面板'); + +-- 项目示例数据 +INSERT INTO `platform_project` (`name`, `code`, `type`, `status`, `version`, `description`, `sort`) +VALUES +('南溪屿后台管理', 'nanxiislet-admin', 'frontend', 'active', '1.0.0', '南溪屿后台管理系统前端', 1), +('南溪屿API服务', 'nanxiislet-api', 'backend', 'active', '1.0.0', '南溪屿后台管理系统API服务', 2); + +-- 账户示例数据 +INSERT INTO `finance_account` (`name`, `type`, `bank_name`, `bank_branch`, `account_no`, `balance`, `status`, `is_default`, `remark`) +VALUES +('公司对公账户', 'corporate', '招商银行', '上海分行', '6226****1234', 100000.00, 'active', 1, '公司主账户'); diff --git a/src/main/resources/db/init_domain_data.sql b/src/main/resources/db/init_domain_data.sql new file mode 100644 index 0000000..4580e20 --- /dev/null +++ b/src/main/resources/db/init_domain_data.sql @@ -0,0 +1,20 @@ +-- ============================================ +-- 初始化域名数据 +-- ============================================ + +USE nanxiislet; + +-- 插入示例域名数据 +INSERT INTO platform_domain (domain, server_id, server_name, server_ip, status, dns_status, ssl_status, port, enable_https, force_https, description, deploy_status) +SELECT 'codeport.nanxiislet.com', 1, '生产服务器', '47.109.57.58', 'active', 'resolved', 'valid', 443, 1, 1, 'CodePort 码头平台域名', 'deployed' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM platform_domain WHERE domain = 'codeport.nanxiislet.com'); + +INSERT INTO platform_domain (domain, server_id, server_name, server_ip, status, dns_status, ssl_status, port, enable_https, force_https, description, deploy_status) +SELECT 'admin.nanxiislet.com', 1, '生产服务器', '47.109.57.58', 'active', 'resolved', 'valid', 443, 1, 1, '南溪屿后台管理系统', 'deployed' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM platform_domain WHERE domain = 'admin.nanxiislet.com'); + +INSERT INTO platform_domain (domain, server_id, server_name, server_ip, status, dns_status, ssl_status, port, enable_https, force_https, description, deploy_status) +SELECT 'api.nanxiislet.com', 1, '生产服务器', '47.109.57.58', 'pending', 'checking', 'none', 80, 0, 0, 'API服务域名', 'not_deployed' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM platform_domain WHERE domain = 'api.nanxiislet.com'); + +SELECT '域名数据初始化完成!' AS Result; diff --git a/src/main/resources/db/init_domain_dictionaries.sql b/src/main/resources/db/init_domain_dictionaries.sql new file mode 100644 index 0000000..485831f --- /dev/null +++ b/src/main/resources/db/init_domain_dictionaries.sql @@ -0,0 +1,38 @@ +-- ================================== +-- 初始化域名相关字典数据 +-- 包含:域名状态、DNS状态、SSL状态 +-- 备注字段(remark)用于存储前端显示的颜色值 +-- ================================== + +USE `nanxiislet`; + +-- 1. 插入字典类型 +INSERT INTO `sys_dict` (`name`, `code`, `remark`) VALUES +('域名状态', 'domain_status', '域名部署及运行状态'), +('DNS状态', 'dns_status', 'DNS解析状态'), +('SSL状态', 'ssl_status', 'SSL证书状态'); + +-- 2. 插入字典项 + +-- 域名状态 (domain_status) +SET @dict_id = (SELECT id FROM sys_dict WHERE code = 'domain_status'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `remark`) VALUES +(@dict_id, '正常', 'active', 1, 'green'), +(@dict_id, '待配置', 'pending', 2, 'orange'), +(@dict_id, '已过期', 'expired', 3, 'red'), +(@dict_id, '异常', 'error', 4, 'red'); + +-- DNS状态 (dns_status) +SET @dict_id = (SELECT id FROM sys_dict WHERE code = 'dns_status'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `remark`) VALUES +(@dict_id, '已解析', 'resolved', 1, 'green'), +(@dict_id, '未解析', 'unresolved', 2, 'red'), +(@dict_id, '检测中', 'checking', 3, 'blue'); + +-- SSL状态 (ssl_status) +SET @dict_id = (SELECT id FROM sys_dict WHERE code = 'ssl_status'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `remark`) VALUES +(@dict_id, '正常', 'valid', 1, 'green'), +(@dict_id, '即将过期', 'expiring', 2, 'orange'), +(@dict_id, '已过期', 'expired', 3, 'red'), +(@dict_id, '未配置', 'none', 4, 'default'); diff --git a/src/main/resources/db/init_platform_server.sql b/src/main/resources/db/init_platform_server.sql new file mode 100644 index 0000000..17fdfe7 --- /dev/null +++ b/src/main/resources/db/init_platform_server.sql @@ -0,0 +1,83 @@ +-- ============================================ +-- 平台服务器管理 - 数据库初始化脚本 +-- ============================================ + +-- 创建 platform_server 表 +CREATE TABLE IF NOT EXISTS `platform_server` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '服务器名称', + `ip` VARCHAR(50) NOT NULL COMMENT '公网IP', + `internal_ip` VARCHAR(50) COMMENT '内网IP', + `port` INT DEFAULT 22 COMMENT 'SSH端口', + `type` VARCHAR(20) DEFAULT 'cloud' COMMENT '服务器类型 physical/virtual/cloud', + `status` VARCHAR(20) DEFAULT 'offline' COMMENT '状态 online/offline/warning/maintenance', + `os` VARCHAR(100) COMMENT '操作系统', + `cpu_cores` INT COMMENT 'CPU核心数', + `memory_total` INT COMMENT '内存大小(GB)', + `disk_total` INT COMMENT '磁盘大小(GB)', + `tags` VARCHAR(500) COMMENT '标签(JSON格式)', + `ssh_user` VARCHAR(50) DEFAULT 'root' COMMENT 'SSH用户名', + `ssh_port` INT DEFAULT 22 COMMENT 'SSH端口', + `panel_url` VARCHAR(200) COMMENT '1Panel面板地址', + `panel_port` INT DEFAULT 42588 COMMENT '1Panel面板端口', + `panel_api_key` VARCHAR(200) COMMENT '1Panel API密钥', + `description` VARCHAR(500) COMMENT '描述', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` VARCHAR(50) COMMENT '创建人', + `updated_by` VARCHAR(50) COMMENT '更新人', + `deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_ip` (`ip`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台服务器表'; + +-- ============================================ +-- 插入生产服务器数据 (47.109.57.58) +-- ============================================ +INSERT INTO `platform_server` ( + `id`, + `name`, + `ip`, + `internal_ip`, + `port`, + `type`, + `status`, + `os`, + `cpu_cores`, + `memory_total`, + `disk_total`, + `tags`, + `ssh_user`, + `ssh_port`, + `panel_url`, + `panel_port`, + `panel_api_key`, + `description` +) VALUES ( + 1, + '生产服务器', + '47.109.57.58', + NULL, + 22, + 'cloud', + 'online', + 'CentOS 7.9', + 4, + 8, + 100, + '["生产", "主节点"]', + 'root', + 22, + 'http://47.109.57.58', + 42588, + 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', + '南溪小岛主生产服务器,部署1Panel面板' +) ON DUPLICATE KEY UPDATE + `name` = VALUES(`name`), + `panel_port` = VALUES(`panel_port`), + `panel_api_key` = VALUES(`panel_api_key`), + `status` = VALUES(`status`), + `updated_at` = CURRENT_TIMESTAMP; + +-- 验证数据 +SELECT * FROM `platform_server` WHERE `id` = 1; diff --git a/src/main/resources/db/init_server_data.sql b/src/main/resources/db/init_server_data.sql new file mode 100644 index 0000000..a86e78d --- /dev/null +++ b/src/main/resources/db/init_server_data.sql @@ -0,0 +1,52 @@ +-- ============================================ +-- 初始化生产服务器数据 (47.109.57.58) +-- ============================================ + +-- 插入或更新服务器数据 +INSERT INTO platform_server ( + id, + name, + ip, + port, + type, + status, + os, + cpu_cores, + memory_total, + disk_total, + tags, + ssh_user, + ssh_port, + panel_port, + panel_api_key, + description, + created_at, + updated_at +) VALUES ( + 1, + '生产服务器', + '47.109.57.58', + 22, + 'cloud', + 'online', + 'CentOS 7.9', + 4, + 8, + 100, + '["生产", "主节点"]', + 'root', + 22, + 42588, + 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', + '南溪小岛主生产服务器,部署1Panel面板', + NOW(), + NOW() +) ON DUPLICATE KEY UPDATE + name = VALUES(name), + panel_port = VALUES(panel_port), + panel_api_key = VALUES(panel_api_key), + status = VALUES(status), + updated_at = NOW(); + +-- 验证数据 +SELECT id, name, ip, panel_port, panel_api_key, status FROM platform_server WHERE id = 1; diff --git a/src/main/resources/db/init_server_dictionaries.sql b/src/main/resources/db/init_server_dictionaries.sql new file mode 100644 index 0000000..b483d89 --- /dev/null +++ b/src/main/resources/db/init_server_dictionaries.sql @@ -0,0 +1,41 @@ +-- ================================== +-- 初始化服务器相关字典数据 +-- 包含:服务器标签、服务器状态、服务器类型 +-- 备注字段(remark)用于存储前端显示的颜色值 +-- ================================== + +USE `nanxiislet`; + +-- 1. 插入字典类型 +INSERT INTO `sys_dict` (`name`, `code`, `remark`) VALUES +('服务器标签', 'server_tag', '服务器用途分类'), +('服务器状态', 'server_status', '服务器运行状态'), +('服务器类型', 'server_type', '服务器硬件类型'); + +-- 2. 插入字典项 + +-- 服务器标签 (server_tag) +SET @dict_id = (SELECT id FROM sys_dict WHERE code = 'server_tag'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `remark`) VALUES +(@dict_id, '生产', '生产', 1, 'red'), +(@dict_id, '测试', '测试', 2, 'blue'), +(@dict_id, '开发', '开发', 3, 'green'), +(@dict_id, '备份', '备份', 4, 'purple'), +(@dict_id, '主节点', '主节点', 5, 'gold'), +(@dict_id, '从节点', '从节点', 6, 'cyan'), +(@dict_id, '离线', '离线', 7, 'default'); + +-- 服务器状态 (server_status) +SET @dict_id = (SELECT id FROM sys_dict WHERE code = 'server_status'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `remark`) VALUES +(@dict_id, '在线', 'online', 1, 'green'), +(@dict_id, '离线', 'offline', 2, 'default'), +(@dict_id, '告警', 'warning', 3, 'orange'), +(@dict_id, '维护中', 'maintenance', 4, 'blue'); + +-- 服务器类型 (server_type) +SET @dict_id = (SELECT id FROM sys_dict WHERE code = 'server_type'); +INSERT INTO `sys_dict_item` (`dict_id`, `label`, `value`, `sort`, `remark`) VALUES +(@dict_id, '云服务器', 'cloud', 1, 'default'), +(@dict_id, '物理机', 'physical', 2, 'default'), +(@dict_id, '虚拟机', 'virtual', 3, 'default'); diff --git a/src/main/resources/db/migration/V20260115_2__add_menu_project_id.sql b/src/main/resources/db/migration/V20260115_2__add_menu_project_id.sql new file mode 100644 index 0000000..b15b70f --- /dev/null +++ b/src/main/resources/db/migration/V20260115_2__add_menu_project_id.sql @@ -0,0 +1,6 @@ +-- 在 sys_menu 表中添加 project_id 字段 +ALTER TABLE sys_menu ADD COLUMN project_id BIGINT COMMENT '关联项目ID'; + +-- 这里的逻辑是:新加的字段默认为 NULL。 +-- 后续代码逻辑中,如果 project_id 为 NULL,可以视为属于主系统(楠溪框架)菜单, +-- 或者在绑定时,将现有菜单更新为特定项目的 ID。 diff --git a/src/main/resources/db/migration/V20260115_3__import_codeport_menus.sql b/src/main/resources/db/migration/V20260115_3__import_codeport_menus.sql new file mode 100644 index 0000000..aa5d02f --- /dev/null +++ b/src/main/resources/db/migration/V20260115_3__import_codeport_menus.sql @@ -0,0 +1,71 @@ +-- 导入 CodePort 项目菜单 (Project ID = 13) +-- 由系统自动生成 + +SET @pid = 13; + +-- 1. 控制台 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '控制台', 'cp_dashboard', 'menu', '/dashboard', 'dashboard/index', 'DashboardOutlined', 10, 0, 1, 0, NOW(), NOW()); + +-- 2. 社区管理 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '社区管理', 'cp_community', 'directory', '/community', NULL, 'TeamOutlined', 20, 0, 1, 0, NOW(), NOW()); +SET @dir_community = LAST_INSERT_ID(); + +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) VALUES +(@pid, @dir_community, '帖子管理', 'cp_posts', 'menu', '/community/posts', 'community/posts/index', 'FileTextOutlined', 10, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_community, '评论管理', 'cp_comments', 'menu', '/community/comments', 'community/comments/index', 'MessageOutlined', 20, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_community, '标签管理', 'cp_tags', 'menu', '/community/tags', 'community/tags/index', 'TagsOutlined', 30, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_community, '城市圈子', 'cp_circles', 'menu', '/community/circles', 'community/circles/index', 'EnvironmentOutlined', 40, 0, 1, 0, NOW(), NOW()); + +-- 3. 客服管理 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '客服管理', 'cp_support', 'directory', '/support', NULL, 'CustomerServiceOutlined', 30, 0, 1, 0, NOW(), NOW()); +SET @dir_support = LAST_INSERT_ID(); + +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) VALUES +(@pid, @dir_support, '接入会话', 'cp_support_console', 'menu', '/support/console', 'support/session/index', 'CommentOutlined', 10, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_support, '会话列表', 'cp_conversations', 'menu', '/support/conversations', 'support/conversations/index', 'UnorderedListOutlined', 20, 0, 1, 0, NOW(), NOW()); + +-- 4. 内容管理 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '内容管理', 'cp_content', 'directory', '/content', NULL, 'ReadOutlined', 40, 0, 1, 0, NOW(), NOW()); +SET @dir_content = LAST_INSERT_ID(); + +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) VALUES +(@pid, @dir_content, '文章管理', 'cp_articles', 'menu', '/content/articles', 'content/articles/index', 'FileMarkdownOutlined', 10, 0, 1, 0, NOW(), NOW()); + +-- 5. 项目管理 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '项目管理', 'cp_project', 'directory', '/project', NULL, 'ProjectOutlined', 50, 0, 1, 0, NOW(), NOW()); +SET @dir_project = LAST_INSERT_ID(); + +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) VALUES +(@pid, @dir_project, '项目列表', 'cp_projects', 'menu', '/project/list', 'project/list/index', 'UnorderedListOutlined', 10, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_project, '招募管理', 'cp_recruitment', 'menu', '/project/recruitment', 'project/recruitment/index', 'UserSwitchOutlined', 20, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_project, '已成交项目', 'cp_signed', 'menu', '/project/signed', 'project/signed/index', 'CheckCircleOutlined', 30, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_project, '合同管理', 'cp_contract', 'menu', '/project/contract', 'project/contract/index', 'FileProtectOutlined', 40, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_project, '会话管理', 'cp_proj_sessions', 'menu', '/project/sessions', 'project/session/index', 'CommentOutlined', 50, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_project, '会话详情', 'cp_proj_sess_detail', 'menu', '/project/sessions/:id', 'project/session/detail', '', 60, 1, 1, 0, NOW(), NOW()); + +-- 6. 人才管理 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '人才管理', 'cp_talent', 'directory', '/talent-mgr', NULL, 'UserOutlined', 60, 0, 1, 0, NOW(), NOW()); +SET @dir_talent = LAST_INSERT_ID(); + +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) VALUES +(@pid, @dir_talent, '人才列表', 'cp_talent_list', 'menu', '/talent', 'talent/index', 'UserOutlined', 10, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_talent, '简历模板', 'cp_resume_tpl', 'menu', '/talent/resume-templates', 'talent/resume-templates', 'FileWordOutlined', 20, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_talent, '人才详情', 'cp_talent_detail', 'menu', '/talent/:id', 'talent/detail', '', 30, 1, 1, 0, NOW(), NOW()); + +-- 7. 用户管理 +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) +VALUES (@pid, 0, '用户管理', 'cp_user', 'directory', '/user', NULL, 'UsergroupAddOutlined', 70, 0, 1, 0, NOW(), NOW()); +SET @dir_user = LAST_INSERT_ID(); + +INSERT INTO sys_menu (project_id, parent_id, name, code, type, path, component, icon, sort, hidden, status, deleted, created_at, updated_at) VALUES +(@pid, @dir_user, '用户列表', 'cp_user_list', 'menu', '/user/list', 'user/list/index', 'UserOutlined', 10, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_user, '角色管理', 'cp_roles', 'menu', '/user/roles', 'user/roles/index', 'SafetyCertificateOutlined', 20, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_user, '认证管理', 'cp_certification', 'menu', '/user/certification', 'user/certification/index', 'IdcardOutlined', 30, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_user, '岗位管理', 'cp_positions', 'menu', '/user/positions', 'user/positions/index', 'SolutionOutlined', 40, 0, 1, 0, NOW(), NOW()), +(@pid, @dir_user, '等级配置', 'cp_levels', 'menu', '/user/levels', 'user/levels/index', 'StarOutlined', 50, 0, 1, 0, NOW(), NOW()); diff --git a/src/main/resources/db/migration/V20260115__add_project_system_type.sql b/src/main/resources/db/migration/V20260115__add_project_system_type.sql new file mode 100644 index 0000000..357dde4 --- /dev/null +++ b/src/main/resources/db/migration/V20260115__add_project_system_type.sql @@ -0,0 +1,4 @@ +-- 字段 system_type 已存在,故注释防止报错 +-- ALTER TABLE platform_project ADD COLUMN system_type VARCHAR(20) DEFAULT 'admin' COMMENT '系统类型 admin/portal'; +-- 字段 integrate_to_framework 已存在,故注释防止报错 +-- ALTER TABLE platform_project ADD COLUMN integrate_to_framework TINYINT(1) DEFAULT 0 COMMENT '是否集成到框架'; diff --git a/src/main/resources/db/migration/V20260117__add_database_menu.sql b/src/main/resources/db/migration/V20260117__add_database_menu.sql new file mode 100644 index 0000000..0452d55 --- /dev/null +++ b/src/main/resources/db/migration/V20260117__add_database_menu.sql @@ -0,0 +1,13 @@ +-- 添加数据库管理菜单 +-- 在平台管理下添加数据库管理菜单 + +-- 获取平台管理菜单的ID +SET @platform_id = (SELECT id FROM sys_menu WHERE code = 'platform' AND deleted = 0); + +-- 插入数据库管理菜单 +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `component`, `icon`, `sort`, `hidden`, `status`, `deleted`, `created_at`, `updated_at`) VALUES +(@platform_id, '数据库管理', 'platform-databases', 'menu', '/platform/databases', 'platform/databases/index', 'DatabaseOutlined', 5, 0, 1, 0, NOW(), NOW()); + +-- 为超级管理员分配此菜单权限 +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT 1, id FROM `sys_menu` WHERE code = 'platform-databases'; diff --git a/src/main/resources/db/migration/V20260118__add_runtime_menu.sql b/src/main/resources/db/migration/V20260118__add_runtime_menu.sql new file mode 100644 index 0000000..458cd3d --- /dev/null +++ b/src/main/resources/db/migration/V20260118__add_runtime_menu.sql @@ -0,0 +1,13 @@ +-- 添加运行环境管理菜单 +-- 在平台管理下添加运行环境管理菜单 + +-- 获取平台管理菜单的ID +SET @platform_id = (SELECT id FROM sys_menu WHERE code = 'platform' AND deleted = 0); + +-- 插入运行环境管理菜单 +INSERT INTO `sys_menu` (`parent_id`, `name`, `code`, `type`, `path`, `component`, `icon`, `sort`, `hidden`, `status`, `deleted`, `created_at`, `updated_at`) VALUES +(@platform_id, '运行环境', 'platform-runtimes', 'menu', '/platform/runtimes', 'platform/runtimes/index', 'CodeOutlined', 6, 0, 1, 0, NOW(), NOW()); + +-- 为超级管理员分配此菜单权限 +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT 1, id FROM `sys_menu` WHERE code = 'platform-runtimes'; diff --git a/src/main/resources/db/migration/nanxiislet.sql b/src/main/resources/db/migration/nanxiislet.sql new file mode 100644 index 0000000..681251b --- /dev/null +++ b/src/main/resources/db/migration/nanxiislet.sql @@ -0,0 +1,972 @@ +/* + Navicat Premium Data Transfer + + Source Server : nanxiJava + Source Server Type : MySQL + Source Server Version : 80407 + Source Host : 192.168.9.100:3306 + Source Schema : nanxiislet + + Target Server Type : MySQL + Target Server Version : 80407 + File Encoding : 65001 + + Date: 21/01/2026 21:08:35 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for finance_account +-- ---------------------------- +DROP TABLE IF EXISTS `finance_account`; +CREATE TABLE `finance_account` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账户名称', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'corporate' COMMENT '账户类型 corporate-对公账户 merchant-商户号', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开户银行', + `bank_branch` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开户支行', + `account_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '银行账号', + `merchant_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商户ID', + `merchant_platform` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商户平台 wechat/alipay/unionpay/other', + `app_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AppID', + `balance` decimal(15, 2) NULL COMMENT '当前余额', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'active' COMMENT '状态 active-正常 inactive-禁用', + `is_default` tinyint(0) NULL DEFAULT 0 COMMENT '是否默认账户', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务账户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_account +-- ---------------------------- +INSERT INTO `finance_account` VALUES (1, '公司对公账户', 'corporate', '招商银行', '上海分行', '6226****1234', NULL, NULL, NULL, 100000.00, 'active', 1, '公司主账户', '2026-01-10 18:37:46', '2026-01-10 18:37:46', NULL, NULL, 0); + +-- ---------------------------- +-- Table structure for finance_budget +-- ---------------------------- +DROP TABLE IF EXISTS `finance_budget`; +CREATE TABLE `finance_budget` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '预算名称', + `period` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'monthly' COMMENT '预算周期 monthly/quarterly/yearly', + `year` int(0) NOT NULL COMMENT '年份', + `quarter` int(0) NULL DEFAULT NULL COMMENT '季度 1-4', + `month` int(0) NULL DEFAULT NULL COMMENT '月份 1-12', + `department_id` bigint(0) NULL DEFAULT NULL COMMENT '部门ID', + `department_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门名称', + `project_id` bigint(0) NULL DEFAULT NULL COMMENT '项目ID', + `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目名称', + `items` json NULL COMMENT '预算明细', + `total_budget` decimal(15, 2) NULL COMMENT '总预算', + `used_amount` decimal(15, 2) NULL COMMENT '已使用金额', + `remaining_amount` decimal(15, 2) NULL COMMENT '剩余金额', + `usage_rate` decimal(5, 2) NULL COMMENT '使用率 0-100', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'draft' COMMENT '状态 draft/active/completed/cancelled', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_by_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人名称', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_year`(`year`) USING BTREE, + INDEX `idx_period`(`period`) USING BTREE, + INDEX `idx_department_id`(`department_id`) USING BTREE, + INDEX `idx_project_id`(`project_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务预算表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_budget +-- ---------------------------- + +-- ---------------------------- +-- Table structure for finance_expense +-- ---------------------------- +DROP TABLE IF EXISTS `finance_expense`; +CREATE TABLE `finance_expense` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `expense_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '支出编号', + `type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'other' COMMENT '支出类型 salary/office/rent/travel/marketing/equipment/service/tax/social_insurance/other', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '支出名称', + `payee_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款方名称', + `payee_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款方账号', + `payee_bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款方开户行', + `amount` decimal(15, 2) NOT NULL COMMENT '金额', + `project_id` bigint(0) NULL DEFAULT NULL COMMENT '关联项目ID', + `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联项目名称', + `department_id` bigint(0) NULL DEFAULT NULL COMMENT '部门ID', + `department_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门名称', + `account_id` bigint(0) NULL DEFAULT NULL COMMENT '付款账户ID', + `account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '付款账户名称', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'draft' COMMENT '状态 draft/pending/approved/paid/rejected', + `approval_id` bigint(0) NULL DEFAULT NULL COMMENT '审批流程ID', + `approval_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批状态', + `attachments` json NULL COMMENT '附件路径', + `payment_date` date NULL DEFAULT NULL COMMENT '付款日期', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_by_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人名称', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_expense_no`(`expense_no`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_project_id`(`project_id`) USING BTREE, + INDEX `idx_department_id`(`department_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_payment_date`(`payment_date`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务支出表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_expense +-- ---------------------------- + +-- ---------------------------- +-- Table structure for finance_income +-- ---------------------------- +DROP TABLE IF EXISTS `finance_income`; +CREATE TABLE `finance_income` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `income_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '收入编号', + `type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'project' COMMENT '收入类型 project/service_fee/consulting/commission/other', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '收入名称', + `customer_id` bigint(0) NULL DEFAULT NULL COMMENT '客户ID', + `customer_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户名称', + `customer_contact` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户联系方式', + `project_id` bigint(0) NULL DEFAULT NULL COMMENT '关联项目ID', + `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联项目名称', + `contract_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '合同编号', + `total_amount` decimal(15, 2) NOT NULL COMMENT '总金额', + `received_amount` decimal(15, 2) NULL COMMENT '已收金额', + `pending_amount` decimal(15, 2) NULL COMMENT '待收金额', + `account_id` bigint(0) NULL DEFAULT NULL COMMENT '收款账户ID', + `account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款账户名称', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态 pending/partial/received/overdue', + `expected_date` date NULL DEFAULT NULL COMMENT '预计收款日期', + `actual_date` date NULL DEFAULT NULL COMMENT '实际收款日期', + `invoice_required` tinyint(0) NULL DEFAULT 0 COMMENT '是否需要发票', + `invoice_issued` tinyint(0) NULL DEFAULT 0 COMMENT '发票是否已开', + `invoice_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发票号', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_by_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人名称', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_income_no`(`income_no`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_customer_id`(`customer_id`) USING BTREE, + INDEX `idx_project_id`(`project_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_expected_date`(`expected_date`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务收入表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_income +-- ---------------------------- + +-- ---------------------------- +-- Table structure for finance_invoice +-- ---------------------------- +DROP TABLE IF EXISTS `finance_invoice`; +CREATE TABLE `finance_invoice` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `settlement_id` bigint(0) NULL DEFAULT NULL COMMENT '关联结算单ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'vat_normal' COMMENT '发票类型 vat_special/vat_normal/personal', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发票抬头', + `tax_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '税号', + `amount` decimal(15, 2) NOT NULL COMMENT '金额', + `file_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电子发票文件URL', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态 pending/issued/rejected', + `submit_time` date NULL DEFAULT NULL COMMENT '提交时间', + `issue_time` date NULL DEFAULT NULL COMMENT '开票时间', + `reject_reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '驳回原因', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_settlement_id`(`settlement_id`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务发票表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_invoice +-- ---------------------------- + +-- ---------------------------- +-- Table structure for finance_reimbursement +-- ---------------------------- +DROP TABLE IF EXISTS `finance_reimbursement`; +CREATE TABLE `finance_reimbursement` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `reimbursement_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '报销单号', + `type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'other' COMMENT '报销类型 travel/meal/transport/communication/office/other', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '报销标题', + `applicant_id` bigint(0) NULL DEFAULT NULL COMMENT '申请人ID', + `applicant_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请人名称', + `department_id` bigint(0) NULL DEFAULT NULL COMMENT '部门ID', + `department_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门名称', + `total_amount` decimal(15, 2) NOT NULL COMMENT '总金额', + `items` json NULL COMMENT '报销明细', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'draft' COMMENT '状态 draft/pending/approved/paid/rejected', + `approval_id` bigint(0) NULL DEFAULT NULL COMMENT '审批流程ID', + `approval_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批状态', + `current_approver` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当前审批人', + `bank_account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款人银行户名', + `bank_account_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款人银行账号', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收款人开户行', + `payment_account_id` bigint(0) NULL DEFAULT NULL COMMENT '付款账户ID', + `payment_date` date NULL DEFAULT NULL COMMENT '付款日期', + `payment_remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '付款备注', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_reimbursement_no`(`reimbursement_no`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_applicant_id`(`applicant_id`) USING BTREE, + INDEX `idx_department_id`(`department_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务报销表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_reimbursement +-- ---------------------------- + +-- ---------------------------- +-- Table structure for finance_settlement +-- ---------------------------- +DROP TABLE IF EXISTS `finance_settlement`; +CREATE TABLE `finance_settlement` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `project_id` bigint(0) NULL DEFAULT NULL COMMENT '项目ID', + `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目名称', + `talent_id` bigint(0) NULL DEFAULT NULL COMMENT '人才ID', + `talent_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '人才名称', + `period` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '结算周期/月份', + `total_amount` decimal(15, 2) NULL COMMENT '项目总额', + `platform_fee` decimal(15, 2) NULL COMMENT '平台服务费', + `taxable_amount` decimal(15, 2) NULL COMMENT '应纳税所得额', + `tax_rate` decimal(5, 4) NULL COMMENT '税率', + `tax_amount` decimal(15, 2) NULL COMMENT '扣税金额', + `actual_amount` decimal(15, 2) NULL COMMENT '实发金额', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态 pending/paying/completed/rejected', + `invoice_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'none' COMMENT '发票状态 none/pending/received', + `bank_account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '银行户名', + `bank_account_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '银行账号', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开户银行', + `audit_time` date NULL DEFAULT NULL COMMENT '审核时间', + `payment_time` date NULL DEFAULT NULL COMMENT '打款时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_project_id`(`project_id`) USING BTREE, + INDEX `idx_talent_id`(`talent_id`) USING BTREE, + INDEX `idx_period`(`period`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '财务结算表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of finance_settlement +-- ---------------------------- + +-- ---------------------------- +-- Table structure for platform_certificate +-- ---------------------------- +DROP TABLE IF EXISTS `platform_certificate`; +CREATE TABLE `platform_certificate` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `server_id` bigint(0) NOT NULL COMMENT '关联服务器ID', + `server_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '服务器名称', + `panel_ssl_id` bigint(0) NULL DEFAULT NULL COMMENT '1Panel证书ID', + `primary_domain` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主域名', + `other_domains` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '其他域名(逗号分隔)', + `cn` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '证书主体名称', + `organization` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '颁发组织', + `provider` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'dnsAccount' COMMENT '验证方式 dnsAccount/httpManual', + `acme_account_id` bigint(0) NULL DEFAULT NULL COMMENT 'Acme账户ID', + `acme_account_email` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'Acme账户邮箱', + `dns_account_id` bigint(0) NULL DEFAULT NULL COMMENT 'DNS账户ID', + `dns_account_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'DNS账户名称', + `dns_account_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'DNS账户类型', + `key_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'P256' COMMENT '密钥算法 P256/P384/RSA2048/RSA4096', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态 pending/valid/expired/error', + `auto_renew` tinyint(1) NULL DEFAULT 1 COMMENT '自动续签', + `start_date` date NULL DEFAULT NULL COMMENT '生效时间', + `expire_date` date NULL DEFAULT NULL COMMENT '过期时间', + `cert_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '证书内容', + `key_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '私钥内容', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `last_sync_time` datetime(0) NULL DEFAULT NULL COMMENT '最后同步时间', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_server_id`(`server_id`) USING BTREE, + INDEX `idx_panel_ssl_id`(`panel_ssl_id`) USING BTREE, + INDEX `idx_primary_domain`(`primary_domain`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_expire_date`(`expire_date`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 35 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '平台证书表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of platform_certificate +-- ---------------------------- +INSERT INTO `platform_certificate` VALUES (1, 1, '生产服务器', 8, 'vnc.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-11-14', '2026-02-12', NULL, NULL, '', '2026-01-13 17:46:37', '2026-01-13 15:46:07', '2026-01-13 15:46:07', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (2, 1, '生产服务器', 7, 'manage.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-11-12', '2026-02-10', NULL, NULL, '', '2026-01-13 17:46:37', '2026-01-13 15:46:07', '2026-01-13 15:46:07', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (3, 1, '生产服务器', 5, 'test.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-30', '2026-03-30', NULL, NULL, '', '2026-01-13 17:46:37', '2026-01-13 15:46:07', '2026-01-13 15:46:07', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (4, 1, '生产服务器', 4, 'rustfs.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-15', '2026-03-15', NULL, NULL, '', '2026-01-13 17:46:37', '2026-01-13 15:46:07', '2026-01-13 15:46:07', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (5, 1, '生产服务器', 3, 'www.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-12', '2026-03-12', NULL, NULL, '', '2026-01-13 17:46:37', '2026-01-13 15:46:07', '2026-01-13 15:46:07', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (6, 1, '生产服务器', 2, 'apis.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-12', '2026-03-12', NULL, NULL, '', '2026-01-13 17:46:37', '2026-01-13 15:46:08', '2026-01-13 15:46:08', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (7, 1, '生产服务器', 10, 'testes.superwax.cn', '', '', '', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'EC 256', 'valid', 1, '0001-01-01', '2026-01-13', NULL, NULL, '', '2026-01-13 17:27:58', '2026-01-13 17:27:21', '2026-01-13 17:28:48', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (8, 1, '生产服务器', 9, 'testes.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, '', '2026-01-13 17:27:58', '2026-01-13 17:27:58', '2026-01-13 17:29:01', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (9, 1, '生产服务器', 11, 'testes.superwax.cn', '', NULL, '', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'EC 256', 'valid', 1, '0001-01-01', '2026-01-13', NULL, NULL, NULL, '2026-01-13 17:29:28', '2026-01-13 17:29:23', '2026-01-13 17:31:29', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (10, 1, '生产服务器', 12, 'testes.superwax.cn', '', NULL, '', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'EC 256', 'valid', 1, '0001-01-01', '2026-01-13', NULL, NULL, NULL, '2026-01-13 17:42:04', '2026-01-13 17:41:41', '2026-01-13 17:44:14', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (11, 1, '生产服务器', 13, 'testes.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, '', '2026-01-13 17:46:36', '2026-01-13 17:46:09', '2026-01-13 17:53:42', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (12, 1, '生产服务器', 14, 'testes.superwax.cn', '', NULL, 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'EC 256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, NULL, '2026-01-13 18:25:58', '2026-01-13 18:25:52', '2026-01-13 18:26:51', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (13, 1, '生产服务器', 15, 'testes.superwax.cn', '', NULL, 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'EC 256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, NULL, '2026-01-13 18:28:24', '2026-01-13 18:28:19', '2026-01-13 18:28:19', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (14, 4, '公司主服务器', 5, 'key.nanxiislet.com', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-18', '2026-03-18', NULL, NULL, '', '2026-01-15 13:23:57', '2026-01-13 18:57:53', '2026-01-13 18:57:53', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (15, 4, '公司主服务器', 4, 'www.nanxiislet.com', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-01', '2026-03-01', NULL, NULL, '', '2026-01-15 13:23:57', '2026-01-13 18:57:53', '2026-01-13 18:57:53', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (16, 4, '公司主服务器', 3, 'gitea.nanxiislet.com', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-11-30', '2026-02-28', NULL, NULL, '', '2026-01-15 13:23:57', '2026-01-13 18:57:53', '2026-01-13 18:57:53', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (17, 4, '公司主服务器', 2, 'nanxiislet.com', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-11-30', '2026-02-28', NULL, NULL, '', '2026-01-15 13:23:57', '2026-01-13 18:57:53', '2026-01-13 18:57:53', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (18, 6, '生成服务器', 15, 'testes.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, '', '2026-01-15 17:49:34', '2026-01-15 17:46:36', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (19, 6, '生成服务器', 8, 'vnc.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-11-14', '2026-02-12', NULL, NULL, '', '2026-01-15 17:49:35', '2026-01-15 17:46:36', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (20, 6, '生成服务器', 7, 'manage.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, '', '2026-01-15 17:49:35', '2026-01-15 17:46:36', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (21, 6, '生成服务器', 5, 'test.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-30', '2026-03-30', NULL, NULL, '', '2026-01-15 17:49:35', '2026-01-15 17:46:36', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (22, 6, '生成服务器', 4, 'rustfs.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-15', '2026-03-15', NULL, NULL, '', '2026-01-15 17:49:35', '2026-01-15 17:46:37', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (23, 6, '生成服务器', 3, 'www.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-12', '2026-03-12', NULL, NULL, '', '2026-01-15 17:49:35', '2026-01-15 17:46:37', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (24, 6, '生成服务器', 2, 'apis.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-12', '2026-03-12', NULL, NULL, '', '2026-01-15 17:49:35', '2026-01-15 17:46:37', '2026-01-15 17:49:49', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (25, 4, '公司主服务器', 6, 'test.nanxiislet.com', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-15', '2026-04-15', NULL, NULL, '', '2026-01-15 13:23:57', '2026-01-15 17:50:32', '2026-01-15 17:50:32', 1, 1, 0); +INSERT INTO `platform_certificate` VALUES (26, 7, '私人服务器', 15, 'testes.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, '', '2026-01-19 16:38:01', '2026-01-19 16:38:01', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (27, 7, '私人服务器', 8, 'vnc.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-15', '2026-04-15', NULL, NULL, '', '2026-01-19 16:38:02', '2026-01-19 16:38:02', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (28, 7, '私人服务器', 7, 'manage.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2026-01-13', '2026-04-13', NULL, NULL, '', '2026-01-19 16:38:02', '2026-01-19 16:38:02', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (29, 7, '私人服务器', 5, 'test.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-30', '2026-03-30', NULL, NULL, '', '2026-01-19 16:38:02', '2026-01-19 16:38:02', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (30, 7, '私人服务器', 4, 'rustfs.superwax.cn', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-15', '2026-03-15', NULL, NULL, '', '2026-01-19 16:38:02', '2026-01-19 16:38:02', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (31, 7, '私人服务器', 3, 'www.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-12', '2026-03-12', NULL, NULL, '', '2026-01-19 16:38:02', '2026-01-19 16:38:02', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (32, 7, '私人服务器', 2, 'apis.superwax.cn', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'P256', 'valid', 1, '2025-12-12', '2026-03-12', NULL, NULL, '', '2026-01-19 16:38:02', '2026-01-19 16:38:02', '2026-01-19 20:08:23', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (33, 8, '后端主服务器', 2, 'apis.nanxiislet.com', '', NULL, '', 'dnsAccount', 1, NULL, 1, NULL, NULL, 'EC 256', 'valid', 1, '0001-01-01', '2026-01-21', NULL, NULL, NULL, '2026-01-21 15:48:54', '2026-01-21 15:48:48', '2026-01-21 15:55:01', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (34, 8, '后端主服务器', 3, 'apis.nanxiislet.com', '', 'E7', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 2, NULL, NULL, 'P256', 'valid', 1, '2026-01-21', '2026-04-21', NULL, NULL, '', '2026-01-21 15:56:25', '2026-01-21 15:55:21', '2026-01-21 17:37:07', 1, 1, 1); +INSERT INTO `platform_certificate` VALUES (35, 8, '后端主服务器', 4, 'apis.codeport.online', '', 'E8', 'Let\'s Encrypt', 'dnsAccount', 1, NULL, 2, NULL, NULL, 'P256', 'valid', 1, '2026-01-21', '2026-04-21', NULL, NULL, '', '2026-01-21 17:39:18', '2026-01-21 17:37:45', '2026-01-21 17:37:45', 1, 1, 0); + +-- ---------------------------- +-- Table structure for platform_domain +-- ---------------------------- +DROP TABLE IF EXISTS `platform_domain`; +CREATE TABLE `platform_domain` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '域名', + `project_id` bigint(0) NULL DEFAULT NULL COMMENT '关联项目ID', + `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联项目名称', + `server_id` bigint(0) NULL DEFAULT NULL COMMENT '关联服务器ID', + `server_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联服务器名称', + `server_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '服务器IP', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态 active/pending/expired/error', + `dns_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'checking' COMMENT 'DNS状态 resolved/unresolved/checking', + `dns_records` json NULL COMMENT 'DNS记录', + `ssl_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'none' COMMENT 'SSL状态 valid/expiring/expired/none', + `ssl_expire_date` date NULL DEFAULT NULL COMMENT 'SSL过期时间', + `certificate_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '证书ID', + `certificate_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '证书名称', + `nginx_config_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'Nginx配置路径', + `proxy_pass` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '代理地址', + `port` int(0) NULL DEFAULT 80 COMMENT '端口', + `enable_https` tinyint(0) NULL DEFAULT 0 COMMENT '是否启用HTTPS', + `force_https` tinyint(0) NULL DEFAULT 0 COMMENT '是否强制HTTPS', + `panel_website_id` bigint(0) NULL DEFAULT NULL COMMENT '1Panel网站ID', + `panel_ssl_id` bigint(0) NULL DEFAULT NULL COMMENT '1Panel证书ID', + `site_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '网站目录路径', + `alias` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '网站别名', + `deploy_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'not_deployed' COMMENT '部署状态 not_deployed/deploying/deployed/failed', + `last_deploy_time` datetime(0) NULL DEFAULT NULL COMMENT '最后部署时间', + `last_deploy_message` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后部署消息', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + `runtime_id` bigint(0) NULL DEFAULT NULL COMMENT '关联运行环境ID', + `runtime_server_id` bigint(0) NULL DEFAULT NULL COMMENT '运行环境所属服务器ID', + `runtime_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '运行环境名称', + `runtime_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '运行环境类型 java/node', + `runtime_deploy_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '运行环境部署状态 not_deployed/deploying/deployed/failed', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_domain`(`domain`) USING BTREE, + INDEX `idx_project_id`(`project_id`) USING BTREE, + INDEX `idx_server_id`(`server_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_deploy_status`(`deploy_status`) USING BTREE, + INDEX `idx_panel_website_id`(`panel_website_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '平台域名表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of platform_domain +-- ---------------------------- +INSERT INTO `platform_domain` VALUES (1, 'vnc.superwax.cn', NULL, NULL, 7, '私人服务器', '47.109.57.58', 'active', 'resolved', NULL, 'valid', '2026-04-15', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: vnc.superwax.cn', '2026-01-13 16:41:19', '2026-01-19 20:08:23', 1, 1, 1, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (2, 'manage.superwax.cn', NULL, NULL, 7, '私人服务器', '47.109.57.58', 'active', 'resolved', NULL, 'valid', '2026-04-13', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: manage.superwax.cn', '2026-01-13 16:41:19', '2026-01-19 20:08:23', 1, 1, 1, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (3, 'test.superwax.cn', NULL, NULL, 7, '私人服务器', '47.109.57.58', 'active', 'resolved', NULL, 'valid', '2026-03-30', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: test.superwax.cn', '2026-01-13 16:41:19', '2026-01-19 20:08:23', 1, 1, 1, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (4, 'rustfs.superwax.cn', NULL, NULL, 7, '私人服务器', '47.109.57.58', 'active', 'resolved', NULL, 'valid', '2026-03-15', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: rustfs.superwax.cn', '2026-01-13 16:41:19', '2026-01-19 20:08:23', 1, 1, 1, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (5, 'www.superwax.cn', NULL, NULL, 7, '私人服务器', '47.109.57.58', 'active', 'resolved', NULL, 'valid', '2026-03-12', '31', NULL, NULL, '', 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: www.superwax.cn', '2026-01-13 16:41:19', '2026-01-19 20:08:23', 1, 1, 1, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (6, 'apis.superwax.cn', NULL, NULL, 7, '私人服务器', '47.109.57.58', 'active', 'resolved', NULL, 'valid', '2026-03-12', '32', NULL, NULL, 'https://127.0.0.1:3000', 80, 1, 0, 42, NULL, NULL, NULL, 'undeployed', '2026-01-19 19:31:19', '已删除部署', '从证书同步: apis.superwax.cn', '2026-01-13 16:41:19', '2026-01-19 20:08:23', 1, 1, 1, 7, 7, 'nodeAPP', NULL, NULL); +INSERT INTO `platform_domain` VALUES (7, 'testes.superwax.cn', 7, '框架', 7, '私人服务器', '47.109.57.58', 'pending', 'resolved', NULL, 'valid', '2026-04-13', '13', NULL, NULL, '', 80, 1, 0, 29, 15, '/opt/1panel/www/sites/testes.superwax.cn/index', 'testes_superwax_cn', 'pending', '2026-01-13 22:18:08', '部署成功', '', '2026-01-13 18:28:42', '2026-01-19 20:08:23', 1, 1, 1, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (8, 'key.nanxiislet.com', 13, 'codePort管理', 4, '公司主服务器', '47.92.161.35', 'active', 'resolved', NULL, 'valid', '2026-03-18', NULL, NULL, NULL, '', 443, 1, 0, 13, 5, NULL, NULL, 'deployed', '2026-01-21 20:50:52', '部署成功', '从证书同步: key.nanxiislet.com', '2026-01-13 18:58:06', '2026-01-15 21:15:29', 1, 1, 0, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (9, 'www.nanxiislet.com', NULL, NULL, 4, '公司主服务器', '47.92.161.35:34885', 'active', 'resolved', NULL, 'valid', '2026-03-01', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: www.nanxiislet.com', '2026-01-13 18:58:07', '2026-01-13 18:58:07', 1, 1, 0, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (10, 'gitea.nanxiislet.com', NULL, NULL, 4, '公司主服务器', '47.92.161.35:34885', 'active', 'resolved', NULL, 'valid', '2026-02-28', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: gitea.nanxiislet.com', '2026-01-13 18:58:07', '2026-01-13 18:58:07', 1, 1, 0, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (11, 'nanxiislet.com', NULL, NULL, 4, '公司主服务器', '47.92.161.35:34885', 'active', 'resolved', NULL, 'valid', '2026-02-28', NULL, NULL, NULL, NULL, 443, 1, 0, NULL, NULL, NULL, NULL, 'not_deployed', NULL, NULL, '从证书同步: nanxiislet.com', '2026-01-13 18:58:07', '2026-01-13 18:58:07', 1, 1, 0, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (12, 'test.nanxiislet.com', 9, '楠溪框架', 4, '公司主服务器', '47.92.161.35', 'active', 'resolved', NULL, 'valid', '2026-04-15', '25', NULL, NULL, '', 443, 1, 0, 11, 6, NULL, NULL, 'deployed', '2026-01-15 17:59:43', '部署成功', '从证书同步: test.nanxiislet.com', '2026-01-15 17:52:41', '2026-01-15 17:59:27', 1, 1, 0, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `platform_domain` VALUES (16, 'apis.nanxiislet.com', NULL, NULL, 8, '后端主服务器', '115.159.225.39', 'active', 'resolved', NULL, 'valid', '2026-04-21', '34', NULL, NULL, 'https://115.159.225.39:8080', 80, 1, 0, 3, NULL, '/opt/1panel/www/sites/apis.nanxiislet.com/index', 'apis_nanxiislet_com', 'undeployed', '2026-01-21 16:15:24', '已删除部署', '后端服务', '2026-01-21 15:57:20', '2026-01-21 17:37:00', 1, 1, 1, 1, 8, 'nanxiJAVA', NULL, NULL); +INSERT INTO `platform_domain` VALUES (17, 'apis.codeport.online', NULL, NULL, 8, '后端主服务器', '115.159.225.39', 'active', 'resolved', NULL, 'valid', '2026-04-21', '35', NULL, NULL, '', 80, 1, 0, 14, NULL, NULL, NULL, 'deployed', '2026-01-21 20:43:47', '运行环境部署成功', '从证书同步: apis.codeport.online', '2026-01-21 17:39:25', '2026-01-21 20:43:44', 1, 1, 0, 2, 8, 'nanxiJAVA', 'java', 'deployed'); + +-- ---------------------------- +-- Table structure for platform_project +-- ---------------------------- +DROP TABLE IF EXISTS `platform_project`; +CREATE TABLE `platform_project` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目编码', + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目类型', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '访问地址', + `domain_id` bigint(0) NULL DEFAULT NULL COMMENT '关联域名ID', + `server_id` bigint(0) NULL DEFAULT NULL COMMENT '关联服务器ID', + `server_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '服务器名称', + `deploy_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部署路径', + `version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当前版本', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'inactive' COMMENT '状态 active/inactive/deploying', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图标', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `sort` int(0) NULL DEFAULT 0 COMMENT '排序', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + `short_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目简称', + `project_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'default' COMMENT '项目分组', + `logo` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'Logo文字', + `color` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主题色', + `domain` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '绑定域名', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `enable_https` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用HTTPS', + `panel_website_id` bigint(0) NULL DEFAULT NULL COMMENT '1Panel网站ID', + `panel_ssl_id` bigint(0) NULL DEFAULT NULL COMMENT '1Panel证书ID', + `last_deploy_time` datetime(0) NULL DEFAULT NULL COMMENT '最后部署时间', + `last_deploy_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后部署状态', + `last_deploy_message` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后部署消息', + `system_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'admin' COMMENT '系统类型 admin/portal', + `integrate_to_framework` tinyint(1) NULL DEFAULT 0 COMMENT '是否集成到框架', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_server_id`(`server_id`) USING BTREE, + INDEX `idx_domain_id`(`domain_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '平台项目表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of platform_project +-- ---------------------------- +INSERT INTO `platform_project` VALUES (1, '南溪屿后台管理', 'nanxiislet-admin', 'frontend', NULL, NULL, NULL, NULL, NULL, '1.0.0', 'active', NULL, '南溪屿后台管理系统前端', 1, '2026-01-10 18:37:46', '2026-01-13 19:11:54', NULL, 1, 1, NULL, 'default', NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL, NULL, 'admin', 0); +INSERT INTO `platform_project` VALUES (2, '南溪屿API服务', 'nanxiislet-api', 'backend', NULL, NULL, NULL, NULL, NULL, '1.0.0', 'active', NULL, '南溪屿后台管理系统API服务', 2, '2026-01-10 18:37:46', '2026-01-13 19:11:51', NULL, 1, 1, NULL, 'default', NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL, NULL, 'admin', 0); +INSERT INTO `platform_project` VALUES (3, '阿里', 'yunai', 'web', 'https://testes.superwax.cn', 7, 1, NULL, '/opt/1panel/www/sites/testes.superwax.cn/index', NULL, 'active', NULL, '测试项目', 0, '2026-01-13 19:12:39', '2026-01-13 21:20:04', 1, 1, 1, 'codes', 'default', '代', '#1890ff', 'testes.superwax.cn', '测试项目', 1, 22, 15, '2026-01-13 20:40:35', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (4, '阿里', 'yunai', 'web', 'https://testes.superwax.cn', 7, 1, NULL, '/opt/1panel/www/sites/testes.superwax.cn/index', NULL, 'active', NULL, '测试项目', 0, '2026-01-13 21:20:36', '2026-01-13 21:28:19', 1, 1, 1, 'codes', 'default', '代', '#1890ff', 'testes.superwax.cn', '', 1, 23, 15, '2026-01-13 21:20:39', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (5, '阿里', 'yunai', 'web', 'https://testes.superwax.cn', 7, 1, NULL, '/opt/1panel/www/sites/testes.superwax.cn/index', NULL, 'active', NULL, '测试项目', 0, '2026-01-13 21:28:48', '2026-01-13 21:34:45', 1, 1, 1, 'codes', 'default', '代', '#1890ff', 'testes.superwax.cn', '', 1, 25, 15, '2026-01-13 21:34:23', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (6, '阿里', 'yunai', 'web', 'https://testes.superwax.cn', 7, 1, NULL, '/opt/1panel/www/sites/testes.superwax.cn/index', NULL, 'active', NULL, '测试项目', 0, '2026-01-13 21:36:04', '2026-01-13 22:17:30', 1, 1, 1, 'codes', 'default', '代', '#1890ff', 'testes.superwax.cn', '测试项目', 1, 28, 15, '2026-01-13 22:17:16', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (7, '框架', 'yunai', 'web', 'http://testes.superwax.cn', 7, 1, NULL, '/opt/1panel/www/sites/testes.superwax.cn/index', NULL, 'active', NULL, '测试项目', 0, '2026-01-13 22:18:02', '2026-01-15 17:39:44', 1, 1, 1, 'codes', 'default', '代', '#1890ff', 'testes.superwax.cn', '测试项目', 1, 29, 15, '2026-01-13 22:18:07', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (8, '楠溪框架', 'nanxiAdmin', 'web', 'https://test.nanxiislet.com', 12, 4, NULL, '/opt/1panel/www/sites/test.nanxiislet.com/index', NULL, 'active', NULL, '框架', 0, '2026-01-15 17:52:29', '2026-01-15 17:55:25', 1, 1, 1, 'NanxiAdmin', 'default', '框', '#1890ff', 'test.nanxiislet.com', '', 1, 10, 6, '2026-01-15 17:53:06', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (9, '楠溪框架', 'nanxiAdmin', 'web', 'https://test.nanxiislet.com', 12, 4, NULL, '/opt/1panel/www/sites/test.nanxiislet.com/index', NULL, 'active', NULL, '公司整体框架', 0, '2026-01-15 17:58:04', '2026-01-15 21:33:47', 1, 1, 0, 'NanxiAdmin', 'default', '框', '#1890ff', 'test.nanxiislet.com', '公司整体框架', 1, 11, 6, '2026-01-15 17:59:41', 'success', '部署成功', 'admin', 0); +INSERT INTO `platform_project` VALUES (10, '阿里', 'yunai', 'web', '', NULL, 4, NULL, '', NULL, 'active', NULL, '', 0, '2026-01-15 18:24:33', '2026-01-15 18:26:49', 1, 1, 1, 'codes', 'default', '代', '#1890ff', NULL, '', 0, NULL, NULL, NULL, NULL, NULL, 'admin', 1); +INSERT INTO `platform_project` VALUES (11, '树莓派', 'yunai', 'web', '', NULL, 4, NULL, '', NULL, 'active', NULL, '', 0, '2026-01-15 18:26:09', '2026-01-15 18:26:54', 1, 1, 1, 'ghfs', 'default', '与', '#1890ff', '', '', 0, NULL, NULL, NULL, NULL, NULL, 'admin', 1); +INSERT INTO `platform_project` VALUES (12, '阿里', 'yunai', 'web', '', NULL, 4, NULL, '', NULL, 'active', NULL, '', 0, '2026-01-15 19:46:40', '2026-01-15 19:49:24', 1, 1, 1, 'codes', 'default', '框', '#1890ff', NULL, '', 0, NULL, NULL, NULL, NULL, NULL, 'admin', 1); +INSERT INTO `platform_project` VALUES (13, 'codePort管理', 'yunai', 'web', 'https://key.nanxiislet.com', 8, 4, NULL, '/opt/1panel/www/sites/key.nanxiislet.com/index', NULL, 'active', NULL, 'codePort后台管理', 0, '2026-01-15 19:56:37', '2026-01-15 21:34:03', 1, 1, 0, 'codes', 'default', '管', '#1890ff', 'key.nanxiislet.com', 'codePort后台管理', 1, 13, 5, '2026-01-21 20:50:49', 'success', '部署成功', 'admin', 1); + +-- ---------------------------- +-- Table structure for platform_server +-- ---------------------------- +DROP TABLE IF EXISTS `platform_server`; +CREATE TABLE `platform_server` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务器名称', + `ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '公网IP', + `internal_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '内网IP', + `port` int(0) NULL DEFAULT 22 COMMENT 'SSH端口', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'cloud' COMMENT '服务器类型 physical/virtual/cloud', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'offline' COMMENT '状态 online/offline/warning/maintenance', + `os` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作系统', + `cpu_cores` int(0) NULL DEFAULT NULL COMMENT 'CPU核心数', + `memory_total` int(0) NULL DEFAULT NULL COMMENT '内存大小(GB)', + `disk_total` int(0) NULL DEFAULT NULL COMMENT '磁盘大小(GB)', + `tags` json NULL COMMENT '标签', + `panel_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '1Panel面板地址', + `panel_port` int(0) NULL DEFAULT NULL COMMENT '1Panel面板端口', + `panel_api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '1Panel API密钥', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE, + INDEX `idx_ip`(`ip`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '平台服务器表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of platform_server +-- ---------------------------- +INSERT INTO `platform_server` VALUES (1, '生产服务器', '47.109.57.58', '192.168.1.10', 22, 'cloud', 'online', 'CentOS 9', 2, 1, 39, '[\"生产\"]', 'http://47.109.57.58:42588/super', 42588, 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', '主生产服务器', '2026-01-10 18:37:46', '2026-01-15 17:39:49', NULL, 1, 1); +INSERT INTO `platform_server` VALUES (2, '测试服务器', '47.98.123.46', '192.168.1.11', 22, 'cloud', 'online', 'Ubuntu 22.04', 2, 4, 50, NULL, NULL, NULL, NULL, '测试环境服务器', '2026-01-10 18:37:46', '2026-01-10 20:23:37', NULL, 1, 1); +INSERT INTO `platform_server` VALUES (3, '阿里', '47.108.5.60', NULL, 22, 'cloud', 'offline', 'centos9.0', NULL, NULL, NULL, NULL, NULL, 8888, NULL, NULL, '2026-01-13 17:04:57', '2026-01-13 18:57:10', 1, 1, 1); +INSERT INTO `platform_server` VALUES (4, '公司主服务器', '47.92.161.35', NULL, 22, 'cloud', 'online', 'centos9.0', 2, 1, 39, '[\"生产\", \"开发\"]', 'http://47.92.161.35:34885/super', 34885, 'rq4YoqLLVEz37GB2XSYOMjBfF7CqtYtZ', NULL, '2026-01-13 18:57:01', '2026-01-21 21:01:02', 1, 1, 0); +INSERT INTO `platform_server` VALUES (5, '树莓派', 'panel.superwax.cn', '192.168.9.100', 22, 'cloud', 'offline', '树莓派系统', NULL, NULL, NULL, '[\"生产\", \"测试\"]', 'http://panel.superwax.cn:8090/super', 8090, 'fjTRfXmFEuPqN8cd25WuOo9ml8W2g0fP', '家里服务器', '2026-01-15 10:34:34', '2026-01-15 15:02:08', 1, 1, 1); +INSERT INTO `platform_server` VALUES (6, '生成服务器', '47.109.57.58', NULL, 22, 'cloud', 'online', 'centos9.0', 2, 1, 39, NULL, 'http://47.109.57.58:42588/super', 42588, 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', NULL, '2026-01-15 17:46:19', '2026-01-15 17:49:50', 1, 1, 1); +INSERT INTO `platform_server` VALUES (7, '私人服务器', '47.109.57.58', NULL, 22, 'cloud', 'online', 'centos9.0', 2, 1, 39, NULL, 'http://47.109.57.58:42588/super', 42588, 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', NULL, '2026-01-17 19:42:04', '2026-01-19 12:08:24', 1, 1, 1); +INSERT INTO `platform_server` VALUES (8, '后端主服务器', '115.159.225.39', NULL, 22, 'cloud', 'online', 'Ubuntu 22.04.4 LTS', 4, 7, 117, '[\"生产\", \"开发\", \"测试\"]', 'http://115.159.225.39:8090/tencentcloud', 8090, 'MBcF6RHwUQC3fvSWxSwKQcd7zpMdYpKC', '部署后端主服务', '2026-01-21 04:59:57', '2026-01-21 21:01:02', 1, 1, 0); + +-- ---------------------------- +-- Table structure for sys_approval_instance +-- ---------------------------- +DROP TABLE IF EXISTS `sys_approval_instance`; +CREATE TABLE `sys_approval_instance` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` bigint(0) NOT NULL COMMENT '模板ID', + `template_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模板名称', + `scenario` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '适用场景', + `business_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务类型', + `business_id` bigint(0) NOT NULL COMMENT '业务ID', + `business_title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '业务标题', + `initiator_id` bigint(0) NOT NULL COMMENT '发起人ID', + `initiator_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发起人名称', + `initiator_avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发起人头像', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'pending' COMMENT '状态', + `current_node_id` bigint(0) NULL DEFAULT NULL COMMENT '当前节点ID', + `current_node_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当前节点名称', + `submitted_at` datetime(0) NULL DEFAULT NULL COMMENT '提交时间', + `completed_at` datetime(0) NULL DEFAULT NULL COMMENT '完成时间', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_template_id`(`template_id`) USING BTREE, + INDEX `idx_business`(`business_type`, `business_id`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '审批实例表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_approval_instance +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sys_approval_node +-- ---------------------------- +DROP TABLE IF EXISTS `sys_approval_node`; +CREATE TABLE `sys_approval_node` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` bigint(0) NOT NULL COMMENT '模板ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '节点名称', + `approver_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'specified' COMMENT '审批人类型: specified-指定人员 role-按角色 department-按部门 superior-上级领导 self_select-发起人自选', + `approver_ids` json NULL COMMENT '审批人ID列表', + `approver_role` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批角色编码', + `dept_id` bigint(0) NULL DEFAULT NULL COMMENT '部门ID', + `dept_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门名称', + `approval_mode` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'or' COMMENT '审批方式: and-会签 or-或签', + `timeout_hours` int(0) NULL DEFAULT NULL COMMENT '超时时间(小时)', + `timeout_action` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '超时操作', + `sort` int(0) NULL DEFAULT 0 COMMENT '节点顺序', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_template_id`(`template_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '审批流程节点表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_approval_node +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sys_approval_record +-- ---------------------------- +DROP TABLE IF EXISTS `sys_approval_record`; +CREATE TABLE `sys_approval_record` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `instance_id` bigint(0) NOT NULL COMMENT '实例ID', + `node_id` bigint(0) NOT NULL COMMENT '节点ID', + `node_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '节点名称', + `approver_id` bigint(0) NOT NULL COMMENT '审批人ID', + `approver_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批人名称', + `approver_avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批人头像', + `action` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作', + `comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批意见', + `operated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_instance_id`(`instance_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '审批记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_approval_record +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sys_approval_template +-- ---------------------------- +DROP TABLE IF EXISTS `sys_approval_template`; +CREATE TABLE `sys_approval_template` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板名称', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `scenario` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '适用场景', + `enabled` tinyint(0) NULL DEFAULT 1 COMMENT '是否启用 0-禁用 1-启用', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_scenario`(`scenario`) USING BTREE, + INDEX `idx_enabled`(`enabled`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '审批流程模板表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_approval_template +-- ---------------------------- +INSERT INTO `sys_approval_template` VALUES (1, '费用报销审批', '员工费用报销审批流程', 'expense_reimbursement', 1, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_approval_template` VALUES (2, '付款申请审批', '付款申请审批流程', 'payment_request', 1, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_approval_template` VALUES (3, '项目发布审批', '项目发布前的审批流程', 'project_publish', 1, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); + +-- ---------------------------- +-- Table structure for sys_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dept`; +CREATE TABLE `sys_dept` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `parent_id` bigint(0) NULL DEFAULT 0 COMMENT '父部门ID,0表示顶级部门', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '部门名称', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门编码', + `leader_id` bigint(0) NULL DEFAULT NULL COMMENT '部门负责人ID', + `leader_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门负责人名称', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱', + `sort` int(0) NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门管理表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_dept +-- ---------------------------- +INSERT INTO `sys_dept` VALUES (1, 0, '南溪屿科技', 'ROOT', NULL, NULL, NULL, NULL, 0, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (2, 1, '技术部', 'TECH', NULL, NULL, NULL, NULL, 1, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (3, 1, '产品部', 'PRODUCT', NULL, NULL, NULL, NULL, 2, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (4, 1, '财务部', 'FINANCE', NULL, NULL, NULL, NULL, 3, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (5, 1, '人力资源部', 'HR', NULL, NULL, NULL, NULL, 4, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (6, 2, '前端开发组', 'TECH-FE', NULL, NULL, NULL, NULL, 1, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (7, 2, '后端开发组', 'TECH-BE', NULL, NULL, NULL, NULL, 2, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); +INSERT INTO `sys_dept` VALUES (8, 2, '运维组', 'TECH-OPS', NULL, NULL, NULL, NULL, 3, 1, NULL, '2026-01-10 18:37:47', '2026-01-10 18:37:47', NULL, NULL, 0); + +-- ---------------------------- +-- Table structure for sys_dict +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict`; +CREATE TABLE `sys_dict` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字典名称', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字典编码', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_dict +-- ---------------------------- +INSERT INTO `sys_dict` VALUES (1, '性别', 'gender', '用户性别', 1, '2026-01-08 22:58:31', '2026-01-08 23:19:08', NULL, 1, 0); +INSERT INTO `sys_dict` VALUES (2, '状态', 'status', '通用状态', 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict` VALUES (3, '审批状态', 'approval_status', '审批流程状态', 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict` VALUES (4, '测试', 'test', '测试字典', 1, '2026-01-08 23:19:36', '2026-01-08 23:19:36', 1, 1, 0); +INSERT INTO `sys_dict` VALUES (5, '系统类别', 'system', '区别系统类型', 1, '2026-01-15 18:04:10', '2026-01-15 18:04:10', 1, 1, 0); +INSERT INTO `sys_dict` VALUES (6, '集成', 'integration', '是否集成管理系统到框架中', 1, '2026-01-15 18:06:26', '2026-01-15 18:06:26', 1, 1, 0); +INSERT INTO `sys_dict` VALUES (7, '运行环境', 'runSysteam', '系统运行环境', 1, '2026-01-21 19:09:55', '2026-01-21 19:09:55', 1, 1, 0); + +-- ---------------------------- +-- Table structure for sys_dict_item +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_item`; +CREATE TABLE `sys_dict_item` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `dict_id` bigint(0) NOT NULL COMMENT '字典ID', + `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字典项标签', + `value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字典项值', + `sort` int(0) NULL DEFAULT 0 COMMENT '排序', + `is_default` tinyint(0) NULL DEFAULT 0 COMMENT '是否默认 0-否 1-是', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_dict_value`(`dict_id`, `value`) USING BTREE, + INDEX `idx_dict_id`(`dict_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典项表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_dict_item +-- ---------------------------- +INSERT INTO `sys_dict_item` VALUES (1, 1, '男', '1', 1, 1, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (2, 1, '女', '2', 2, 0, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (3, 1, '未知', '0', 3, 0, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (4, 2, '正常', '1', 1, 1, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (5, 2, '禁用', '0', 2, 0, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (6, 3, '待审批', 'pending', 1, 1, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (7, 3, '已通过', 'approved', 2, 0, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (8, 3, '已驳回', 'rejected', 3, 0, NULL, 1, '2026-01-08 22:58:31', '2026-01-08 22:58:31', NULL, NULL, 0); +INSERT INTO `sys_dict_item` VALUES (9, 4, '开启', 'open', 0, 1, '', 1, '2026-01-08 23:19:50', '2026-01-08 23:20:39', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (10, 4, '关闭', 'disable', 0, 0, '', 0, '2026-01-08 23:20:06', '2026-01-08 23:20:21', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (11, 5, '门户', 'portal', 0, 0, '', 1, '2026-01-15 18:04:27', '2026-01-15 18:04:27', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (12, 5, '管理端', 'admin', 1, 0, '', 1, '2026-01-15 18:04:38', '2026-01-15 18:04:38', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (13, 6, '集成', 'yes', 0, 0, '', 1, '2026-01-15 18:06:39', '2026-01-15 18:06:39', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (14, 6, '不集成', 'no', 0, 0, '', 1, '2026-01-15 18:06:55', '2026-01-15 18:06:55', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (15, 7, 'node', 'node', 0, 0, 'node运行环境', 1, '2026-01-21 19:10:21', '2026-01-21 19:10:21', 1, 1, 0); +INSERT INTO `sys_dict_item` VALUES (16, 7, 'java', 'java', 1, 0, 'java运行环境', 1, '2026-01-21 19:10:38', '2026-01-21 19:10:38', 1, 1, 0); + +-- ---------------------------- +-- Table structure for sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `parent_id` bigint(0) NULL DEFAULT 0 COMMENT '父菜单ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单编码/Key', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'menu' COMMENT '菜单类型 directory-目录 menu-菜单 button-按钮', + `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '路由路径', + `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '组件路径', + `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图标', + `permission` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '权限标识', + `sort` int(0) NULL DEFAULT 0 COMMENT '排序', + `hidden` tinyint(0) NULL DEFAULT 0 COMMENT '是否隐藏 0-显示 1-隐藏', + `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + `project_id` bigint(0) NULL DEFAULT NULL COMMENT '关联项目ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_code`(`code`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 67 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_menu +-- ---------------------------- +INSERT INTO `sys_menu` VALUES (1, 0, '财务管理', 'finance', 'directory', '/finance', NULL, 'PayCircleOutlined', NULL, 1, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (2, 1, '财务总览', 'finance-overview', 'menu', '/finance/overview', NULL, 'DashboardOutlined', NULL, 1, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (3, 1, '收入管理', 'income', 'menu', '/finance/income', NULL, 'RiseOutlined', NULL, 2, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (4, 1, '支出管理', 'expense', 'menu', '/finance/expense', NULL, 'FallOutlined', NULL, 3, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (5, 1, '报销管理', 'reimbursement', 'menu', '/finance/reimbursement', NULL, 'AuditOutlined', NULL, 4, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (6, 1, '结算管理', 'settlement', 'menu', '/finance/settlement', NULL, 'TransactionOutlined', NULL, 5, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (7, 1, '发票管理', 'invoice', 'menu', '/finance/invoice', NULL, 'FileTextOutlined', NULL, 6, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (8, 1, '账户管理', 'finance-accounts', 'menu', '/finance/accounts', NULL, 'BankOutlined', NULL, 7, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (9, 1, '预算管理', 'budget', 'menu', '/finance/budget', NULL, 'FundOutlined', NULL, 8, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (10, 1, '财务报表', 'finance-reports', 'menu', '/finance/reports', NULL, 'BarChartOutlined', NULL, 9, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (11, 0, '平台管理', 'platform', 'directory', '/platform', NULL, 'AppstoreOutlined', NULL, 2, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (12, 11, '项目管理', 'platform-projects', 'menu', '/platform/projects', NULL, 'ProjectOutlined', NULL, 1, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (13, 11, '服务器管理', 'platform-servers', 'menu', '/platform/servers', NULL, 'CloudServerOutlined', NULL, 2, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (14, 11, '域名管理', 'platform-domains', 'menu', '/platform/domains', NULL, 'GlobalOutlined', NULL, 3, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (15, 11, '证书管理', 'platform-certificates', 'menu', '/platform/certificates', NULL, 'SafetyCertificateOutlined', NULL, 4, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (16, 0, '系统管理', 'system', 'directory', '/system', NULL, 'SettingOutlined', NULL, 3, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (17, 16, '用户管理', 'system-users', 'menu', '/system/users', NULL, 'UserOutlined', NULL, 1, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (18, 16, '角色管理', 'system-roles', 'menu', '/system/roles', NULL, 'TeamOutlined', NULL, 2, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (19, 16, '菜单管理', 'system-menus', 'menu', '/system/menus', NULL, 'MenuOutlined', NULL, 4, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-09 20:42:19', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (20, 16, '字典管理', 'system-dict', 'menu', '/system/dict', NULL, 'BookOutlined', NULL, 5, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-09 20:42:19', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (21, 16, '审批流程', 'system-approval', 'menu', '/system/approval', NULL, 'ApartmentOutlined', NULL, 6, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-09 20:42:19', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (22, 16, '审批实例', 'system-approval-instances', 'menu', '/system/approval/instances', NULL, 'AuditOutlined', NULL, 7, 0, 1, NULL, '2026-01-08 20:54:12', '2026-01-09 20:42:19', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (23, 16, '部门管理', 'system-dept', 'menu', '/system/dept', NULL, 'ClusterOutlined', NULL, 3, 0, 1, NULL, '2026-01-09 20:42:19', '2026-01-09 20:42:19', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (28, 0, '阿里', 'yunai', 'menu', '/dashboard/wei', '', 'AlertOutlined', '', 0, 0, 1, '', '2026-01-15 18:46:45', '2026-01-15 18:46:56', 1, 1, 1, NULL); +INSERT INTO `sys_menu` VALUES (37, 0, '控制台', 'cp_dashboard', 'menu', '/dashboard', 'dashboard/index', 'DashboardOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (38, 0, '社区管理', 'cp_community', 'directory', '/community', NULL, 'TeamOutlined', NULL, 20, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (39, 38, '帖子管理', 'cp_posts', 'menu', '/community/posts', 'community/posts/index', 'FileTextOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (40, 38, '评论管理', 'cp_comments', 'menu', '/community/comments', 'community/comments/index', 'MessageOutlined', NULL, 20, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (41, 38, '标签管理', 'cp_tags', 'menu', '/community/tags', 'community/tags/index', 'TagsOutlined', NULL, 30, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (42, 38, '城市圈子', 'cp_circles', 'menu', '/community/circles', 'community/circles/index', 'EnvironmentOutlined', NULL, 40, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (43, 0, '客服管理', 'cp_support', 'directory', '/support', NULL, 'CustomerServiceOutlined', NULL, 30, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (44, 43, '接入会话', 'cp_support_console', 'menu', '/support/console', 'support/session/index', 'CommentOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (45, 43, '会话列表', 'cp_conversations', 'menu', '/support/conversations', 'support/conversations/index', 'UnorderedListOutlined', NULL, 20, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (46, 0, '内容管理', 'cp_content', 'directory', '/content', NULL, 'ReadOutlined', NULL, 40, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (47, 46, '文章管理', 'cp_articles', 'menu', '/content/articles', 'content/articles/index', 'FileMarkdownOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (48, 0, '项目管理', 'cp_project', 'directory', '/project', NULL, 'ProjectOutlined', NULL, 50, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (49, 48, '项目列表', 'cp_projects', 'menu', '/project/list', 'project/list/index', 'UnorderedListOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (50, 48, '招募管理', 'cp_recruitment', 'menu', '/project/recruitment', 'project/recruitment/index', 'UserSwitchOutlined', NULL, 20, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (51, 48, '已成交项目', 'cp_signed', 'menu', '/project/signed', 'project/signed/index', 'CheckCircleOutlined', NULL, 30, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (52, 48, '合同管理', 'cp_contract', 'menu', '/project/contract', 'project/contract/index', 'FileProtectOutlined', NULL, 40, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (53, 48, '会话管理', 'cp_proj_sessions', 'menu', '/project/sessions', 'project/session/index', 'CommentOutlined', NULL, 50, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (54, 48, '会话详情', 'cp_proj_sess_detail', 'menu', '/project/sessions/:id', 'project/session/detail', '', NULL, 60, 1, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (55, 0, '人才管理', 'cp_talent', 'directory', '/talent-mgr', NULL, 'UserOutlined', NULL, 60, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (56, 55, '人才列表', 'cp_talent_list', 'menu', '/talent', 'talent/index', 'UserOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (57, 55, '简历模板', 'cp_resume_tpl', 'menu', '/talent/resume-templates', 'talent/resume-templates', 'FileWordOutlined', NULL, 20, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (58, 55, '人才详情', 'cp_talent_detail', 'menu', '/talent/:id', 'talent/detail', '', NULL, 30, 1, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (59, 0, '用户管理', 'cp_user', 'directory', '/user', NULL, 'UsergroupAddOutlined', NULL, 70, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (60, 59, '用户列表', 'cp_user_list', 'menu', '/user/list', 'user/list/index', 'UserOutlined', NULL, 10, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (61, 59, '角色管理', 'cp_roles', 'menu', '/user/roles', 'user/roles/index', 'SafetyCertificateOutlined', NULL, 20, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (62, 59, '认证管理', 'cp_certification', 'menu', '/user/certification', 'user/certification/index', 'IdcardOutlined', NULL, 30, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (63, 59, '岗位管理', 'cp_positions', 'menu', '/user/positions', 'user/positions/index', 'SolutionOutlined', NULL, 40, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (64, 59, '等级配置', 'cp_levels', 'menu', '/user/levels', 'user/levels/index', 'StarOutlined', NULL, 50, 0, 1, NULL, '2026-01-15 20:42:20', '2026-01-15 20:42:20', NULL, NULL, 0, 13); +INSERT INTO `sys_menu` VALUES (65, 11, '数据库管理', 'platform-databases', 'menu', '/platform/databases', 'platform/databases/index', 'DatabaseOutlined', NULL, 5, 0, 1, NULL, '2026-01-17 20:28:33', '2026-01-17 20:28:33', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (66, 11, '运行环境', 'platform-runtimes', 'menu', '/platform/runtimes', 'platform/runtimes/index', 'CodeOutlined', NULL, 6, 0, 1, NULL, '2026-01-18 22:09:47', '2026-01-18 22:09:47', NULL, NULL, 0, NULL); +INSERT INTO `sys_menu` VALUES (67, 11, '文件管理', 'PlatformFiles', 'menu', '/platform/files', 'platform/files/index', 'FileOutlined', NULL, 99, 0, 1, NULL, '2026-01-21 16:48:47', '2026-01-21 16:48:47', NULL, NULL, 0, 9); + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色编码', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '角色描述', + `sort` int(0) NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_role +-- ---------------------------- +INSERT INTO `sys_role` VALUES (1, 'super_admin', '超级管理员', '拥有系统所有权限', 1, 1, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0); +INSERT INTO `sys_role` VALUES (2, 'admin', '管理员', '系统管理员,可管理大部分功能', 2, 1, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0); +INSERT INTO `sys_role` VALUES (3, 'finance', '财务', '财务人员,只能访问财务相关功能', 3, 1, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0); +INSERT INTO `sys_role` VALUES (4, 'customer_service', '客服', '客服人员,只能访问客服相关功能', 4, 1, '2026-01-08 20:54:12', '2026-01-08 20:54:12', NULL, NULL, 0); + +-- ---------------------------- +-- Table structure for sys_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role_menu`; +CREATE TABLE `sys_role_menu` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `role_id` bigint(0) NOT NULL COMMENT '角色ID', + `menu_id` bigint(0) NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_role_menu`(`role_id`, `menu_id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_menu_id`(`menu_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色菜单关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_role_menu +-- ---------------------------- +INSERT INTO `sys_role_menu` VALUES (1, 1, 1); +INSERT INTO `sys_role_menu` VALUES (4, 1, 2); +INSERT INTO `sys_role_menu` VALUES (5, 1, 3); +INSERT INTO `sys_role_menu` VALUES (6, 1, 4); +INSERT INTO `sys_role_menu` VALUES (7, 1, 5); +INSERT INTO `sys_role_menu` VALUES (8, 1, 6); +INSERT INTO `sys_role_menu` VALUES (9, 1, 7); +INSERT INTO `sys_role_menu` VALUES (10, 1, 8); +INSERT INTO `sys_role_menu` VALUES (11, 1, 9); +INSERT INTO `sys_role_menu` VALUES (12, 1, 10); +INSERT INTO `sys_role_menu` VALUES (2, 1, 11); +INSERT INTO `sys_role_menu` VALUES (13, 1, 12); +INSERT INTO `sys_role_menu` VALUES (14, 1, 13); +INSERT INTO `sys_role_menu` VALUES (15, 1, 14); +INSERT INTO `sys_role_menu` VALUES (16, 1, 15); +INSERT INTO `sys_role_menu` VALUES (3, 1, 16); +INSERT INTO `sys_role_menu` VALUES (17, 1, 17); +INSERT INTO `sys_role_menu` VALUES (18, 1, 18); +INSERT INTO `sys_role_menu` VALUES (19, 1, 19); +INSERT INTO `sys_role_menu` VALUES (20, 1, 20); +INSERT INTO `sys_role_menu` VALUES (21, 1, 21); +INSERT INTO `sys_role_menu` VALUES (22, 1, 22); +INSERT INTO `sys_role_menu` VALUES (32, 1, 23); +INSERT INTO `sys_role_menu` VALUES (37, 1, 65); +INSERT INTO `sys_role_menu` VALUES (38, 1, 66); + +-- ---------------------------- +-- Table structure for sys_user +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号', + `role` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'user' COMMENT '角色编码', + `dept_id` bigint(0) NULL DEFAULT NULL COMMENT '部门ID', + `dept_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '部门名称', + `status` tinyint(0) NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `last_login_time` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间', + `last_login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录IP', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `created_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `updated_at` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间', + `created_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人ID', + `updated_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人ID', + `deleted` tinyint(0) NULL DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_username`(`username`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_email`(`email`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_user +-- ---------------------------- +INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '超级管理员', NULL, NULL, NULL, 'super_admin', NULL, NULL, 1, '2026-01-21 19:03:34', NULL, '系统初始管理员', '2026-01-10 18:37:46', '2026-01-10 18:37:46', NULL, NULL, 0); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/resources/db/migration_project_deploy.sql b/src/main/resources/db/migration_project_deploy.sql new file mode 100644 index 0000000..01330eb --- /dev/null +++ b/src/main/resources/db/migration_project_deploy.sql @@ -0,0 +1,83 @@ +-- ============================================ +-- 项目部署功能 - 数据库迁移脚本 +-- 如果列已存在会报错,可以忽略重复列的错误继续执行 +-- ============================================ + +-- 创建一个存储过程来安全地添加列 +DROP PROCEDURE IF EXISTS add_column_if_not_exists; + +DELIMITER $$ + +CREATE PROCEDURE add_column_if_not_exists( + IN p_table VARCHAR(64), + IN p_column VARCHAR(64), + IN p_definition VARCHAR(255) +) +BEGIN + DECLARE column_exists INT DEFAULT 0; + + SELECT COUNT(*) INTO column_exists + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table + AND COLUMN_NAME = p_column; + + IF column_exists = 0 THEN + SET @sql = CONCAT('ALTER TABLE ', p_table, ' ADD COLUMN ', p_column, ' ', p_definition); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SELECT CONCAT('Added column: ', p_table, '.', p_column) AS Result; + ELSE + SELECT CONCAT('Column already exists: ', p_table, '.', p_column) AS Result; + END IF; +END$$ + +DELIMITER ; + +-- ============================================ +-- platform_project 表新增字段 +-- ============================================ + +CALL add_column_if_not_exists('platform_project', 'short_name', 'VARCHAR(50) COMMENT ''项目简称'''); +CALL add_column_if_not_exists('platform_project', 'project_group', 'VARCHAR(50) DEFAULT ''default'' COMMENT ''项目分组'''); +CALL add_column_if_not_exists('platform_project', 'logo', 'VARCHAR(50) COMMENT ''Logo文字'''); +CALL add_column_if_not_exists('platform_project', 'color', 'VARCHAR(50) COMMENT ''主题色'''); +CALL add_column_if_not_exists('platform_project', 'domain', 'VARCHAR(200) COMMENT ''绑定域名'''); +CALL add_column_if_not_exists('platform_project', 'remark', 'VARCHAR(500) COMMENT ''备注'''); +CALL add_column_if_not_exists('platform_project', 'enable_https', 'TINYINT(1) DEFAULT 0 COMMENT ''是否启用HTTPS'''); +CALL add_column_if_not_exists('platform_project', 'panel_website_id', 'BIGINT COMMENT ''1Panel网站ID'''); +CALL add_column_if_not_exists('platform_project', 'panel_ssl_id', 'BIGINT COMMENT ''1Panel证书ID'''); +CALL add_column_if_not_exists('platform_project', 'last_deploy_time', 'DATETIME COMMENT ''最后部署时间'''); +CALL add_column_if_not_exists('platform_project', 'last_deploy_status', 'VARCHAR(50) COMMENT ''最后部署状态'''); +CALL add_column_if_not_exists('platform_project', 'last_deploy_message', 'VARCHAR(500) COMMENT ''最后部署消息'''); + +-- ============================================ +-- platform_server 表新增字段 +-- ============================================ + +CALL add_column_if_not_exists('platform_server', 'panel_url', 'VARCHAR(200) COMMENT ''1Panel面板地址'''); +CALL add_column_if_not_exists('platform_server', 'panel_port', 'INT DEFAULT 42588 COMMENT ''1Panel面板端口'''); +CALL add_column_if_not_exists('platform_server', 'panel_api_key', 'VARCHAR(200) COMMENT ''1Panel API密钥'''); + +-- ============================================ +-- 更新服务器数据(更新1Panel配置) +-- ============================================ + +-- 如果服务器记录已存在,只更新 1Panel 配置 +UPDATE platform_server +SET panel_port = 42588, + panel_api_key = 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI' +WHERE id = 1; + +-- 如果没有更新任何行,则插入新记录 +INSERT INTO platform_server (id, name, ip, port, type, status, os, panel_port, panel_api_key) +SELECT 1, '生产服务器', '47.109.57.58', 22, 'cloud', 'online', 'CentOS 7', 42588, 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI' +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM platform_server WHERE id = 1); + +-- 清理存储过程 +DROP PROCEDURE IF EXISTS add_column_if_not_exists; + +-- 显示执行结果 +SELECT '数据库迁移完成!' AS '执行结果'; diff --git a/src/main/resources/db/remove_ssh_columns.sql b/src/main/resources/db/remove_ssh_columns.sql new file mode 100644 index 0000000..2700e8a --- /dev/null +++ b/src/main/resources/db/remove_ssh_columns.sql @@ -0,0 +1,3 @@ +-- 移除 platform_server 表中的 SSH 相关字段 +ALTER TABLE platform_server DROP COLUMN ssh_user; +ALTER TABLE platform_server DROP COLUMN ssh_port; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..9a822ad --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,554 @@ +-- ================================== +-- Nanxiislet Admin 数据库初始化脚本 +-- 创建日期: 2026-01-06 +-- ================================== + +-- 创建数据库(如果不存在) +CREATE DATABASE IF NOT EXISTS `nanxiislet` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE `nanxiislet`; + +-- ================================== +-- 系统用户表 +-- ================================== +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码', + `nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称', + `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', + `role` VARCHAR(50) DEFAULT 'user' COMMENT '角色编码', + `dept_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `dept_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间', + `last_login_ip` VARCHAR(50) DEFAULT NULL COMMENT '最后登录IP', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_phone` (`phone`), + KEY `idx_email` (`email`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统用户表'; + +-- ================================== +-- 平台服务器表 +-- ================================== +DROP TABLE IF EXISTS `platform_server`; +CREATE TABLE `platform_server` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '服务器名称', + `ip` VARCHAR(50) NOT NULL COMMENT '公网IP', + `internal_ip` VARCHAR(50) DEFAULT NULL COMMENT '内网IP', + `port` INT DEFAULT 22 COMMENT 'SSH端口', + `type` VARCHAR(20) DEFAULT 'cloud' COMMENT '服务器类型 physical/virtual/cloud', + `status` VARCHAR(20) DEFAULT 'offline' COMMENT '状态 online/offline/warning/maintenance', + `os` VARCHAR(100) DEFAULT NULL COMMENT '操作系统', + `cpu_cores` INT DEFAULT NULL COMMENT 'CPU核心数', + `memory_total` INT DEFAULT NULL COMMENT '内存大小(GB)', + `disk_total` INT DEFAULT NULL COMMENT '磁盘大小(GB)', + `tags` JSON DEFAULT NULL COMMENT '标签', + + `panel_url` VARCHAR(255) DEFAULT NULL COMMENT '1Panel面板地址', + `panel_port` INT DEFAULT NULL COMMENT '1Panel面板端口', + `panel_api_key` VARCHAR(255) DEFAULT NULL COMMENT '1Panel API密钥', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_ip` (`ip`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台服务器表'; + +-- ================================== +-- 平台项目表 +-- ================================== +DROP TABLE IF EXISTS `platform_project`; +CREATE TABLE `platform_project` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '项目名称', + `code` VARCHAR(50) DEFAULT NULL COMMENT '项目编码', + `type` VARCHAR(50) DEFAULT NULL COMMENT '项目类型', + `url` VARCHAR(255) DEFAULT NULL COMMENT '访问地址', + `domain_id` BIGINT DEFAULT NULL COMMENT '关联域名ID', + `server_id` BIGINT DEFAULT NULL COMMENT '关联服务器ID', + `server_name` VARCHAR(100) DEFAULT NULL COMMENT '服务器名称', + `deploy_path` VARCHAR(255) DEFAULT NULL COMMENT '部署路径', + `version` VARCHAR(50) DEFAULT NULL COMMENT '当前版本', + `status` VARCHAR(20) DEFAULT 'inactive' COMMENT '状态 active/inactive/deploying', + `icon` VARCHAR(255) DEFAULT NULL COMMENT '图标', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `sort` INT DEFAULT 0 COMMENT '排序', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_code` (`code`), + KEY `idx_server_id` (`server_id`), + KEY `idx_domain_id` (`domain_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台项目表'; + +-- ================================== +-- 平台域名表 +-- ================================== +DROP TABLE IF EXISTS `platform_domain`; +CREATE TABLE `platform_domain` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `domain` VARCHAR(255) NOT NULL COMMENT '域名', + `project_id` BIGINT DEFAULT NULL COMMENT '关联项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '关联项目名称', + `server_id` BIGINT DEFAULT NULL COMMENT '关联服务器ID', + `server_name` VARCHAR(100) DEFAULT NULL COMMENT '关联服务器名称', + `server_ip` VARCHAR(50) DEFAULT NULL COMMENT '服务器IP', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 active/pending/expired/error', + `dns_status` VARCHAR(20) DEFAULT 'checking' COMMENT 'DNS状态 resolved/unresolved/checking', + `dns_records` JSON DEFAULT NULL COMMENT 'DNS记录', + `ssl_status` VARCHAR(20) DEFAULT 'none' COMMENT 'SSL状态 valid/expiring/expired/none', + `ssl_expire_date` DATE DEFAULT NULL COMMENT 'SSL过期时间', + `certificate_id` VARCHAR(100) DEFAULT NULL COMMENT '证书ID', + `certificate_name` VARCHAR(100) DEFAULT NULL COMMENT '证书名称', + `nginx_config_path` VARCHAR(255) DEFAULT NULL COMMENT 'Nginx配置路径', + `proxy_pass` VARCHAR(255) DEFAULT NULL COMMENT '代理地址', + `port` INT DEFAULT 80 COMMENT '端口', + `enable_https` TINYINT DEFAULT 0 COMMENT '是否启用HTTPS', + `force_https` TINYINT DEFAULT 0 COMMENT '是否强制HTTPS', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_domain` (`domain`), + KEY `idx_project_id` (`project_id`), + KEY `idx_server_id` (`server_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台域名表'; + +-- ================================== +-- 财务账户表 +-- ================================== +DROP TABLE IF EXISTS `finance_account`; +CREATE TABLE `finance_account` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '账户名称', + `type` VARCHAR(20) DEFAULT 'corporate' COMMENT '账户类型 corporate-对公账户 merchant-商户号', + `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '开户银行', + `bank_branch` VARCHAR(100) DEFAULT NULL COMMENT '开户支行', + `account_no` VARCHAR(50) DEFAULT NULL COMMENT '银行账号', + `merchant_id` VARCHAR(100) DEFAULT NULL COMMENT '商户ID', + `merchant_platform` VARCHAR(20) DEFAULT NULL COMMENT '商户平台 wechat/alipay/unionpay/other', + `app_id` VARCHAR(100) DEFAULT NULL COMMENT 'AppID', + `balance` DECIMAL(15,2) DEFAULT 0.00 COMMENT '当前余额', + `status` VARCHAR(20) DEFAULT 'active' COMMENT '状态 active-正常 inactive-禁用', + `is_default` TINYINT DEFAULT 0 COMMENT '是否默认账户', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`), + KEY `idx_type` (`type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务账户表'; + +-- ================================== +-- 财务收入表 +-- ================================== +DROP TABLE IF EXISTS `finance_income`; +CREATE TABLE `finance_income` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `income_no` VARCHAR(50) NOT NULL COMMENT '收入编号', + `type` VARCHAR(30) DEFAULT 'project' COMMENT '收入类型 project/service_fee/consulting/commission/other', + `title` VARCHAR(200) NOT NULL COMMENT '收入名称', + `customer_id` BIGINT DEFAULT NULL COMMENT '客户ID', + `customer_name` VARCHAR(100) DEFAULT NULL COMMENT '客户名称', + `customer_contact` VARCHAR(100) DEFAULT NULL COMMENT '客户联系方式', + `project_id` BIGINT DEFAULT NULL COMMENT '关联项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '关联项目名称', + `contract_no` VARCHAR(50) DEFAULT NULL COMMENT '合同编号', + `total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '总金额', + `received_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '已收金额', + `pending_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '待收金额', + `account_id` BIGINT DEFAULT NULL COMMENT '收款账户ID', + `account_name` VARCHAR(100) DEFAULT NULL COMMENT '收款账户名称', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/partial/received/overdue', + `expected_date` DATE DEFAULT NULL COMMENT '预计收款日期', + `actual_date` DATE DEFAULT NULL COMMENT '实际收款日期', + `invoice_required` TINYINT DEFAULT 0 COMMENT '是否需要发票', + `invoice_issued` TINYINT DEFAULT 0 COMMENT '发票是否已开', + `invoice_no` VARCHAR(50) DEFAULT NULL COMMENT '发票号', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人名称', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_income_no` (`income_no`), + KEY `idx_type` (`type`), + KEY `idx_customer_id` (`customer_id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_status` (`status`), + KEY `idx_expected_date` (`expected_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务收入表'; + +-- ================================== +-- 财务支出表 +-- ================================== +DROP TABLE IF EXISTS `finance_expense`; +CREATE TABLE `finance_expense` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `expense_no` VARCHAR(50) NOT NULL COMMENT '支出编号', + `type` VARCHAR(30) DEFAULT 'other' COMMENT '支出类型 salary/office/rent/travel/marketing/equipment/service/tax/social_insurance/other', + `title` VARCHAR(200) NOT NULL COMMENT '支出名称', + `payee_name` VARCHAR(100) DEFAULT NULL COMMENT '收款方名称', + `payee_account` VARCHAR(50) DEFAULT NULL COMMENT '收款方账号', + `payee_bank_name` VARCHAR(100) DEFAULT NULL COMMENT '收款方开户行', + `amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '金额', + `project_id` BIGINT DEFAULT NULL COMMENT '关联项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '关联项目名称', + `department_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `department_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `account_id` BIGINT DEFAULT NULL COMMENT '付款账户ID', + `account_name` VARCHAR(100) DEFAULT NULL COMMENT '付款账户名称', + `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态 draft/pending/approved/paid/rejected', + `approval_id` BIGINT DEFAULT NULL COMMENT '审批流程ID', + `approval_status` VARCHAR(20) DEFAULT NULL COMMENT '审批状态', + `attachments` JSON DEFAULT NULL COMMENT '附件路径', + `payment_date` DATE DEFAULT NULL COMMENT '付款日期', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人名称', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_expense_no` (`expense_no`), + KEY `idx_type` (`type`), + KEY `idx_project_id` (`project_id`), + KEY `idx_department_id` (`department_id`), + KEY `idx_status` (`status`), + KEY `idx_payment_date` (`payment_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务支出表'; + +-- ================================== +-- 财务预算表 +-- ================================== +DROP TABLE IF EXISTS `finance_budget`; +CREATE TABLE `finance_budget` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '预算名称', + `period` VARCHAR(20) DEFAULT 'monthly' COMMENT '预算周期 monthly/quarterly/yearly', + `year` INT NOT NULL COMMENT '年份', + `quarter` INT DEFAULT NULL COMMENT '季度 1-4', + `month` INT DEFAULT NULL COMMENT '月份 1-12', + `department_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `department_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `project_id` BIGINT DEFAULT NULL COMMENT '项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '项目名称', + `items` JSON DEFAULT NULL COMMENT '预算明细', + `total_budget` DECIMAL(15,2) DEFAULT 0.00 COMMENT '总预算', + `used_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '已使用金额', + `remaining_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '剩余金额', + `usage_rate` DECIMAL(5,2) DEFAULT 0.00 COMMENT '使用率 0-100', + `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态 draft/active/completed/cancelled', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_by_name` VARCHAR(50) DEFAULT NULL COMMENT '创建人名称', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_year` (`year`), + KEY `idx_period` (`period`), + KEY `idx_department_id` (`department_id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务预算表'; + +-- ================================== +-- 财务报销表 +-- ================================== +DROP TABLE IF EXISTS `finance_reimbursement`; +CREATE TABLE `finance_reimbursement` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `reimbursement_no` VARCHAR(50) NOT NULL COMMENT '报销单号', + `type` VARCHAR(30) DEFAULT 'other' COMMENT '报销类型 travel/meal/transport/communication/office/other', + `title` VARCHAR(200) NOT NULL COMMENT '报销标题', + `applicant_id` BIGINT DEFAULT NULL COMMENT '申请人ID', + `applicant_name` VARCHAR(50) DEFAULT NULL COMMENT '申请人名称', + `department_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `department_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '总金额', + `items` JSON DEFAULT NULL COMMENT '报销明细', + `status` VARCHAR(20) DEFAULT 'draft' COMMENT '状态 draft/pending/approved/paid/rejected', + `approval_id` BIGINT DEFAULT NULL COMMENT '审批流程ID', + `approval_status` VARCHAR(20) DEFAULT NULL COMMENT '审批状态', + `current_approver` VARCHAR(50) DEFAULT NULL COMMENT '当前审批人', + `bank_account_name` VARCHAR(100) DEFAULT NULL COMMENT '收款人银行户名', + `bank_account_no` VARCHAR(50) DEFAULT NULL COMMENT '收款人银行账号', + `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '收款人开户行', + `payment_account_id` BIGINT DEFAULT NULL COMMENT '付款账户ID', + `payment_date` DATE DEFAULT NULL COMMENT '付款日期', + `payment_remark` VARCHAR(500) DEFAULT NULL COMMENT '付款备注', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_reimbursement_no` (`reimbursement_no`), + KEY `idx_type` (`type`), + KEY `idx_applicant_id` (`applicant_id`), + KEY `idx_department_id` (`department_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务报销表'; + +-- ================================== +-- 财务发票表 +-- ================================== +DROP TABLE IF EXISTS `finance_invoice`; +CREATE TABLE `finance_invoice` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `settlement_id` BIGINT DEFAULT NULL COMMENT '关联结算单ID', + `type` VARCHAR(20) DEFAULT 'vat_normal' COMMENT '发票类型 vat_special/vat_normal/personal', + `title` VARCHAR(200) NOT NULL COMMENT '发票抬头', + `tax_code` VARCHAR(50) DEFAULT NULL COMMENT '税号', + `amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '金额', + `file_url` VARCHAR(255) DEFAULT NULL COMMENT '电子发票文件URL', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/issued/rejected', + `submit_time` DATE DEFAULT NULL COMMENT '提交时间', + `issue_time` DATE DEFAULT NULL COMMENT '开票时间', + `reject_reason` VARCHAR(500) DEFAULT NULL COMMENT '驳回原因', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_settlement_id` (`settlement_id`), + KEY `idx_type` (`type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务发票表'; + +-- ================================== +-- 财务结算表 +-- ================================== +DROP TABLE IF EXISTS `finance_settlement`; +CREATE TABLE `finance_settlement` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `project_id` BIGINT DEFAULT NULL COMMENT '项目ID', + `project_name` VARCHAR(100) DEFAULT NULL COMMENT '项目名称', + `talent_id` BIGINT DEFAULT NULL COMMENT '人才ID', + `talent_name` VARCHAR(50) DEFAULT NULL COMMENT '人才名称', + `period` VARCHAR(20) DEFAULT NULL COMMENT '结算周期/月份', + `total_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '项目总额', + `platform_fee` DECIMAL(15,2) DEFAULT 0.00 COMMENT '平台服务费', + `taxable_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '应纳税所得额', + `tax_rate` DECIMAL(5,4) DEFAULT 0.0000 COMMENT '税率', + `tax_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '扣税金额', + `actual_amount` DECIMAL(15,2) DEFAULT 0.00 COMMENT '实发金额', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending/paying/completed/rejected', + `invoice_status` VARCHAR(20) DEFAULT 'none' COMMENT '发票状态 none/pending/received', + `bank_account_name` VARCHAR(100) DEFAULT NULL COMMENT '银行户名', + `bank_account_no` VARCHAR(50) DEFAULT NULL COMMENT '银行账号', + `bank_name` VARCHAR(100) DEFAULT NULL COMMENT '开户银行', + `audit_time` DATE DEFAULT NULL COMMENT '审核时间', + `payment_time` DATE DEFAULT NULL COMMENT '打款时间', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_project_id` (`project_id`), + KEY `idx_talent_id` (`talent_id`), + KEY `idx_period` (`period`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='财务结算表'; + +-- ================================== +-- 初始化管理员账户 +-- 密码: admin123 (BCrypt加密) +-- ================================== +INSERT INTO `sys_user` (`username`, `password`, `nickname`, `role`, `status`, `remark`) +VALUES ('admin', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '超级管理员', 'super_admin', 1, '系统初始管理员'); + +-- ================================== +-- 初始化示例数据 +-- ================================== + +-- 服务器示例数据 +INSERT INTO `platform_server` (`name`, `ip`, `internal_ip`, `port`, `type`, `status`, `os`, `cpu_cores`, `memory_total`, `disk_total`, `ssh_user`, `ssh_port`, `panel_port`, `panel_api_key`, `description`) +VALUES +('生产服务器', '47.109.57.58', NULL, 22, 'cloud', 'online', 'CentOS 7.9', 4, 8, 100, 'root', 22, 42588, 'KBya4QxxPB3b8eYCSECOfhgWpXE0ZhuI', '南溪小岛主生产服务器,部署1Panel面板'); + +-- 项目示例数据 +INSERT INTO `platform_project` (`name`, `code`, `type`, `status`, `version`, `description`, `sort`) +VALUES +('南溪屿后台管理', 'nanxiislet-admin', 'frontend', 'active', '1.0.0', '南溪屿后台管理系统前端', 1), +('南溪屿API服务', 'nanxiislet-api', 'backend', 'active', '1.0.0', '南溪屿后台管理系统API服务', 2); + +-- 账户示例数据 +INSERT INTO `finance_account` (`name`, `type`, `bank_name`, `bank_branch`, `account_no`, `balance`, `status`, `is_default`, `remark`) +VALUES +('公司对公账户', 'corporate', '招商银行', '上海分行', '6226****1234', 100000.00, 'active', 1, '公司主账户'); + +-- ================================== +-- 部门管理表 +-- ================================== +DROP TABLE IF EXISTS `sys_dept`; +CREATE TABLE `sys_dept` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `parent_id` BIGINT DEFAULT 0 COMMENT '父部门ID,0表示顶级部门', + `name` VARCHAR(100) NOT NULL COMMENT '部门名称', + `code` VARCHAR(50) DEFAULT NULL COMMENT '部门编码', + `leader_id` BIGINT DEFAULT NULL COMMENT '部门负责人ID', + `leader_name` VARCHAR(50) DEFAULT NULL COMMENT '部门负责人名称', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `sort` INT DEFAULT 0 COMMENT '排序', + `status` TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常', + `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_code` (`code`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门管理表'; + +-- 初始化部门数据 +INSERT INTO `sys_dept` (`parent_id`, `name`, `code`, `sort`, `status`) VALUES +(0, '南溪屿科技', 'ROOT', 0, 1), +(1, '技术部', 'TECH', 1, 1), +(1, '产品部', 'PRODUCT', 2, 1), +(1, '财务部', 'FINANCE', 3, 1), +(1, '人力资源部', 'HR', 4, 1), +(2, '前端开发组', 'TECH-FE', 1, 1), +(2, '后端开发组', 'TECH-BE', 2, 1), +(2, '运维组', 'TECH-OPS', 3, 1); + +-- ================================== +-- 审批流程模板表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_template`; +CREATE TABLE `sys_approval_template` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` VARCHAR(100) NOT NULL COMMENT '模板名称', + `description` VARCHAR(500) DEFAULT NULL COMMENT '描述', + `scenario` VARCHAR(50) NOT NULL COMMENT '适用场景', + `enabled` TINYINT DEFAULT 1 COMMENT '是否启用 0-禁用 1-启用', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `created_by` BIGINT DEFAULT NULL COMMENT '创建人ID', + `updated_by` BIGINT DEFAULT NULL COMMENT '更新人ID', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记 0-未删除 1-已删除', + PRIMARY KEY (`id`), + KEY `idx_scenario` (`scenario`), + KEY `idx_enabled` (`enabled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程模板表'; + +-- ================================== +-- 审批流程节点表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_node`; +CREATE TABLE `sys_approval_node` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` BIGINT NOT NULL COMMENT '模板ID', + `name` VARCHAR(100) NOT NULL COMMENT '节点名称', + `approver_type` VARCHAR(20) NOT NULL DEFAULT 'specified' COMMENT '审批人类型: specified-指定人员 role-按角色 department-按部门 superior-上级领导 self_select-发起人自选', + `approver_ids` JSON DEFAULT NULL COMMENT '审批人ID列表', + `approver_role` VARCHAR(50) DEFAULT NULL COMMENT '审批角色编码', + `dept_id` BIGINT DEFAULT NULL COMMENT '部门ID', + `dept_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称', + `approval_mode` VARCHAR(10) DEFAULT 'or' COMMENT '审批方式: and-会签 or-或签', + `timeout_hours` INT DEFAULT NULL COMMENT '超时时间(小时)', + `timeout_action` VARCHAR(20) DEFAULT NULL COMMENT '超时操作', + `sort` INT DEFAULT 0 COMMENT '节点顺序', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流程节点表'; + +-- ================================== +-- 审批实例表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_instance`; +CREATE TABLE `sys_approval_instance` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `template_id` BIGINT NOT NULL COMMENT '模板ID', + `template_name` VARCHAR(100) DEFAULT NULL COMMENT '模板名称', + `scenario` VARCHAR(50) NOT NULL COMMENT '适用场景', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型', + `business_id` BIGINT NOT NULL COMMENT '业务ID', + `business_title` VARCHAR(200) DEFAULT NULL COMMENT '业务标题', + `initiator_id` BIGINT NOT NULL COMMENT '发起人ID', + `initiator_name` VARCHAR(50) DEFAULT NULL COMMENT '发起人名称', + `initiator_avatar` VARCHAR(255) DEFAULT NULL COMMENT '发起人头像', + `status` VARCHAR(20) DEFAULT 'pending' COMMENT '状态', + `current_node_id` BIGINT DEFAULT NULL COMMENT '当前节点ID', + `current_node_name` VARCHAR(100) DEFAULT NULL COMMENT '当前节点名称', + `submitted_at` DATETIME DEFAULT NULL COMMENT '提交时间', + `completed_at` DATETIME DEFAULT NULL COMMENT '完成时间', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` TINYINT DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_template_id` (`template_id`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批实例表'; + +-- ================================== +-- 审批记录表 +-- ================================== +DROP TABLE IF EXISTS `sys_approval_record`; +CREATE TABLE `sys_approval_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `instance_id` BIGINT NOT NULL COMMENT '实例ID', + `node_id` BIGINT NOT NULL COMMENT '节点ID', + `node_name` VARCHAR(100) DEFAULT NULL COMMENT '节点名称', + `approver_id` BIGINT NOT NULL COMMENT '审批人ID', + `approver_name` VARCHAR(50) DEFAULT NULL COMMENT '审批人名称', + `approver_avatar` VARCHAR(255) DEFAULT NULL COMMENT '审批人头像', + `action` VARCHAR(20) NOT NULL COMMENT '操作', + `comment` VARCHAR(500) DEFAULT NULL COMMENT '审批意见', + `operated_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + PRIMARY KEY (`id`), + KEY `idx_instance_id` (`instance_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表'; + +-- 初始化审批流程模板示例数据 +INSERT INTO `sys_approval_template` (`name`, `description`, `scenario`, `enabled`) VALUES +('费用报销审批', '员工费用报销审批流程', 'expense_reimbursement', 1), +('付款申请审批', '付款申请审批流程', 'payment_request', 1), +('项目发布审批', '项目发布前的审批流程', 'project_publish', 1);