feat: 添加ERP服务和系统服务,完善员工门户功能
## 新增服务模块 ### 1. ERP服务 (hzhub-erp) - 新增独立的ERP数据适配服务 - 支持SQL Server 2008 R2数据源 - 提供动态API配置管理系统 - 包含客户管理、销售数据等业务接口 ### 2. 系统服务 (hzhub-system) - 新增独立的系统管理服务 - 用户、角色、权限、部门、菜单管理 - 租户管理、操作日志、在线用户监控 - 工作流引擎(warm-flow)集成 - 企业微信审批同步功能 ### 3. API网关 (hzhub-gateway) - 新增Spring Cloud Gateway网关服务 - JWT认证、路由分发、限流熔断 - XSS防护、请求日志记录 - 统一入口端口8080 ## 后台管理功能增强 ### ERP动态API管理 - 新增动态API配置管理界面 - API测试、文档预览、统计监控 - 错误日志查看、缓存管理 - 从数据库表自动导入API配置 ### 系统管理增强 - 企业微信配置管理 - 企业微信审批同步配置 - 部门和用户管理优化 ## 员工门户功能完善 ### 业务页面 - 审批中心:工作流审批、待办任务 - CRM管理:客户关系管理 - 经销商管理:经销商数据展示 - 供应链管理:采购、库存、销售 - BI报表:数据可视化分析 - ERP数据探索:SQL Server数据查询 ### 个人中心 - 基本设置:个人信息管理 - 安全设置:密码修改、登录日志 - 锁屏功能:自动锁屏、手动锁屏 ### 其他功能 - 标签页管理:多标签页导航 - 页面缓存:keepAlive缓存机制 - 会话超时:自动检测并提示 ## 经销商门户 ### 页面路由 - 新增经销商管理页面路由 - AI聊天界面完善 ## 文档更新 - ERP API数据库初始化指南 - ERP API前端完整实现文档 - ERP API测试和验证指南 - Gateway路由迁移计划 - 项目配置文档更新 ## 部署脚本 - 统一启动/停止/重启脚本 - Docker Compose配置优化 - Nginx配置文件更新 ## 技术栈 - 后端: Spring Boot 3.5.8, Java 17 - 前端: Vue 3, TypeScript, Element Plus, Vben Admin - 工作流: warm-flow 1.8.2 - 网关: Spring Cloud Gateway - 数据库: MySQL 8.0, SQL Server 2008 R2 - 缓存: Redis 7 - 向量库: Weaviate 1.25.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
209
CLAUDE.md
@@ -4,14 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
HZHub (汇智中台) is an enterprise-level business platform built on HZHub-AI, integrating AI capabilities with ERP data adaptation. It consists of multiple frontend portals, a backend AI service, an ERP service, and a gateway, orchestrated via Docker Compose.
|
||||
HZHub (汇智中台) is an enterprise-level business platform integrating AI capabilities, system management, approval workflows, and ERP data adaptation. It consists of multiple frontend portals, backend microservices, and an API gateway, orchestrated via Docker Compose.
|
||||
|
||||
## Commands
|
||||
|
||||
### Docker Deployment (Production-like)
|
||||
|
||||
```bash
|
||||
# Start all services (recommended for integration testing)
|
||||
# Start all services
|
||||
cd hzhub-deploy
|
||||
docker-compose up -d
|
||||
|
||||
@@ -20,7 +20,7 @@ docker-compose ps
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f hzhub-ai
|
||||
docker-compose logs -f hzhub-admin
|
||||
docker-compose logs -f hzhub-system
|
||||
|
||||
# Restart services
|
||||
docker-compose restart hzhub-ai
|
||||
@@ -32,61 +32,47 @@ docker-compose down
|
||||
### Backend Development (Spring Boot)
|
||||
|
||||
```bash
|
||||
# Run AI service locally (foreground)
|
||||
# Build hzhub-ai common modules first (required by hzhub-system)
|
||||
cd hzhub-ai
|
||||
mvn clean install -DskipTests
|
||||
|
||||
# Run AI service locally (foreground)
|
||||
cd hzhub-ai/hzhub-admin
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
|
||||
# Run AI service locally (background)
|
||||
cd hzhub-ai
|
||||
./start.sh # Start service in background
|
||||
./status.sh # Check service status
|
||||
./logs.sh # View logs
|
||||
./stop.sh # Stop service
|
||||
./restart.sh # Restart service
|
||||
# Run system service locally (foreground)
|
||||
cd hzhub-system
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
|
||||
# Build all modules
|
||||
cd hzhub-ai
|
||||
mvn clean package
|
||||
|
||||
# Build specific module
|
||||
cd hzhub-ai/hzhub-modules/hzhub-chat
|
||||
mvn clean package
|
||||
# Run ERP service locally (foreground)
|
||||
cd hzhub-erp
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=dev
|
||||
|
||||
# Run tests
|
||||
mvn test
|
||||
```
|
||||
|
||||
**💡 Tip:** For background service management, see [SERVICE_MANAGEMENT.md](../SERVICE_MANAGEMENT.md)
|
||||
**Important:** hzhub-system depends on hzhub-ai's common modules. Always run `mvn clean install -DskipTests` in hzhub-ai first.
|
||||
|
||||
### Frontend Development (Vue 3 + Vben Admin)
|
||||
### Frontend Development (Vue 3)
|
||||
|
||||
```bash
|
||||
# Admin portal development
|
||||
# Admin portal (Vben Admin + Ant Design Vue)
|
||||
cd hzhub-admin
|
||||
pnpm install # Install dependencies
|
||||
pnpm dev # Start dev server (foreground)
|
||||
./start.sh # Start dev server (background)
|
||||
./status.sh # Check service status
|
||||
./logs.sh # View logs
|
||||
pnpm build # Build all packages
|
||||
pnpm --filter=@vben/web-antd build:prod # Build admin frontend
|
||||
pnpm install
|
||||
pnpm dev # Port 5666
|
||||
|
||||
# Employee portal development
|
||||
# Employee portal (Element Plus)
|
||||
cd hzhub-portal-employee
|
||||
pnpm install
|
||||
pnpm dev # Start dev server (foreground)
|
||||
./start.sh # Start dev server (background)
|
||||
./status.sh # Check service status
|
||||
./logs.sh # View logs
|
||||
pnpm dev # Port 5137
|
||||
|
||||
# Dealer portal development
|
||||
# Dealer portal (Element Plus)
|
||||
cd hzhub-portal-dealer
|
||||
pnpm install
|
||||
pnpm dev
|
||||
pnpm dev # Port 5138
|
||||
```
|
||||
|
||||
**💡 Tip:** For background service management (start/stop/restart/status/logs), see [SERVICE_MANAGEMENT.md](../SERVICE_MANAGEMENT.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-Service Structure
|
||||
@@ -99,64 +85,79 @@ pnpm dev
|
||||
└────────────┬────────────────────────────┘
|
||||
│
|
||||
┌────────┴────────┐
|
||||
│ hzhub-gateway │ (API Gateway - planned)
|
||||
│ Spring Cloud │ Auth, routing, rate limiting
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────┴────────┬────────────┐
|
||||
│ hzhub-ai │ hzhub-erp │
|
||||
│ (AI Service) │ (Planned) │
|
||||
│ Spring Boot │ JDBC to │
|
||||
│ 3.5.8 │ SQL Server│
|
||||
└─────────────────┴────────────┘
|
||||
│ hzhub-gateway │ (API Gateway, port 8080)
|
||||
│ Spring Cloud │ JWT auth, routing, rate limiting, XSS
|
||||
└───┬──────┬──────┘
|
||||
│ │
|
||||
┌───▼──┐ ┌▼────────────┐
|
||||
│hzhub │ │ hzhub-system│ hzhub-erp
|
||||
│ -ai │ │ (System Mgmt)│ (SQL Server)
|
||||
│ 6039 │ │ 8083 │ 8082
|
||||
└──────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Backend Module Organization
|
||||
### Gateway Routes
|
||||
|
||||
**hzhub-ai** is organized as a multi-module Maven project:
|
||||
| Path Prefix | Target Service | Features |
|
||||
|-------------|---------------|----------|
|
||||
| `/ai/**` | hzhub-ai:6039 | AI chat, knowledge base, AI workflow |
|
||||
| `/system/**` | hzhub-system:8083 | Users, roles, permissions, tenants, OSS |
|
||||
| `/monitor/**` | hzhub-system:8083 | Operation logs, online users, cache |
|
||||
| `/auth/**` | hzhub-system:8083 | Login, register, captcha, tenant list |
|
||||
| `/resource/**` | hzhub-system:8083 | Email/SMS code, SSE, WebSocket |
|
||||
| `/workflow/**` | hzhub-system:8083 | Approval workflows (warm-flow) |
|
||||
| `/tool/**` | hzhub-system:8083 | Code generator (velocity) |
|
||||
| `/erp/**` | hzhub-erp:8082 | ERP customer data |
|
||||
|
||||
- **hzhub-admin**: Main application entry point (`HZHubAIApplication.java`), configuration files
|
||||
- **hzhub-common**: Shared utilities (core, redis, mybatis, security, satoken, oss, chat, etc.)
|
||||
- **hzhub-modules**: Business modules
|
||||
- **hzhub-chat**: Chat/AI conversation functionality
|
||||
- **hzhub-system**: System management, users, roles, permissions
|
||||
- **hzhub-workflow**: Workflow engine (Flowable-based)
|
||||
- **hzhub-aiflow**: AI workflow orchestration
|
||||
- **hzhub-generator**: Code generator
|
||||
- **hzhub-extend**: Extensions (monitoring, job scheduling)
|
||||
### Backend Service Organization
|
||||
|
||||
**hzhub-ai** (AI Service, port 6039):
|
||||
- **hzhub-admin**: Main application entry point (`HZHubAIApplication.java`)
|
||||
- **hzhub-common**: Shared utility modules (core, redis, mybatis, security, oss, chat, etc.)
|
||||
- **hzhub-modules**:
|
||||
- **hzhub-chat**: AI conversation, knowledge base, MCP
|
||||
- **hzhub-aiflow**: AI workflow orchestration (LangGraph4j)
|
||||
- **hzhub-extend**: Monitoring (spring-boot-admin), job scheduling
|
||||
|
||||
**hzhub-system** (System Service, port 8083):
|
||||
- Independent Spring Boot service
|
||||
- Depends on hzhub-ai common modules (must build hzhub-ai first)
|
||||
- Entry point: `HZHubSystemApplication.java`
|
||||
- **Auth**: login, register, captcha, tenant list (`/auth/**`, `/resource/**`)
|
||||
- **System**: users, roles, menus, departments, dicts, config, posts, tenants, OSS, clients, social login (`/system/**`)
|
||||
- **Monitor**: operation logs, online users, login logs, cache (`/monitor/**`)
|
||||
- **Workflow**: approval process definitions, instances, tasks (`/workflow/**`)
|
||||
- **Generator**: code generation from database tables (`/tool/gen`)
|
||||
|
||||
**hzhub-erp** (ERP Service, port 8082):
|
||||
- SQL Server 2008 R2 data adapter
|
||||
- Customer management, sales data exploration
|
||||
|
||||
### Frontend Architecture
|
||||
|
||||
**hzhub-admin** uses a monorepo structure with pnpm + turbo:
|
||||
**hzhub-admin** (Vben Admin monorepo):
|
||||
- Vue 3 + TypeScript + Ant Design Vue
|
||||
- Pinia state management
|
||||
- Features: system management, workflow, AI flow, knowledge base, chat, monitoring, code generator
|
||||
|
||||
```
|
||||
hzhub-admin/
|
||||
├── apps/
|
||||
│ └── web-antd/ # Main admin application (Ant Design Vue)
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API calls
|
||||
│ │ ├── views/ # Page components
|
||||
│ │ ├── router/ # Vue Router config
|
||||
│ │ └── store/ # Pinia stores
|
||||
│ └── package.json
|
||||
├── packages/ # Shared packages
|
||||
└── package.json # Root monorepo config
|
||||
```
|
||||
**hzhub-portal-employee** (Element Plus):
|
||||
- Vue 3 + TypeScript + Element Plus
|
||||
- Features: dashboard, approval center, CRM, dealer management, supply chain, BI reports, AI chat, ERP
|
||||
|
||||
**Portal applications** (hzhub-portal-employee, hzhub-portal-dealer) are Vue 3 apps with:
|
||||
- Composition API (`<script setup>`)
|
||||
- Pinia state management with persistence
|
||||
- Element Plus UI components
|
||||
- hook-fetch for HTTP requests with SSE support
|
||||
**hzhub-portal-dealer** (Element Plus):
|
||||
- Vue 3 + TypeScript + Element Plus
|
||||
- Features: AI chat interface only
|
||||
|
||||
### Key Technologies
|
||||
|
||||
**Backend (hzhub-ai)**:
|
||||
- Spring Boot 3.5.8, Spring AI 2.0
|
||||
- LangChain4j for AI/LLM integration
|
||||
- Sa-Token for authentication (JWT-based)
|
||||
**Backend**:
|
||||
- Spring Boot 3.5.8, Java 17
|
||||
- LangChain4j + LangGraph4j for AI/LLM integration
|
||||
- Sa-Token for authentication (JWT-based, HmacSHA256)
|
||||
- MyBatis-Plus for database access
|
||||
- Dynamic datasource for multi-database support
|
||||
- warm-flow 1.8.2 for approval workflows
|
||||
- Velocity 2.3 for code generation templates
|
||||
- Weaviate for vector database (knowledge retrieval)
|
||||
|
||||
**Frontend**:
|
||||
@@ -168,7 +169,7 @@ hzhub-admin/
|
||||
|
||||
**Infrastructure**:
|
||||
- MySQL 8.0 (business data)
|
||||
- Redis 7 (cache, session)
|
||||
- Redis 7 (cache, session, rate limiting)
|
||||
- Weaviate 1.25.0 (vector database)
|
||||
- n8n (workflow automation)
|
||||
- MinIO (object storage)
|
||||
@@ -177,7 +178,7 @@ hzhub-admin/
|
||||
|
||||
### Backend API Structure
|
||||
|
||||
Controllers in `hzhub-modules/*/controller/` follow REST conventions:
|
||||
Controllers follow REST conventions:
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@@ -197,7 +198,8 @@ Response wrapper `R<T>` from `hzhub-common-core` provides standardized API respo
|
||||
- Sa-Token handles JWT authentication
|
||||
- Client ID header required: `ClientID` (passed in frontend requests)
|
||||
- Token auto-injected via `Authorization: Bearer <token>`
|
||||
- User permissions checked via Sa-Token's permission system
|
||||
- Gateway injects `X-User-Id`, `X-Client-Id`, `X-Gateway-Verified: true` headers
|
||||
- Backend services trust `X-Gateway-Verified` header to skip redundant JWT validation
|
||||
|
||||
### Dynamic Datasource
|
||||
|
||||
@@ -256,13 +258,14 @@ docker-compose up -d mysql redis weaviate
|
||||
|
||||
**Backend**:
|
||||
- `SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL`: MySQL connection URL
|
||||
- `SPING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME`: DB username
|
||||
- `SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME`: DB username
|
||||
- `SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD`: DB password
|
||||
- `SPRING_DATA_REDIS_HOST`: Redis host
|
||||
- `SPRING_DATA_REDIS_PORT`: Redis port
|
||||
- `JWT_SECRET`: Shared JWT secret (all services)
|
||||
|
||||
**Frontend** (in `.env.development`):
|
||||
- `VITE_API_URL`: Backend API base URL (default: http://localhost:6039)
|
||||
- `VITE_API_URL=http://localhost:8080` (Gateway URL)
|
||||
- `VITE_CLIENT_ID`: Client identifier for auth
|
||||
- `VITE_WEB_TITLE`: Page title
|
||||
|
||||
@@ -277,19 +280,15 @@ class ChatServiceTest {
|
||||
}
|
||||
```
|
||||
|
||||
Frontend uses Vitest for unit tests:
|
||||
```bash
|
||||
cd hzhub-admin
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker Image Building
|
||||
|
||||
Backend Dockerfile in `hzhub-ai/Dockerfile`:
|
||||
- Builds from Maven source
|
||||
- Runs on port 6039
|
||||
Backend Dockerfiles in each service directory:
|
||||
- Multi-stage build (Maven → JRE)
|
||||
- hzhub-ai: port 6039
|
||||
- hzhub-system: port 8083
|
||||
- hzhub-erp: port 8082
|
||||
|
||||
Frontend Dockerfiles in each portal directory:
|
||||
- Builds with pnpm
|
||||
@@ -299,27 +298,32 @@ Frontend Dockerfiles in each portal directory:
|
||||
|
||||
| Service | Port | Access |
|
||||
|---------|------|--------|
|
||||
| hzhub-admin (frontend) | 5666 | http://localhost:5666 |
|
||||
| hzhub-gateway | 8080 | http://localhost:8080 (统一入口) |
|
||||
| hzhub-ai | 6039 | http://localhost:6039 |
|
||||
| hzhub-system | 8083 | http://localhost:8083 |
|
||||
| hzhub-erp | 8082 | http://localhost:8082 |
|
||||
| hzhub-admin | 5666 | http://localhost:5666 |
|
||||
| hzhub-portal-employee | 5137 | http://localhost:5137 |
|
||||
| hzhub-portal-dealer | 5138 | http://localhost:5138 |
|
||||
| hzhub-ai (backend API) | 6039 | http://localhost:6039 |
|
||||
| MySQL | 3306 | localhost:3306 |
|
||||
| Redis | 6379 | localhost:6379 |
|
||||
| Weaviate | 28080 | http://localhost:28080 |
|
||||
| n8n | 5678 | http://localhost:5678 |
|
||||
| MinIO | 9000/9001 | http://localhost:9000 (API), http://localhost:9001 (Console) |
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Backend Module
|
||||
|
||||
1. Create module in `hzhub-ai/hzhub-modules/`
|
||||
2. Add to parent `pom.xml` modules list
|
||||
3. Include in `hzhub-admin` dependencies
|
||||
1. Decide which service it belongs to (AI, system, or ERP)
|
||||
2. For hzhub-ai: create module in `hzhub-ai/hzhub-modules/`, add to parent pom.xml
|
||||
3. For hzhub-system: create package under `src/main/java/org/hzhub/`, add dependencies to pom.xml
|
||||
4. Create controller, service, mapper following existing patterns
|
||||
5. Add gateway route in `hzhub-gateway/src/main/resources/application.yml`
|
||||
|
||||
### Adding a New API Endpoint
|
||||
|
||||
1. Create controller in module's `controller/` package
|
||||
1. Create controller in the appropriate service
|
||||
2. Use `@RestController` and `@RequestMapping`
|
||||
3. Return `R<T>` wrapper for responses
|
||||
4. Add permission check if needed: `@SaCheckPermission("system:user:list")`
|
||||
@@ -348,4 +352,5 @@ Weaviate is used for knowledge retrieval (RAG). Collection creation and querying
|
||||
- **Java version**: JDK 17+
|
||||
- **Node version**: Node 22+ with pnpm 10+
|
||||
- **Code style**: ESLint `@antfu/eslint-config` for frontend; backend follows Spring conventions
|
||||
- **Git hooks**: Lefthook configured (see `lefthook.yml`)
|
||||
- **Git hooks**: Lefthook configured (see `lefthook.yml`)
|
||||
- **Build order**: hzhub-ai → hzhub-system → hzhub-erp (hzhub-system depends on hzhub-ai common modules)
|
||||
|
||||
120
docs/erp-api-database-init-guide.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# ERP API 管理平台 - 数据库初始化指南
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 方式一:通过 Docker MySQL 容器执行(推荐)
|
||||
|
||||
1. **进入 MySQL 容器**:
|
||||
```bash
|
||||
docker exec -it hzhub-mysql mysql -u root -phzhub123
|
||||
```
|
||||
|
||||
2. **切换到 hzhub 数据库**:
|
||||
```sql
|
||||
USE hzhub;
|
||||
```
|
||||
|
||||
3. **执行 SQL 文件**:
|
||||
```sql
|
||||
source /data/hzhub/hzhub-erp/docs/sql/erp_api_tables.sql;
|
||||
```
|
||||
|
||||
或者直接复制粘贴 SQL 内容执行。
|
||||
|
||||
### 方式二:通过 Navicat/DBeaver 等工具执行
|
||||
|
||||
1. 连接到 MySQL 数据库:
|
||||
- Host: localhost 或 192.168.120.60
|
||||
- Port: 3306
|
||||
- Database: hzhub
|
||||
- Username: root
|
||||
- Password: hzhub123
|
||||
|
||||
2. 打开 SQL 文件:`/data/hzhub/hzhub-erp/docs/sql/erp_api_tables.sql`
|
||||
|
||||
3. 执行整个 SQL 文件
|
||||
|
||||
### 方式三:通过管理后台执行(如果支持)
|
||||
|
||||
某些系统管理后台提供 SQL 执行功能,可以直接粘贴 SQL 执行。
|
||||
|
||||
---
|
||||
|
||||
## SQL 文件内容说明
|
||||
|
||||
该 SQL 文件包含:
|
||||
|
||||
### 1. 数据库表创建
|
||||
|
||||
**erp_api_config**:API 配置主表
|
||||
- 存储 API 基本信息、SQL 模板、权限配置、缓存配置等
|
||||
- 包含版本字段(api_version)支持 v1/v2 版本管理
|
||||
|
||||
**erp_api_param**:API 参数配置表
|
||||
- 存储 API 参数定义(名称、类型、位置、是否必填等)
|
||||
- 通过外键关联到 erp_api_config,级联删除
|
||||
|
||||
**erp_api_stats**:API 调用统计表
|
||||
- 记录每次 API 调用(调用时间、参数、响应时间、状态、错误信息)
|
||||
- 用于监控统计和错误分析
|
||||
|
||||
### 2. 菜单配置
|
||||
|
||||
在 sys_menu 表中插入 ERP 管理相关菜单:
|
||||
- **ERP管理**(一级菜单)- 目录
|
||||
- **API配置**(二级菜单)- API 配置管理页面
|
||||
- API查询、API新增、API修改、API删除、API测试、清除缓存(按钮权限)
|
||||
- **API监控**(二级菜单)- API 调用统计监控页面
|
||||
|
||||
---
|
||||
|
||||
## 验证 SQL 执行结果
|
||||
|
||||
执行完成后,验证表是否创建成功:
|
||||
|
||||
```sql
|
||||
-- 查看表结构
|
||||
SHOW TABLES LIKE 'erp_api_%';
|
||||
|
||||
-- 查看表数量(应该有 3 张表)
|
||||
SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = 'hzhub' AND table_name LIKE 'erp_api_%';
|
||||
|
||||
-- 查看菜单是否插入成功
|
||||
SELECT menu_id, menu_name, parent_id, path, perms
|
||||
FROM sys_menu WHERE menu_name LIKE '%ERP%' OR menu_name LIKE '%API%';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重要说明
|
||||
|
||||
1. **表依赖顺序**:必须先创建 erp_api_config,再创建 erp_api_param(有外键约束)
|
||||
2. **菜单插入**:sys_menu 表在 hzhub-system 服务管理的数据库中,菜单 SQL 需要在同一数据库执行
|
||||
3. **权限配置**:菜单权限使用 `erp:api:*` 格式,后续需要在角色管理中分配权限
|
||||
4. **备份建议**:首次在生产环境执行前,建议先备份数据库
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
SQL 执行成功后:
|
||||
|
||||
1. **重启 ERP 服务**:
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-erp
|
||||
./restart.sh
|
||||
```
|
||||
|
||||
2. **验证服务启动**:
|
||||
```bash
|
||||
curl http://localhost:8082/actuator/health
|
||||
```
|
||||
|
||||
3. **测试 API 配置接口**(需要先登录管理后台获取 Token):
|
||||
```bash
|
||||
# 获取 Token 后,测试列表接口(会返回空列表)
|
||||
curl -H "Authorization: Bearer <token>" http://localhost:8080/erp/api/config/list
|
||||
```
|
||||
|
||||
4. **准备前端界面**:继续创建前端代码以使用管理界面操作
|
||||
168
docs/erp-api-frontend-complete.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# ERP API 管理平台 - 前端代码完成清单
|
||||
|
||||
## ✅ 已创建的前端文件
|
||||
|
||||
### API 定义文件(2个)
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/api/erp/api/index.ts` - API 函数定义(完整)
|
||||
- 包含所有接口函数:list、info、add、edit、remove、test、preview、stats、errorLog、cache等
|
||||
- 完整的 TypeScript 类型定义
|
||||
|
||||
### 数据定义文件(1个)
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/data.tsx` - 列定义和表单 Schema
|
||||
- 搜索表单 Schema
|
||||
- 表格列定义
|
||||
- 各类选项配置(API方法、版本、结果类型、参数类型等)
|
||||
|
||||
### 主页面组件(2个)
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/index.vue` - **API配置列表页** ⭐
|
||||
- 标准的 VxeGrid 表格页面
|
||||
- 搜索、分页、工具栏(新增、删除、从表导入)
|
||||
- 操作列(测试、文档、编辑、删除、清缓存)
|
||||
- 状态切换 Switch
|
||||
- 权限控制 v-access:code
|
||||
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/stats/index.vue` - API监控页(占位符)
|
||||
|
||||
### 弹窗组件(2个)
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/test-modal.vue` - **API测试弹窗** ⭐
|
||||
- 参数输入表单(动态生成)
|
||||
- 执行按钮
|
||||
- 结果展示(JSON格式化)
|
||||
- 错误信息详情
|
||||
- 执行时间统计
|
||||
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/doc-preview-modal.vue` - 文档预览弹窗
|
||||
- 多Tab展示(基本信息、参数说明、SQL模板、使用示例)
|
||||
|
||||
### 编辑页组件(3个)
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/edit-api.vue` - **编辑页主文件** ⭐
|
||||
- 两Tab设计(基础设置 + 参数配置)
|
||||
- 数据加载和保存逻辑
|
||||
- provide/inject 数据共享
|
||||
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/edit-tabs/basic-setting.vue` - 基础设置 Tab
|
||||
- VbenForm 表单组件
|
||||
- 所有配置字段(API名称、路径、SQL模板、权限、缓存等)
|
||||
- 动态显示字段(分页参数、权限标识、缓存配置)
|
||||
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/edit-tabs/params-config.vue` - 参数配置 Tab
|
||||
- VxeGrid inline-editable 表格
|
||||
- 新增、删除参数功能
|
||||
- 参数字段编辑(名称、类型、位置、必填、默认值等)
|
||||
|
||||
---
|
||||
|
||||
## 📊 文件统计
|
||||
|
||||
**总计**:**9个前端文件**
|
||||
|
||||
**核心功能页面**:
|
||||
- 1个列表页(index.vue)
|
||||
- 1个编辑页(edit-api.vue + 2个Tab组件)
|
||||
- 2个弹窗(test-modal、doc-preview-modal)
|
||||
- 1个监控页(占位符)
|
||||
|
||||
**代码特点**:
|
||||
- ✅ 使用 Vben Admin 标准组件和模式
|
||||
- ✅ 完整的 TypeScript 类型定义
|
||||
- ✅ 权限控制集成(v-access:code)
|
||||
- ✅ 响应式表单和表格
|
||||
- ✅ CodeMirror JSON 展示
|
||||
- ✅ inline-editable 参数配置表格
|
||||
|
||||
---
|
||||
|
||||
## 🚀 下一步操作
|
||||
|
||||
### 刷新页面测试
|
||||
|
||||
现在所有前端代码已创建完成,请:
|
||||
|
||||
1. **刷新管理后台页面**(Ctrl+F5 或 Cmd+Shift+R)
|
||||
2. **重新登录**(如果需要)
|
||||
3. **点击左侧菜单**:"ERP管理 > API配置"
|
||||
4. **应该看到**:
|
||||
- 空列表页(表格正常显示)
|
||||
- 工具栏有"新增"、"从表导入"、"批量删除"按钮
|
||||
|
||||
### 功能测试清单
|
||||
|
||||
✅ **列表页测试**:
|
||||
- 表格正常显示
|
||||
- 搜索表单可用
|
||||
- 新增按钮跳转到编辑页
|
||||
|
||||
✅ **新增测试**:
|
||||
- 点击"新增"进入编辑页
|
||||
- 两Tab切换正常
|
||||
- 基础设置表单正常
|
||||
- 参数配置表格可编辑
|
||||
|
||||
✅ **保存测试**:
|
||||
- 填写必填字段(API名称、路径、SQL模板)
|
||||
- 点击"保存配置"
|
||||
- 返回列表页,能看到新创建的配置
|
||||
|
||||
✅ **测试功能**:
|
||||
- 在列表中点击"测试"
|
||||
- 弹窗正常打开
|
||||
- 输入参数(或空)
|
||||
- 点击"执行"
|
||||
- 查看执行结果和执行时间
|
||||
|
||||
---
|
||||
|
||||
## 可能遇到的问题
|
||||
|
||||
### 问题1: 页面仍然404
|
||||
|
||||
**原因**:前端服务未重启或缓存未清除
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 重启前端服务
|
||||
cd /data/hzhub/hzhub-admin/apps/web-antd
|
||||
pnpm dev
|
||||
|
||||
# 或清除浏览器缓存并刷新
|
||||
```
|
||||
|
||||
### 问题2: 编译错误
|
||||
|
||||
**原因**:可能缺少某些类型定义
|
||||
|
||||
**解决**:查看编译错误信息,我会立即修复
|
||||
|
||||
### 问题3: 路径错误
|
||||
|
||||
**原因**:路由配置问题(菜单已配置,但路由可能需要调整)
|
||||
|
||||
**解决**:检查菜单中的 `component` 字段是否正确映射到组件路径
|
||||
|
||||
---
|
||||
|
||||
## 📝 菜单配置确认
|
||||
|
||||
请确认 sys_menu 表中的配置:
|
||||
|
||||
```sql
|
||||
-- ERP管理(一级菜单)
|
||||
menu_name='ERP管理', path='/erp', component='ParentView'
|
||||
|
||||
-- API配置(二级菜单)
|
||||
menu_name='API配置', path='api', component='erp/api/index'
|
||||
|
||||
-- API监控(二级菜单)
|
||||
menu_name='API监控', path='stats', component='erp/stats/index'
|
||||
```
|
||||
|
||||
`component` 字段应该映射到:
|
||||
- `erp/api/index` → `/views/erp/api/index.vue`
|
||||
- `erp/stats/index` → `/views/erp/stats/index.vue`
|
||||
|
||||
---
|
||||
|
||||
**请刷新页面并告诉我结果!**
|
||||
|
||||
如果页面正常显示,我们将进入完整功能测试阶段。
|
||||
如果遇到问题,请告诉我具体错误信息,我会立即协助解决。🔧
|
||||
743
docs/erp-api-management-plan.md
Normal file
@@ -0,0 +1,743 @@
|
||||
# ERP API 管理平台实现方案
|
||||
|
||||
## Context
|
||||
|
||||
**需求背景**:用户需要一个界面来查看和管理 ERP 数据库的定制 API 封装,并提供手动创建 API 的功能。当前 ERP 服务只有少量固定的 API(CustomerController),缺乏灵活的 API 管理能力。
|
||||
|
||||
**重要决策**:**完全集成到管理后台(hzhub-admin)中**,作为系统管理的一个新模块,实现统一管理:
|
||||
- 前端界面集成在 hzhub-admin 的标准管理界面中(左侧菜单 + 右侧功能页面)
|
||||
- 通过后端菜单系统动态添加菜单项(类似"系统管理"、"工具"等模块)
|
||||
- 使用统一的权限体系(通过 Sa-Token 和 permission_code)
|
||||
- 使用统一的认证体系(JWT Token 共享)
|
||||
- 与现有的系统管理、代码生成器等模块保持一致的界面风格和操作体验
|
||||
|
||||
**设计目标**:
|
||||
1. 查看 ERP 数据库中的所有表结构
|
||||
2. 从数据库表一键生成 API 配置
|
||||
3. 手动创建和编辑 API(配置 SQL、参数、权限等)
|
||||
4. API 测试功能(在线测试执行)
|
||||
5. API 文档预览
|
||||
6. API 调用统计和监控(完整功能)
|
||||
7. Redis 缓存支持(完整功能)
|
||||
8. 版本管理支持(完整功能)
|
||||
|
||||
**参考架构**:
|
||||
- 基于代码生成器(gen_table + gen_table_column)的两表元数据管理模式
|
||||
- 使用 JdbcTemplate 执行动态 SQL(安全、简单)
|
||||
- 参考 Vben Admin 的标准 CRUD 界面模式
|
||||
|
||||
---
|
||||
|
||||
## 一、架构设计
|
||||
|
||||
### 1.1 数据库表设计
|
||||
|
||||
创建两张元数据表存储 API 配置:
|
||||
|
||||
**erp_api_config(API 配置主表)**:
|
||||
|
||||
```sql
|
||||
CREATE TABLE erp_api_config (
|
||||
api_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'API ID',
|
||||
api_name VARCHAR(100) NOT NULL COMMENT 'API名称',
|
||||
api_path VARCHAR(200) NOT NULL COMMENT 'API路径(如 /erp/dynamic/customer/list)',
|
||||
api_method VARCHAR(10) NOT NULL DEFAULT 'GET' COMMENT 'HTTP方法(GET/POST)',
|
||||
api_desc VARCHAR(500) COMMENT 'API描述',
|
||||
api_version VARCHAR(10) DEFAULT 'v1' COMMENT 'API版本号(v1/v2)',
|
||||
|
||||
-- 数据源配置
|
||||
data_source VARCHAR(50) DEFAULT 'erp' COMMENT '数据源名称',
|
||||
|
||||
-- SQL配置
|
||||
sql_template TEXT NOT NULL COMMENT 'SQL模板(支持参数占位符 #{paramName})',
|
||||
result_type VARCHAR(20) NOT NULL DEFAULT 'LIST' COMMENT '结果类型(LIST/SINGLE/COUNT)',
|
||||
|
||||
-- 分页配置
|
||||
support_pagination TINYINT(1) DEFAULT 0 COMMENT '是否支持分页',
|
||||
page_param_name VARCHAR(50) DEFAULT 'pageNum' COMMENT '页码参数名',
|
||||
size_param_name VARCHAR(50) DEFAULT 'pageSize' COMMENT '页大小参数名',
|
||||
|
||||
-- 权限配置
|
||||
require_auth TINYINT(1) DEFAULT 0 COMMENT '是否需要认证',
|
||||
permission_code VARCHAR(100) COMMENT '权限标识(如 erp:customer:list)',
|
||||
|
||||
-- 缓存配置
|
||||
enable_cache TINYINT(1) DEFAULT 0 COMMENT '是否启用缓存',
|
||||
cache_key_template VARCHAR(200) COMMENT '缓存键模板(支持参数占位符)',
|
||||
cache_ttl INT DEFAULT 300 COMMENT '缓存过期时间(秒)',
|
||||
|
||||
-- 来源表信息
|
||||
source_table VARCHAR(100) COMMENT '来源表名',
|
||||
source_table_comment VARCHAR(500) COMMENT '来源表描述',
|
||||
|
||||
-- 状态
|
||||
status TINYINT(1) DEFAULT 1 COMMENT '状态(0禁用 1启用)',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
remark VARCHAR(500)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ERP动态API配置表';
|
||||
|
||||
CREATE INDEX idx_api_path_method ON erp_api_config(api_path, api_method, api_version);
|
||||
CREATE INDEX idx_status ON erp_api_config(status);
|
||||
```
|
||||
|
||||
**erp_api_param(API 参数配置表)**:
|
||||
|
||||
```sql
|
||||
CREATE TABLE erp_api_param (
|
||||
param_id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
api_id BIGINT NOT NULL COMMENT '所属API ID',
|
||||
|
||||
-- 参数基本信息
|
||||
param_name VARCHAR(100) NOT NULL COMMENT '参数名称',
|
||||
param_desc VARCHAR(500) COMMENT '参数描述',
|
||||
param_type VARCHAR(20) NOT NULL DEFAULT 'String' COMMENT '参数类型(String/Integer/Long/Date/Boolean)',
|
||||
|
||||
-- 参数位置
|
||||
param_position VARCHAR(20) NOT NULL DEFAULT 'QUERY' COMMENT '参数位置(QUERY/BODY)',
|
||||
|
||||
-- 参数验证
|
||||
is_required TINYINT(1) DEFAULT 0 COMMENT '是否必填',
|
||||
default_value VARCHAR(200) COMMENT '默认值',
|
||||
|
||||
-- SQL映射
|
||||
sql_param_name VARCHAR(100) COMMENT 'SQL参数名',
|
||||
|
||||
-- 排序
|
||||
sort INT DEFAULT 0,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ERP动态API参数配置表';
|
||||
|
||||
CREATE INDEX idx_api_id ON erp_api_param(api_id);
|
||||
ALTER TABLE erp_api_param ADD CONSTRAINT fk_api_param_api
|
||||
FOREIGN KEY (api_id) REFERENCES erp_api_config(api_id) ON DELETE CASCADE;
|
||||
```
|
||||
|
||||
**erp_api_stats(API 调用统计表)**:
|
||||
|
||||
```sql
|
||||
CREATE TABLE erp_api_stats (
|
||||
stats_id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
api_id BIGINT NOT NULL COMMENT 'API ID',
|
||||
|
||||
-- 调用信息
|
||||
call_time DATETIME NOT NULL COMMENT '调用时间',
|
||||
call_params TEXT COMMENT '调用参数(JSON)',
|
||||
response_time INT COMMENT '响应时间(ms)',
|
||||
call_status VARCHAR(10) COMMENT '调用状态(SUCCESS/ERROR)',
|
||||
|
||||
-- 错误信息
|
||||
error_message TEXT COMMENT '错误消息',
|
||||
error_stack TEXT COMMENT '错误堆栈',
|
||||
|
||||
-- 客户端信息
|
||||
client_ip VARCHAR(50) COMMENT '客户端IP',
|
||||
user_id VARCHAR(50) COMMENT '用户ID',
|
||||
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ERP动态API调用统计表';
|
||||
|
||||
CREATE INDEX idx_api_id_time ON erp_api_stats(api_id, call_time);
|
||||
CREATE INDEX idx_call_status ON erp_api_stats(call_status);
|
||||
```
|
||||
|
||||
### 1.2 核心技术选型
|
||||
|
||||
**动态 SQL 执行**:JdbcTemplate
|
||||
- 优点:简单、安全、支持 PreparedStatement 参数绑定
|
||||
- 避免复杂的 MyBatis 动态 SQL(不适合动态生成场景)
|
||||
|
||||
**参数类型转换**:自定义 ParamTypeConverter
|
||||
- 支持 String、Integer、Long、Date、Boolean 类型
|
||||
- 自动转换前端参数到 SQL 参数类型
|
||||
|
||||
**结果映射**:动态 RowMapper → Map<String, Object>
|
||||
- 避免硬编码 VO 类,灵活适配不同表结构
|
||||
|
||||
**安全防护**:
|
||||
- SQL 注入防护:强制 PreparedStatement 参数绑定(#{param} → ?)
|
||||
- SQL 关键字白名单:只允许 SELECT、FROM、WHERE、AND、OR、ORDER、BY 等安全关键字
|
||||
- 禁止危险操作:DROP、DELETE、TRUNCATE、ALTER、CREATE 等
|
||||
- 可选权限控制:通过 Sa-Token 的 permission_code 验证
|
||||
|
||||
### 1.3 后端架构
|
||||
|
||||
**文件结构**:
|
||||
|
||||
```
|
||||
hzhub-erp/src/main/java/org/hzhub/erp/
|
||||
├── controller/
|
||||
│ ├── ErpApiController.java # API 配置管理 CRUD
|
||||
│ └── DynamicApiController.java # 动态 API 执行入口(通配符路由)
|
||||
├── service/
|
||||
│ ├── IErpApiService.java # API 配置 Service 接口
|
||||
│ ├── impl/
|
||||
│ │ ├── ErpApiServiceImpl.java # API 配置管理 + 从表导入
|
||||
│ │ ├── DynamicApiExecutor.java # 核心 SQL 执行引擎
|
||||
│ │ ├── ApiCacheService.java # Redis 缓存服务
|
||||
│ │ └── ApiMonitorService.java # 监控统计服务
|
||||
│ └── ParamTypeConverter.java # 参数类型转换器
|
||||
├── domain/
|
||||
│ ├── ErpApiConfig.java # API 配置实体
|
||||
│ ├── ErpApiParam.java # API 参数实体
|
||||
│ └── vo/
|
||||
│ │ ├── ErpApiConfigVO.java # API 配置 VO
|
||||
│ │ └── ApiTestResultVO.java # API 测试结果 VO
|
||||
├── mapper/
|
||||
│ ├── ErpApiConfigMapper.java # API 配置 Mapper
|
||||
│ ├── ErpApiParamMapper.java # API 参数 Mapper
|
||||
│ └── ErpApiStatsMapper.java # API 统计 Mapper
|
||||
└── util/
|
||||
└── SqlValidator.java # SQL 安全验证工具
|
||||
```
|
||||
|
||||
**关键模块设计**:
|
||||
|
||||
1. **DynamicApiExecutor(核心执行引擎)**:
|
||||
- 处理 SQL 模板:将 #{param} 转换为 ? 占位符
|
||||
- 参数绑定:按照参数顺序提取值并绑定到 PreparedStatement
|
||||
- 分页处理:自动添加 OFFSET FETCH(SQL Server 语法)
|
||||
- 结果封装:LIST → TableDataInfo,SINGLE → R,COUNT → R<Long>
|
||||
- 缓存检查:检查 Redis 缓存,如果存在直接返回
|
||||
- 统计记录:记录执行时间、调用参数到 erp_api_stats
|
||||
|
||||
2. **ApiCacheService(缓存服务)**:
|
||||
- 缓存键生成:根据 cache_key_template 和参数生成缓存键
|
||||
- 缓存操作:get、set、delete、clear
|
||||
- 缓存过期处理:支持 TTL 配置
|
||||
- 缓存命中率统计:记录缓存命中和未命中次数
|
||||
|
||||
3. **ApiMonitorService(监控服务)**:
|
||||
- 调用记录:记录每次调用到 erp_api_stats
|
||||
- 统计查询:按 API ID、时间段查询调用统计
|
||||
- 错误分析:查询错误日志、错误率统计
|
||||
- 性能分析:响应时间趋势、慢查询告警
|
||||
|
||||
4. **ErpApiController(配置管理)**:
|
||||
- CRUD 接口:list、getInfo、add、edit、remove
|
||||
- 从表导入:importFromTable(调用 ErpExploreController 获取表结构)
|
||||
- API 测试:testApi(执行并返回结果 + 执行时间)
|
||||
- 状态切换:changeStatus
|
||||
- 统计查询:getApiStats、getApiErrorLog
|
||||
- 版本管理:支持 v1/v2 版本号,路径自动包含版本(/erp/dynamic/v1/xxx)
|
||||
|
||||
5. **DynamicApiController(动态路由)**:
|
||||
- 通配符路径:`@GetMapping("/erp/dynamic/{version}/{apiPath}")`
|
||||
- 根据请求路径、版本和方法查询 API 配置
|
||||
- 权限检查(如果 require_auth = 1)
|
||||
- 参数验证和类型转换
|
||||
- 缓存检查(如果 enable_cache = 1)
|
||||
- 调用 DynamicApiExecutor 执行
|
||||
- 记录统计信息
|
||||
|
||||
---
|
||||
|
||||
## 二、功能模块详细设计
|
||||
|
||||
### 2.1 API 配置管理(CRUD)
|
||||
|
||||
**后端接口**:
|
||||
|
||||
| Method | Path | 功能 |
|
||||
|--------|------|------|
|
||||
| GET | `/erp/api/config/list` | 分页查询 API 配置列表 |
|
||||
| GET | `/erp/api/config/{apiId}` | 获取配置详情(含参数列表) |
|
||||
| POST | `/erp/api/config` | 新增 API 配置 |
|
||||
| PUT | `/erp/api/config` | 修改 API 配置 |
|
||||
| DELETE | `/erp/api/config/{apiIds}` | 删除配置 |
|
||||
| PUT | `/erp/api/config/changeStatus` | 启用/禁用 |
|
||||
| POST | `/erp/api/config/importFromTable` | 从表导入 |
|
||||
| GET | `/erp/api/config/syncTable/{apiId}` | 同步表结构 |
|
||||
| POST | `/erp/api/config/test/{apiId}` | API 测试 |
|
||||
| GET | `/erp/api/config/preview/{apiId}` | API 文档预览 |
|
||||
| GET | `/erp/api/config/stats/{apiId}` | 查询调用统计 |
|
||||
| GET | `/erp/api/config/errorLog/{apiId}` | 查询错误日志 |
|
||||
| DELETE | `/erp/api/config/cache/{apiId}` | 清除缓存 |
|
||||
|
||||
### 2.2 从数据库表导入
|
||||
|
||||
**流程**:
|
||||
1. 前端调用 ErpExploreController 获取表列表(`/erp/test/explore`)
|
||||
2. 用户选择要导入的表(多选)
|
||||
3. 后端 `importFromTable` 方法:
|
||||
- 查询表的列信息(调用 `/erp/test/explore/table?tableName=xxx`)
|
||||
- 生成默认查询 SQL:`SELECT * FROM tableName WHERE 1=1`
|
||||
- 为每个列创建参数配置(paramName、paramType、paramDesc)
|
||||
- 保存到 erp_api_config 和 erp_api_param
|
||||
|
||||
**自动生成逻辑**:
|
||||
- API 名称:`{tableName}列表查询`
|
||||
- API 路径:`/erp/dynamic/{tableName}`
|
||||
- SQL 类型:根据列数据类型推断 Java 类型(varchar → String, int → Integer, datetime → Date)
|
||||
- 支持分页:默认启用
|
||||
|
||||
### 2.3 API 测试功能
|
||||
|
||||
**设计**:
|
||||
- 输入:apiId + testParams(Map<String, Object>)
|
||||
- 输出:ApiTestResultVO(包含执行结果、执行时间、SQL、错误信息)
|
||||
|
||||
**ApiTestResultVO 结构**:
|
||||
```java
|
||||
public class ApiTestResultVO {
|
||||
private String apiPath;
|
||||
private String testMethod;
|
||||
private Map<String, Object> requestParams;
|
||||
private Boolean success;
|
||||
private Object data; // 执行结果
|
||||
private Long executionTime; // 执行时间(ms)
|
||||
private String executedSql; // 实际执行的 SQL
|
||||
private String errorMessage; // 错误信息(如果失败)
|
||||
private String errorStack; // 错误堆栈
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 API 文档预览
|
||||
|
||||
**生成内容**:
|
||||
- 基本信息:API 名称、路径、方法、描述
|
||||
- 参数说明表:参数名、类型、必填、默认值、描述
|
||||
- SQL 模板说明
|
||||
- 响应格式示例
|
||||
- 使用示例(请求 URL + 参数示例)
|
||||
|
||||
---
|
||||
|
||||
## 三、前端界面设计
|
||||
|
||||
### 3.1 文件结构
|
||||
|
||||
```
|
||||
hzhub-admin/apps/web-antd/src/
|
||||
├── api/erp/
|
||||
│ ├── api/
|
||||
│ │ ├── index.ts # API 函数定义
|
||||
│ │ └── model.d.ts # TypeScript 类型
|
||||
│ └── explore/
|
||||
│ │ ├── index.ts # 数据库探查 API
|
||||
│ │ └── model.d.ts
|
||||
├── views/erp/api/
|
||||
│ ├── index.vue # API 配置列表页
|
||||
│ ├── data.tsx # 列定义、搜索表单 schema
|
||||
│ ├── edit-api.vue # 编辑页(两 Tab)
|
||||
│ ├── edit-tabs/
|
||||
│ │ ├── basic-setting.vue # 基础设置 Tab
|
||||
│ │ └── params-config.vue # 参数配置 Tab(VxeGrid inline-editable)
|
||||
│ ├── table-import-modal.vue # 从表导入弹窗
|
||||
│ ├── test-modal.vue # API 测试弹窗
|
||||
│ ├── doc-preview-modal.vue # 文档预览弹窗
|
||||
│ └── stats-modal.vue # 统计监控弹窗
|
||||
```
|
||||
|
||||
### 3.2 列表页(index.vue)
|
||||
|
||||
**参考**:tenant/index.vue(完整 CRUD)
|
||||
|
||||
**功能**:
|
||||
- VxeGrid 表格展示 API 配置(apiName、apiPath、apiMethod、status、createTime)
|
||||
- 搜索表单:apiName、apiPath、status
|
||||
- 操作列:测试、编辑、删除、启用/禁用、查看统计
|
||||
- 工具栏:从表导入、新增 API
|
||||
|
||||
### 3.3 编辑页(edit-api.vue)
|
||||
|
||||
**参考**:generator/edit-gen.vue(两 Tab 设计)
|
||||
|
||||
**Tab 1 - 基础设置**:
|
||||
- API 名称、路径、方法(下拉选择 GET/POST)
|
||||
- SQL 模板(使用 CodeMirror 编辑器,支持 SQL Server 语法高亮)
|
||||
- 数据源(下拉选择)
|
||||
- 结果类型(LIST/SINGLE/COUNT)
|
||||
- 分页配置(是否支持分页、参数名)
|
||||
- 权限配置(是否需要认证、权限标识)
|
||||
- 缓存配置(是否启用缓存、缓存键模板、TTL)
|
||||
- 版本号(下拉选择 v1/v2)
|
||||
- 来源表信息(只读,显示来源表名和描述)
|
||||
|
||||
**Tab 2 - 参数配置**:
|
||||
- VxeGrid inline-editable 表格
|
||||
- 列:paramName、paramType(下拉)、paramPosition(下拉)、isRequired(复选框)、defaultValue、paramDesc
|
||||
- 工具栏:新增参数、删除参数、同步表结构
|
||||
|
||||
### 3.4 从表导入弹窗(table-import-modal.vue)
|
||||
|
||||
**参考**:generator/table-import-modal.vue
|
||||
|
||||
**功能**:
|
||||
- 调用 `/erp/test/explore` 获取表列表
|
||||
- VxeGrid 表格展示:tableName、schemaName、rowCount、columnCount、hasPrimaryKey
|
||||
- 多选框选择要导入的表
|
||||
- 确认导入:调用 `/erp/api/config/importFromTable`
|
||||
|
||||
### 3.5 API 测试弹窗(test-modal.vue)
|
||||
|
||||
**设计**:
|
||||
- 上半部分:参数输入表单(根据参数配置动态生成)
|
||||
- 下半部分:执行结果展示
|
||||
- 元信息:执行时间、状态(成功/失败)
|
||||
- 结果数据:JSON 格式化展示(使用 CodeMirror)
|
||||
- 错误信息:如果失败,显示错误消息和堆栈
|
||||
|
||||
### 3.6 文档预览弹窗(doc-preview-modal.vue)
|
||||
|
||||
**参考**:generator/code-preview-modal.vue(多 Tab 展示)
|
||||
|
||||
**Tab 内容**:
|
||||
- API 说明:名称、路径、方法、描述
|
||||
- 参数说明:表格形式展示参数列表
|
||||
- SQL 模板:代码高亮显示
|
||||
- 响应示例:JSON 示例
|
||||
- 使用示例:完整的请求 URL 和参数示例
|
||||
|
||||
### 3.7 统计监控弹窗(stats-modal.vue)
|
||||
|
||||
**设计**:
|
||||
- 上部分:统计概览(总调用次数、平均响应时间、错误率)
|
||||
- 中部分:调用次数趋势图(使用 Charts)
|
||||
- 下部分:响应时间趋势图
|
||||
- 错误日志列表:最近的错误调用记录
|
||||
|
||||
---
|
||||
|
||||
## 四、SQL 模板设计
|
||||
|
||||
### 4.1 参数占位符语法
|
||||
|
||||
使用 `#{paramName}` 作为参数占位符,自动转换为 PreparedStatement 的 `?`:
|
||||
|
||||
**示例 SQL 模板**:
|
||||
|
||||
```sql
|
||||
-- 简单查询
|
||||
SELECT * FROM customer
|
||||
WHERE #{customerCode} IS NOT NULL THEN customer_code = #{customerCode}
|
||||
AND #{companyCode} IS NOT NULL THEN company_code = #{companyCode}
|
||||
|
||||
-- 带分页查询
|
||||
SELECT customer_code, customer_name, company_code
|
||||
FROM customer
|
||||
WHERE #{keyword} IS NOT NULL THEN customer_name LIKE '%#{keyword}%'
|
||||
ORDER BY customer_code
|
||||
-- 分页参数自动添加:OFFSET #{pageNum} ROWS FETCH NEXT #{pageSize} ROWS ONLY
|
||||
|
||||
-- 聚合查询
|
||||
SELECT COUNT(*) AS total
|
||||
FROM customer
|
||||
WHERE #{salesAreaCode} IS NOT NULL THEN sales_area_code = #{salesAreaCode}
|
||||
```
|
||||
|
||||
### 4.2 SQL 安全验证
|
||||
|
||||
**白名单关键字**:
|
||||
```java
|
||||
Set<String> ALLOWED_KEYWORDS = Set.of(
|
||||
"SELECT", "FROM", "WHERE", "AND", "OR", "ORDER", "BY", "GROUP", "HAVING",
|
||||
"OFFSET", "FETCH", "NEXT", "ROWS", "ONLY", "AS", "COUNT", "SUM", "AVG"
|
||||
);
|
||||
```
|
||||
|
||||
**禁止关键字**:
|
||||
```java
|
||||
Set<String> DANGEROUS_KEYWORDS = Set.of(
|
||||
"DROP", "DELETE", "TRUNCATE", "ALTER", "CREATE", "GRANT", "INSERT", "UPDATE"
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、安全考虑
|
||||
|
||||
### 5.1 SQL 注入防护
|
||||
|
||||
1. **强制参数绑定**:禁止拼接 SQL,必须使用 PreparedStatement
|
||||
2. **关键字验证**:执行前验证 SQL 只包含白名单关键字
|
||||
3. **参数值验证**:基于配置的正则表达式验证参数格式
|
||||
|
||||
### 5.2 权限控制
|
||||
|
||||
1. **可选认证**:通过 `require_auth` 字段控制
|
||||
2. **权限标识**:通过 `permission_code` 配置(如 `erp:customer:list`)
|
||||
3. **Sa-Token 验证**:
|
||||
```java
|
||||
if (config.getRequireAuth() == 1) {
|
||||
StpUtil.checkPermission(config.getPermissionCode());
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 访问控制
|
||||
|
||||
- 所有动态 API 默认需要通过网关验证(`X-Gateway-Verified` header)
|
||||
- 可选启用 Sa-Token 权限检查
|
||||
- 支持 RateLimiter 限流(后续添加)
|
||||
|
||||
---
|
||||
|
||||
## 六、实现步骤(分阶段)
|
||||
|
||||
根据用户确认的设计决策:
|
||||
- **路由机制**:使用通配符路由(`/erp/dynamic/{version}/{apiPath}`)
|
||||
- **SQL 编辑器**:基础版本(CodeMirror + 语法高亮)
|
||||
- **高级功能**:第一版实现完整功能(缓存、监控、版本管理)
|
||||
|
||||
### Phase 1:核心功能 + 基础增强(第 1-2 周)
|
||||
|
||||
**目标**:实现最小可用版本 + 基础高级功能
|
||||
|
||||
1. **数据库表创建**(hzhub-erp):
|
||||
- 创建 erp_api_config 和 erp_api_param 表(包含缓存、版本字段)
|
||||
- 创建 erp_api_stats 表(监控统计)
|
||||
- 添加索引和外键约束
|
||||
|
||||
2. **后端基础架构**:
|
||||
- 创建 Domain 实体类(ErpApiConfig、ErpApiParam、ErpApiStats)
|
||||
- 创建 Mapper 接口和 XML(基础 CRUD)
|
||||
- 创建 Service 接口和实现(ErpApiServiceImpl)
|
||||
- 创建 DynamicApiExecutor(核心 SQL 执行引擎)
|
||||
- 创建 ParamTypeConverter(参数类型转换)
|
||||
- 创建 ApiCacheService(Redis 缓存服务)
|
||||
- 创建 ApiMonitorService(调用统计服务)
|
||||
|
||||
3. **Controller 实现**:
|
||||
- ErpApiController(CRUD 接口 + 缓存配置 + 版本管理)
|
||||
- DynamicApiController(通配符路由 + 缓存检查 + 统计记录)
|
||||
|
||||
4. **前端基础页面**:
|
||||
- 列表页(index.vue)
|
||||
- 基础的增删改查功能
|
||||
- CodeMirror SQL 编辑器(语法高亮)
|
||||
|
||||
### Phase 2:导入和编辑功能 + 版本管理(第 3 周)
|
||||
|
||||
**目标**:完善配置管理功能 + 版本支持
|
||||
|
||||
1. **从表导入**:
|
||||
- Service:importFromTable 方法
|
||||
- SQL 模板自动生成
|
||||
- 参数自动生成
|
||||
|
||||
2. **编辑页**:
|
||||
- 两 Tab 设计(基础设置 + 参数配置)
|
||||
- VxeGrid inline-editable 参数编辑
|
||||
- SQL 编辑器(CodeMirror + SQL Server 语法高亮)
|
||||
- 版本管理字段(api_version,支持 v1/v2)
|
||||
|
||||
3. **前端弹窗**:
|
||||
- 从表导入弹窗
|
||||
- 基础设置表单(包含缓存配置)
|
||||
- 版本选择下拉框
|
||||
|
||||
### Phase 3:测试和文档 + 监控统计(第 4 周)
|
||||
|
||||
**目标**:增强可用性 + 监控功能
|
||||
|
||||
1. **API 测试功能**:
|
||||
- Service:testApi 方法
|
||||
- 执行日志和错误处理
|
||||
- 前端测试弹窗
|
||||
|
||||
2. **API 文档预览**:
|
||||
- Service:generateApiDoc 方法
|
||||
- 前端文档预览弹窗
|
||||
|
||||
3. **监控统计功能**:
|
||||
- 创建 erp_api_stats 表(记录调用次数、响应时间、错误率)
|
||||
- ApiMonitorService:记录每次调用(调用时间、参数、响应时间)
|
||||
- 统计接口:getApiStats、getApiErrorLog
|
||||
- 前端统计页面:调用次数图表、响应时间趋势
|
||||
|
||||
4. **优化体验**:
|
||||
- JSON 格式化展示
|
||||
- 执行时间统计
|
||||
- 调用统计可视化
|
||||
|
||||
### Phase 4:安全增强和稳定性(第 5 周)
|
||||
|
||||
**目标**:提升安全性和稳定性
|
||||
|
||||
1. **安全增强**:
|
||||
- SQL 安全验证(SqlValidator)
|
||||
- 权限控制集成
|
||||
- 参数验证
|
||||
|
||||
2. **缓存优化**:
|
||||
- Redis 缓存键生成策略
|
||||
- 缓存过期处理(TTL)
|
||||
- 缓存命中率统计
|
||||
|
||||
3. **同步表结构**:
|
||||
- syncTableStructure 方法
|
||||
|
||||
4. **状态管理**:
|
||||
- 启用/禁用功能
|
||||
- API 状态验证
|
||||
|
||||
5. **监控告警**:
|
||||
- 响应时间阈值告警
|
||||
- 错误率告警
|
||||
- 调用异常记录
|
||||
|
||||
---
|
||||
|
||||
## 七、关键文件清单
|
||||
|
||||
### 后端关键文件(需创建)
|
||||
|
||||
1. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/service/impl/DynamicApiExecutor.java` - 核心 SQL 执行引擎
|
||||
2. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/service/impl/ApiCacheService.java` - Redis 缓存服务
|
||||
3. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/service/impl/ApiMonitorService.java` - 监控统计服务
|
||||
4. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/controller/DynamicApiController.java` - 动态路由入口
|
||||
5. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/service/impl/ErpApiServiceImpl.java` - API 配置管理核心业务
|
||||
6. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/controller/ErpApiController.java` - API 配置管理 Controller
|
||||
7. `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/util/SqlValidator.java` - SQL 安全验证工具
|
||||
|
||||
### 前端关键文件(需创建)
|
||||
|
||||
1. `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/index.vue` - API 配置列表页
|
||||
2. `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/edit-api.vue` - 编辑页(两 Tab)
|
||||
3. `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/test-modal.vue` - API 测试弹窗
|
||||
4. `/data/hzhub/hzhub-admin/apps/web-antd/src/views/erp/api/stats-modal.vue` - 统计监控弹窗
|
||||
5. `/data/hzhub/hzhub-admin/apps/web-antd/src/api/erp/api/index.ts` - API 函数定义
|
||||
|
||||
### 需复用的现有文件
|
||||
|
||||
- `/data/hzhub/hzhub-erp/src/main/java/org/hzhub/erp/controller/ErpExploreController.java` - 数据库探查功能
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/tool/gen/edit-gen.vue` - 编辑页设计参考
|
||||
- `/data/hzhub/hzhub-admin/apps/web-antd/src/views/system/tenant/index.vue` - CRUD 页面参考
|
||||
|
||||
---
|
||||
|
||||
## 八、验证方式
|
||||
|
||||
### 8.1 后端测试
|
||||
|
||||
1. **单元测试**:
|
||||
- DynamicApiExecutor SQL 执行测试
|
||||
- ParamTypeConverter 类型转换测试
|
||||
- SqlValidator 安全验证测试
|
||||
|
||||
2. **集成测试**:
|
||||
- 从表导入流程测试
|
||||
- API 执行流程测试(完整流程)
|
||||
- 权限控制测试
|
||||
|
||||
3. **手动测试**:
|
||||
- 启动 hzhub-erp 服务
|
||||
- 访问 Actuator 健康检查:`curl http://localhost:8082/actuator/health`
|
||||
- 测试动态 API:`curl http://localhost:8082/erp/dynamic/v1/customer/list?pageNum=1&pageSize=10`
|
||||
|
||||
### 8.2 前端测试
|
||||
|
||||
1. **页面功能测试**:
|
||||
- 启动前端:`cd hzhub-admin && pnpm dev`
|
||||
- 访问 API 管理页面:`http://localhost:5666/#/erp/api`
|
||||
- 测试列表查询、新增、编辑、删除功能
|
||||
- 测试从表导入功能
|
||||
- 测试 API 测试功能
|
||||
|
||||
2. **API 执行测试**:
|
||||
- 创建一个测试 API
|
||||
- 输入参数执行测试
|
||||
- 查看执行结果和执行时间
|
||||
- 测试错误场景(错误的 SQL、缺少参数)
|
||||
|
||||
### 8.3 端到端测试
|
||||
|
||||
1. 通过网关访问动态 API:
|
||||
- 先登录获取 Token
|
||||
- 访问:`http://localhost:8080/erp/dynamic/v1/customer/list?pageNum=1&pageSize=10`
|
||||
- 验证返回数据格式和分页信息
|
||||
|
||||
2. 权限测试:
|
||||
- 创建一个需要权限的 API(require_auth=1, permission_code='erp:test:list')
|
||||
- 未登录访问应该返回 401
|
||||
- 无权限用户访问应该返回 403
|
||||
|
||||
---
|
||||
|
||||
## 九、用户确认的设计决策
|
||||
|
||||
根据用户反馈,最终确定的设计决策:
|
||||
|
||||
1. **动态路由机制**:使用通配符路由(`/erp/dynamic/{version}/{apiPath}`)
|
||||
- 简单稳定,所有动态 API 共享统一前缀
|
||||
- 支持版本号(v1/v2),路径自动包含版本
|
||||
|
||||
2. **SQL 编辑器**:基础版本(CodeMirror + SQL Server 语法高亮)
|
||||
- 第一版使用代码编辑器 + 语法高亮
|
||||
- 快速实现核心功能,后续可扩展可视化构建器
|
||||
|
||||
3. **高级功能**:第一版实现完整功能
|
||||
- Redis 缓存:支持缓存配置、缓存键模板、TTL、缓存清除
|
||||
- API 监控:调用统计、响应时间记录、错误日志
|
||||
- 版本管理:支持 v1/v2 版本号,可同时运行多版本 API
|
||||
- 统计可视化:调用次数图表、响应时间趋势、错误率统计
|
||||
|
||||
这些决策已整合到实现步骤和数据库表设计中。
|
||||
|
||||
---
|
||||
|
||||
## 十、集成到管理后台的具体方案
|
||||
|
||||
### 10.1 菜单配置
|
||||
|
||||
需要在 hzhub-system 的菜单表中添加 ERP 管理菜单(通过 SQL 插入):
|
||||
|
||||
```sql
|
||||
-- ERP 管理菜单(一级菜单)
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
VALUES ('ERP管理', 0, 5, '/erp', NULL, 1, 0, 'M', '0', '0', '', 'monitor', 'admin', NOW(), 'ERP API管理目录');
|
||||
|
||||
-- API 配置管理菜单(二级菜单)
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
VALUES ('API配置', (SELECT menu_id FROM sys_menu WHERE menu_name='ERP管理'), 1, 'api', 'erp/api/index', 1, 0, 'C', '0', '0', 'erp:api:list', 'tool', 'admin', NOW(), 'API配置管理菜单');
|
||||
|
||||
-- API 配置按钮权限
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES ('API查询', (SELECT menu_id FROM sys_menu WHERE menu_name='API配置'), 1, '', NULL, 1, 0, 'F', '0', '0', 'erp:api:query', '#', 'admin', NOW());
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES ('API新增', (SELECT menu_id FROM sys_menu WHERE menu_name='API配置'), 2, '', NULL, 1, 0, 'F', '0', '0', 'erp:api:add', '#', 'admin', NOW());
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES ('API修改', (SELECT menu_id FROM sys_menu WHERE menu_name='API配置'), 3, '', NULL, 1, 0, 'F', '0', '0', 'erp:api:edit', '#', 'admin', NOW());
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES ('API删除', (SELECT menu_id FROM sys_menu WHERE menu_name='API配置'), 4, '', NULL, 1, 0, 'F', '0', '0', 'erp:api:remove', '#', 'admin', NOW());
|
||||
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||
VALUES ('API测试', (SELECT menu_id FROM sys_menu WHERE menu_name='API配置'), 5, '', NULL, 1, 0, 'F', '0', '0', 'erp:api:test', '#', 'admin', NOW());
|
||||
|
||||
-- API 监控统计菜单(二级菜单)
|
||||
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
|
||||
VALUES ('API监控', (SELECT menu_id FROM sys_menu WHERE menu_name='ERP管理'), 2, 'stats', 'erp/stats/index', 1, 0, 'C', '0', '0', 'erp:api:stats', 'monitor', 'admin', NOW(), 'API调用统计监控');
|
||||
```
|
||||
|
||||
### 10.2 权限体系集成
|
||||
|
||||
- 所有 API 配置管理接口都需要权限验证(通过 Sa-Token)
|
||||
- 前端按钮使用 `v-access:code="['erp:api:add']"` 控制权限
|
||||
- 动态 API 本身的权限通过配置表的 `require_auth` 和 `permission_code` 字段控制
|
||||
- 与现有的系统管理、监控等模块共享统一的 JWT Token
|
||||
|
||||
### 10.3 组件复用
|
||||
|
||||
复用 hzhub-admin 的标准组件和适配器:
|
||||
- `useVbenVxeGrid` - 表格组件(列表页)
|
||||
- `useVbenForm` - 表单组件(搜索、编辑)
|
||||
- `useVbenDrawer` / `useVbenModal` - 抽屉/弹窗组件
|
||||
- `TableSwitch` - 状态切换组件
|
||||
- `CodeMirror` - SQL 编辑器(复用代码生成器的编辑器)
|
||||
|
||||
### 10.4 API 函数集成
|
||||
|
||||
在 `/data/hzhub/hzhub-admin/apps/web-antd/src/api/erp/` 目录下新增 API 模块,遵循现有的 API 定义模式:
|
||||
- 使用 `requestClient` 统一请求客户端
|
||||
- 使用 `postWithMsg` / `putWithMsg` / `deleteWithMsg` 自动提示消息
|
||||
- 定义 TypeScript 类型(model.d.ts)
|
||||
|
||||
---
|
||||
|
||||
本方案提供了一个完整的 ERP API 管理平台设计,**完全集成到管理后台中**,实现统一管理。从数据库表设计、后端架构、前端界面到安全防护都有详细规划,包含缓存、监控、版本管理等完整功能。实现步骤按优先级分阶段推进,确保核心功能优先完成,逐步增强功能和安全性。
|
||||
277
docs/erp-api-quick-test-guide.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# ERP API 管理平台 - 快速测试指南
|
||||
|
||||
## 当前状态确认
|
||||
|
||||
✅ **ERP 服务运行正常**(Health: UP)
|
||||
✅ **SQL Server 数据库连接正常**
|
||||
✅ **数据库表已创建**(需要在 MySQL hzhub 数据库中验证)
|
||||
|
||||
---
|
||||
|
||||
## 测试方案一:通过管理后台测试(推荐)
|
||||
|
||||
### Step 1: 访问管理后台
|
||||
|
||||
访问地址:http://192.168.120.60:5666 或 http://localhost:5666
|
||||
|
||||
登录管理员账号
|
||||
|
||||
### Step 2: 检查新菜单
|
||||
|
||||
刷新页面后,左侧菜单应该出现:
|
||||
```
|
||||
系统管理
|
||||
工具
|
||||
监控
|
||||
ERP管理 ← 新增
|
||||
├── API配置 ← 点击进入列表页
|
||||
└── API监控
|
||||
```
|
||||
|
||||
如果菜单未显示,可能需要:
|
||||
- 重新登录
|
||||
- 清除浏览器缓存
|
||||
- 检查角色权限是否包含 `erp:api:list`
|
||||
|
||||
### Step 3: 进入 API 配置页面
|
||||
|
||||
点击"ERP管理 > API配置",应该看到:
|
||||
- 空列表(因为还没有创建任何 API 配置)
|
||||
- 工具栏有"新增"按钮
|
||||
|
||||
### Step 4: 创建测试 API
|
||||
|
||||
点击"新增"按钮,填写以下信息:
|
||||
|
||||
**基础信息**:
|
||||
- API名称:`测试客户列表查询`
|
||||
- API路径:`/erp/dynamic/v1/test_customer_list`
|
||||
- HTTP方法:`GET`
|
||||
- API描述:`测试SQL Server客户数据查询`
|
||||
- API版本:`v1`
|
||||
- 数据源:`erp`(默认)
|
||||
- 结果类型:`LIST`
|
||||
- 支持分页:`否`(先测试不分页)
|
||||
|
||||
**SQL模板**(选择一个测试):
|
||||
|
||||
**方案A:简单查询(推荐,不依赖具体表结构)**
|
||||
```sql
|
||||
SELECT TOP 10 customer_code, customer_name FROM customer_general
|
||||
```
|
||||
|
||||
**方案B:查询系统表(通用)**
|
||||
```sql
|
||||
SELECT TOP 5 TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
||||
```
|
||||
|
||||
**权限配置**:
|
||||
- 是否需要认证:`否`(方便测试)
|
||||
- 状态:`启用`
|
||||
|
||||
点击"确定"保存。
|
||||
|
||||
### Step 5: 测试 API 执行
|
||||
|
||||
在列表中找到刚创建的 API,点击"测试"按钮。
|
||||
|
||||
**测试参数**:输入空对象 `{}`(因为SQL没有参数)
|
||||
|
||||
点击"执行",查看结果:
|
||||
- 应该显示 SQL Server 的数据
|
||||
- 显示执行时间(毫秒)
|
||||
- 显示成功状态
|
||||
|
||||
如果成功,返回类似:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{"customer_code": "C001", "customer_name": "客户A"},
|
||||
{"customer_code": "C002", "customer_name": "客户B"}
|
||||
],
|
||||
"executionTime": 156,
|
||||
"executedSql": "SELECT TOP 10 customer_code, customer_name FROM customer_general"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: 测试带参数的 API(可选)
|
||||
|
||||
创建第二个测试 API:
|
||||
|
||||
**信息**:
|
||||
- API名称:`测试带参数查询`
|
||||
- API路径:`/erp/dynamic/v1/test_param_query`
|
||||
- SQL模板:
|
||||
```sql
|
||||
SELECT customer_code, customer_name FROM customer_general
|
||||
WHERE #{customerCode} IS NOT NULL THEN customer_code = #{customerCode}
|
||||
```
|
||||
|
||||
**测试参数**:
|
||||
```json
|
||||
{
|
||||
"customerCode": "C001"
|
||||
}
|
||||
```
|
||||
|
||||
查看结果,应该只返回指定客户的数据。
|
||||
|
||||
---
|
||||
|
||||
## 测试方案二:通过 curl 直接测试 API
|
||||
|
||||
### 测试1:无参数查询
|
||||
|
||||
```bash
|
||||
# 直接访问动态API(不需要Token,因为配置了@SaIgnore)
|
||||
curl http://localhost:8082/erp/dynamic/v1/test_customer_list
|
||||
|
||||
# 或通过网关访问(需要先获取Token)
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
http://localhost:8080/erp/dynamic/v1/test_customer_list
|
||||
```
|
||||
|
||||
**预期返回**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": [
|
||||
{"customer_code": "xxx", "customer_name": "xxx"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 测试2:带参数查询
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8082/erp/dynamic/v1/test_param_query \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"customerCode":"C001"}'
|
||||
```
|
||||
|
||||
### 测试3:API配置管理接口
|
||||
|
||||
```bash
|
||||
# 查询API配置列表(需要Token)
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
http://localhost:8080/erp/api/config/list?pageNum=1&pageSize=10
|
||||
|
||||
# 测试API(需要Token)
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:8080/erp/api/config/test/1 \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试验证清单
|
||||
|
||||
### 功能验证
|
||||
|
||||
✅ **菜单显示**:ERP管理菜单正常显示
|
||||
✅ **列表页面**:API配置列表正常加载
|
||||
✅ **新增功能**:能成功保存API配置到MySQL数据库
|
||||
✅ **测试功能**:API测试弹窗正常工作
|
||||
✅ **SQL执行**:动态SQL能成功查询SQL Server数据
|
||||
✅ **参数处理**:带参数的SQL能正确执行
|
||||
✅ **错误处理**:错误的SQL能显示详细错误信息
|
||||
|
||||
### 数据验证
|
||||
|
||||
检查 MySQL 数据库:
|
||||
|
||||
```sql
|
||||
-- 查看创建的API配置
|
||||
SELECT * FROM hzhub.erp_api_config;
|
||||
|
||||
-- 查看参数配置(如果有)
|
||||
SELECT * FROM hzhub.erp_api_param;
|
||||
|
||||
-- 查看菜单配置
|
||||
SELECT menu_id, menu_name, menu_type, visible, perms
|
||||
FROM hzhub.sys_menu
|
||||
WHERE menu_name LIKE '%ERP%' OR menu_name LIKE '%API%';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 可能遇到的问题
|
||||
|
||||
### 问题1: 菜单不显示
|
||||
|
||||
**原因**:
|
||||
- SQL未执行到sys_menu表
|
||||
- 角色未分配权限
|
||||
|
||||
**解决**:
|
||||
```sql
|
||||
-- 检查菜单是否插入
|
||||
SELECT * FROM sys_menu WHERE menu_name='ERP管理';
|
||||
|
||||
-- 给管理员角色分配权限(假设角色ID=1)
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, menu_id FROM sys_menu WHERE perms LIKE 'erp:api:%';
|
||||
```
|
||||
|
||||
### 问题2: API执行失败
|
||||
|
||||
**原因**:
|
||||
- SQL Server连接问题
|
||||
- SQL语法错误
|
||||
- 表不存在
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 测试SQL Server连接
|
||||
curl http://localhost:8082/erp/test/connection
|
||||
|
||||
# 查看错误详情
|
||||
# 在测试弹窗中查看完整的错误信息和堆栈
|
||||
```
|
||||
|
||||
### 问题3: 保存配置失败
|
||||
|
||||
**原因**:
|
||||
- MySQL表未创建
|
||||
- 字段类型不匹配
|
||||
|
||||
**解决**:
|
||||
```sql
|
||||
-- 验证表结构
|
||||
DESC hzhub.erp_api_config;
|
||||
DESC hzhub.erp_api_param;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试成功后的下一步
|
||||
|
||||
测试成功后,我将继续创建:
|
||||
|
||||
1. **前端完整界面**(Phase 2)
|
||||
- API配置列表页(含搜索、分页)
|
||||
- 编辑页(基础设置 + 参数配置两Tab)
|
||||
- 从表导入弹窗
|
||||
- 测试弹窗优化
|
||||
|
||||
2. **从表导入功能**(Phase 2)
|
||||
- 自动生成SQL模板
|
||||
- 自动生成参数配置
|
||||
|
||||
3. **监控统计功能**(Phase 3)
|
||||
- API调用统计表记录
|
||||
- 统计图表展示
|
||||
|
||||
---
|
||||
|
||||
## 请告诉我测试结果
|
||||
|
||||
请尝试测试并告诉我:
|
||||
- ✅ 测试成功,看到了数据
|
||||
- ❌ 遇到问题,具体错误信息
|
||||
|
||||
我将根据测试结果继续优化或创建前端代码!
|
||||
163
docs/erp-api-testing-guide.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# ERP API 管理平台 - 测试流程
|
||||
|
||||
## 当前状态
|
||||
|
||||
✅ **后端代码已完成**(Phase 1)
|
||||
✅ **SQL 文件已创建**:`/data/hzhub/hzhub-erp/docs/sql/erp_api_tables.sql`
|
||||
✅ **初始化指南已创建**:`/data/hzhub/docs/erp-api-database-init-guide.md`
|
||||
⏳ **数据库表待创建**(需手动执行)
|
||||
⏳ **ERP 服务待重启**
|
||||
|
||||
---
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### Step 1: 创建数据库表(需手动执行)
|
||||
|
||||
**推荐方式**:通过 Docker MySQL 容器执行
|
||||
|
||||
```bash
|
||||
# 1. 进入 MySQL 容器
|
||||
docker exec -it hzhub-mysql mysql -u root -phzhub123
|
||||
|
||||
# 2. 在 MySQL 命令行中执行
|
||||
USE hzhub;
|
||||
|
||||
# 3. 复制粘贴以下 SQL(或执行 SQL 文件)
|
||||
-- 见文件:/data/hzhub/hzhub-erp/docs/sql/erp_api_tables.sql
|
||||
|
||||
# 4. 验证表创建
|
||||
SHOW TABLES LIKE 'erp_api_%';
|
||||
SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'hzhub' AND table_name LIKE 'erp_api_%';
|
||||
|
||||
# 5. 验证菜单插入
|
||||
SELECT menu_id, menu_name, perms FROM sys_menu WHERE menu_name LIKE '%ERP%' OR menu_name LIKE '%API%';
|
||||
|
||||
# 6. 退出
|
||||
exit;
|
||||
```
|
||||
|
||||
**如果 MySQL 容器不可访问**,可使用 Navicat/DBeaver 等工具连接 localhost:3306 执行 SQL。
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 重启 ERP 服务(自动执行)
|
||||
|
||||
执行完 SQL 后,我将继续执行:
|
||||
|
||||
```bash
|
||||
cd /data/hzhub/hzhub-erp
|
||||
./restart.sh # 或 ./start.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 验证服务健康(自动执行)
|
||||
|
||||
```bash
|
||||
# 检查健康状态
|
||||
curl http://localhost:8082/actuator/health
|
||||
|
||||
# 检查 ERP 服务进程
|
||||
cd hzhub-erp && ./status.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 手动测试 API(需你手动执行)
|
||||
|
||||
#### 4.1 通过管理后台测试(推荐)
|
||||
|
||||
1. **访问管理后台**:http://192.168.120.60:5666
|
||||
2. **登录**(使用管理员账号)
|
||||
3. **刷新菜单**(菜单会动态加载,新菜单"ERP管理"应该出现)
|
||||
4. **进入 ERP管理 > API配置**
|
||||
5. **点击"新增"创建测试 API**:
|
||||
- API名称:`测试客户查询`
|
||||
- API路径:`/erp/dynamic/v1/test_customer`
|
||||
- HTTP方法:`GET`
|
||||
- SQL模板:
|
||||
```sql
|
||||
SELECT TOP 10 customer_code, customer_name FROM customer_general
|
||||
```
|
||||
- 结果类型:`LIST`
|
||||
- 支持分页:`否`
|
||||
- 状态:`启用`
|
||||
|
||||
6. **点击"测试"按钮**:输入空参数 `{}`,查看执行结果
|
||||
|
||||
#### 4.2 通过 curl 直接测试
|
||||
|
||||
```bash
|
||||
# 测试动态 API 执行(无需 Token,因为当前 @SaIgnore)
|
||||
curl http://localhost:8082/erp/dynamic/v1/test_customer
|
||||
|
||||
# 如果通过网关访问(需要 Token)
|
||||
curl -H "Authorization: Bearer <token>" http://localhost:8080/erp/dynamic/v1/test_customer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 测试结果验证
|
||||
|
||||
**预期结果**:
|
||||
|
||||
1. **API 配置保存成功**:能在列表中看到刚创建的 API
|
||||
2. **测试执行成功**:返回 SQL Server 数据库中的客户数据
|
||||
3. **执行时间记录**:能看到响应时间(毫秒)
|
||||
4. **错误处理**:如果 SQL 错误,能看到详细的错误信息
|
||||
|
||||
---
|
||||
|
||||
## 可能遇到的问题
|
||||
|
||||
### 问题 1: 菜单不显示
|
||||
|
||||
**原因**:菜单 SQL 未执行或角色无权限
|
||||
|
||||
**解决**:
|
||||
1. 检查 sys_menu 表是否插入成功
|
||||
2. 在角色管理中为管理员角色分配 `erp:api:*` 权限
|
||||
3. 清除浏览器缓存重新登录
|
||||
|
||||
### 问题 2: SQL 执行失败
|
||||
|
||||
**原因**:SQL Server 连接问题或 SQL 语法错误
|
||||
|
||||
**解决**:
|
||||
1. 检查 ERP 服务配置:`hzhub-erp/src/main/resources/application-dev.yml`
|
||||
2. 确认 SQL Server 地址:`192.168.120.10:8042`
|
||||
3. 测试 SQL Server 连接:
|
||||
```bash
|
||||
curl http://localhost:8082/erp/test/connection
|
||||
```
|
||||
|
||||
### 问题 3: 动态 API 返回 404
|
||||
|
||||
**原因**:API 配置路径不匹配或服务未重启
|
||||
|
||||
**解决**:
|
||||
1. 确认 ERP 服务已重启
|
||||
2. 检查 API 配置中的 `api_path` 是否为 `/erp/dynamic/v1/xxx`
|
||||
3. 确认 `api_method` 和 `api_version` 正确
|
||||
|
||||
---
|
||||
|
||||
## 下一步计划
|
||||
|
||||
测试成功后,我将继续:
|
||||
|
||||
1. **创建前端界面**(API 配置列表、编辑页、测试弹窗)
|
||||
2. **实现从表导入功能**(自动生成 SQL 和参数)
|
||||
3. **添加缓存和监控功能**
|
||||
4. **完善安全验证和权限控制**
|
||||
|
||||
---
|
||||
|
||||
## 准备好了吗?
|
||||
|
||||
**请告诉我 SQL 执行完成后的状态**:
|
||||
- ✅ SQL 执行成功,表和菜单已创建
|
||||
- ❌ SQL 执行遇到问题,需要帮助
|
||||
|
||||
我将根据你的反馈继续后续步骤。
|
||||
543
docs/gateway-migration-plan.md
Normal file
@@ -0,0 +1,543 @@
|
||||
# HZHub-Gateway 迁移方案
|
||||
|
||||
## 一、现状分析
|
||||
|
||||
### 当前架构(去中心化)
|
||||
|
||||
```
|
||||
前端门户 → Nginx/Vite代理 → 直连后端服务
|
||||
admin → localhost:6039 (hzhub-ai)
|
||||
employee → localhost:6039 (hzhub-ai) + localhost:8082 (hzhub-erp)
|
||||
dealer → localhost:6039 (hzhub-ai) + localhost:8082 (hzhub-erp)
|
||||
```
|
||||
|
||||
hzhub-gateway 已存在但未部署,仅有基础路由配置(3条路由 + CORS)。
|
||||
|
||||
### hzhub-ai 中可迁移的网关级功能
|
||||
|
||||
| 功能 | 位置 | 是否适合迁移 |
|
||||
|------|------|:---:|
|
||||
| Sa-Token JWT 验证 | `SaTokenConfig` + `SecurityConfig` | ✅ 是 — 全局统一鉴权 |
|
||||
| XSS 过滤 | `XssFilter` (javax.servlet Filter) | ✅ 是 — WebFilter 可实现 |
|
||||
| 请求/响应加密 | `CryptoFilter` (RSA 加解密) | ⚠️ 部分 — 需适配 WebFlux |
|
||||
| 接口限流 | `@RateLimiter` + Redisson | ✅ 是 — 可用 Redis + RequestRateLimiter |
|
||||
| 幂等性校验 | `@Idempotent` | ❌ 否 — 业务级,保留后端 |
|
||||
| SSE 流式响应 | ChatController | ❌ 否 — 业务级,保留后端 |
|
||||
| WebSocket 握手鉴权 | `PlusWebSocketInterceptor` | ❌ 否 — 协议不同,保留后端 |
|
||||
| 数据权限拦截 | MyBatis 拦截器 | ❌ 否 — ORM 层,保留后端 |
|
||||
|
||||
### 核心挑战
|
||||
|
||||
1. **Servlet vs WebFlux**:Gateway 基于 Spring WebFlux(响应式),hzhub-ai 的 Filter 是 Servlet 规范,不能直接复用,需改写为 `WebFilter`
|
||||
2. **JWT 解析**:Gateway 只需验证 JWT 合法性并透传用户信息,无需完整 Sa-Token 登录态
|
||||
3. **加密适配**:CryptoFilter 依赖 `ServletInputStream`,需改写为 `ServerHttpRequestDecorator`
|
||||
|
||||
---
|
||||
|
||||
## 二、目标架构
|
||||
|
||||
```
|
||||
前端门户 → Gateway(:8080) → 后端服务
|
||||
/ai/** → hzhub-ai(:8081)
|
||||
/erp/** → hzhub-erp(:8082)
|
||||
/system/** → hzhub-ai(:8081)
|
||||
```
|
||||
|
||||
Gateway 承担:
|
||||
- **统一鉴权**:验证 JWT Token,解析用户信息注入请求头
|
||||
- **XSS 防护**:对 POST/PUT 请求体进行 XSS 清洗
|
||||
- **接口限流**:基于 Redis 的令牌桶限流
|
||||
- **请求/响应加密**:按需解密请求、加密响应
|
||||
- **路由转发**:按路径分发到对应服务
|
||||
- **跨域处理**:全局 CORS
|
||||
|
||||
后端服务(hzhub-ai / hzhub-erp)承担:
|
||||
- **业务逻辑**:CRUD、AI 对话、工作流等
|
||||
- **数据权限**:MyBatis 层的多租户数据隔离
|
||||
- **幂等性**:防重复提交
|
||||
- **SSE/WebSocket**:流式推送
|
||||
|
||||
### 服务间信任模型
|
||||
|
||||
```
|
||||
外部请求 → Gateway → 后端服务
|
||||
│ │
|
||||
验证JWT 信任Gateway
|
||||
透传的用户头
|
||||
```
|
||||
|
||||
- Gateway 验证 JWT 通过后,将用户信息注入请求头:`X-User-Id`、`X-User-Name`、`X-Client-Id`
|
||||
- 后端服务在 `SecurityConfig` 中配置:**来自 Gateway 的请求跳过 JWT 验证**
|
||||
- 判断依据:检查请求头 `X-Gateway-Verified: true`(或共享内网 IP 白名单)
|
||||
- 开发环境:保留直连后端的鉴权能力(双重模式)
|
||||
|
||||
---
|
||||
|
||||
## 三、实施步骤
|
||||
|
||||
### Phase 1: Gateway 基础增强(认证 + 路由)
|
||||
|
||||
**目标**:让 Gateway 能正常转发请求并验证 JWT
|
||||
|
||||
#### 3.1 修改 pom.xml
|
||||
|
||||
```xml
|
||||
<!-- 已有:spring-cloud-starter-gateway, spring-cloud-starter-loadbalancer -->
|
||||
|
||||
<!-- 新增 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-jwt</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 3.2 JWT 认证全局过滤器
|
||||
|
||||
```java
|
||||
// src/main/java/org/hzhub/gateway/filter/AuthGlobalFilter.java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthGlobalFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private final SaTokenConfig saTokenConfig;
|
||||
|
||||
// 放行路径
|
||||
private static final List<String> WHITE_LIST = List.of(
|
||||
"/erp/test/", "/erp/customer/", "/actuator/"
|
||||
);
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
String path = exchange.getRequest().getURI().getPath();
|
||||
|
||||
// 白名单放行
|
||||
if (isWhiteList(path)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 从 Header 获取 Token
|
||||
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
|
||||
if (token == null || !token.startsWith("Bearer ")) {
|
||||
return unauthorized(exchange, "未登录或登录已过期");
|
||||
}
|
||||
|
||||
// 验证 JWT(使用 sa-token-jwt)
|
||||
String jwtToken = token.substring(7);
|
||||
try {
|
||||
SaTokenInfo info = SaManager.getTokenInfo(jwtToken);
|
||||
if (info == null || !info.isLogin()) {
|
||||
return unauthorized(exchange, "登录已过期");
|
||||
}
|
||||
// 透传用户信息到后端
|
||||
ServerHttpRequest mutated = exchange.getRequest().mutate()
|
||||
.header("X-User-Id", info.getLoginId())
|
||||
.header("X-Client-Id", exchange.getRequest().getHeaders().getFirst("ClientID"))
|
||||
.header("X-Gateway-Verified", "true")
|
||||
.build();
|
||||
return chain.filter(exchange.mutate().request(mutated).build());
|
||||
} catch (Exception e) {
|
||||
return unauthorized(exchange, "Token 无效");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWhiteList(String path) {
|
||||
return WHITE_LIST.stream().anyMatch(path::startsWith);
|
||||
}
|
||||
|
||||
private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
|
||||
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
|
||||
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||
byte[] bytes = ("{\"code\":401,\"msg\":\"" + msg + "\"}").getBytes(StandardCharsets.UTF_8);
|
||||
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
|
||||
return exchange.getResponse().writeWith(Mono.just(buffer));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() { return -100; } // 最高优先级
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 更新 application.yml
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: hzhub-gateway
|
||||
|
||||
# Redis 配置(限流 + Token 验证共享)
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: hzhub-ai
|
||||
uri: http://${AI_HOST:localhost}:${AI_PORT:8081}
|
||||
predicates:
|
||||
- Path=/ai/**,/system/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
- id: hzhub-erp
|
||||
uri: http://${ERP_HOST:localhost}:${ERP_PORT:8082}
|
||||
predicates:
|
||||
- Path=/erp/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
globalcors:
|
||||
cors-configurations:
|
||||
'[/**]':
|
||||
allowedOriginPatterns: "*"
|
||||
allowedMethods: "*"
|
||||
allowedHeaders: "*"
|
||||
allowCredentials: true
|
||||
maxAge: 3600
|
||||
|
||||
# Sa-Token(JWT 验证用,与后端服务共享 secret)
|
||||
sa-token:
|
||||
jwt-secret-key: ${JWT_SECRET:abcdefghijklmnopqrstuvwxyz}
|
||||
token-name: Authorization
|
||||
|
||||
# 日志
|
||||
logging:
|
||||
level:
|
||||
org.hzhub.gateway: debug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: XSS 过滤 + 限流
|
||||
|
||||
#### 3.4 XSS 全局过滤器(WebFlux 版)
|
||||
|
||||
```java
|
||||
// src/main/java/org/hzhub/gateway/filter/XssGlobalFilter.java
|
||||
@Component
|
||||
public class XssGlobalFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private static final List<String> EXCLUDE_PATHS = List.of("/ai/upload");
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
String path = exchange.getRequest().getURI().getPath();
|
||||
HttpMethod method = exchange.getRequest().getMethod();
|
||||
|
||||
// GET/DELETE 不处理;排除白名单
|
||||
if (method == HttpMethod.GET || method == HttpMethod.DELETE
|
||||
|| EXCLUDE_PATHS.stream().anyMatch(path::startsWith)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 包装请求,对 Body 进行 XSS 清洗
|
||||
ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
|
||||
@Override
|
||||
public Flux<DataBuffer> getBody() {
|
||||
return super.getBody().map(buffer -> {
|
||||
byte[] bytes = new byte[buffer.readableByteCount()];
|
||||
buffer.read(bytes);
|
||||
String body = new String(bytes, StandardCharsets.UTF_8);
|
||||
String cleaned = cleanXss(body);
|
||||
byte[] cleanedBytes = cleaned.getBytes(StandardCharsets.UTF_8);
|
||||
DataBufferFactory factory = buffer.factory();
|
||||
DataBuffer newBuffer = factory.allocateBuffer(cleanedBytes.length);
|
||||
newBuffer.write(cleanedBytes);
|
||||
return newBuffer;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return chain.filter(exchange.mutate().request(decorated).build());
|
||||
}
|
||||
|
||||
private String cleanXss(String value) {
|
||||
if (value == null) return null;
|
||||
return value
|
||||
.replaceAll("<script>", "<script>")
|
||||
.replaceAll("</script>", "</script>")
|
||||
.replaceAll("javascript:", "")
|
||||
.replaceAll("on\\w+\\s*=", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() { return -50; }
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.5 限流配置(基于 Redis)
|
||||
|
||||
```yaml
|
||||
# 在 application.yml 的 gateway 配置中添加
|
||||
spring.cloud.gateway.default-filters:
|
||||
- name: RequestRateLimiter
|
||||
args:
|
||||
redis-rate-limiter.replenishRate: 10 # 每秒补充令牌数
|
||||
redis-rate-limiter.burstCapacity: 20 # 令牌桶容量
|
||||
key-resolver: "#{@ipKeyResolver}" # 按 IP 限流
|
||||
```
|
||||
|
||||
```java
|
||||
// src/main/java/org/hzhub/gateway/config/RateLimiterConfig.java
|
||||
@Configuration
|
||||
public class RateLimiterConfig {
|
||||
|
||||
/**
|
||||
* 按 IP 地址限流
|
||||
*/
|
||||
@Bean
|
||||
public KeyResolver ipKeyResolver() {
|
||||
return exchange -> {
|
||||
String ip = exchange.getRequest().getRemoteAddress() != null
|
||||
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
return Mono.just("gateway:ratelimit:" + ip);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按路径+IP 限流(更细粒度)
|
||||
*/
|
||||
@Bean
|
||||
public KeyResolver pathIpKeyResolver() {
|
||||
return exchange -> {
|
||||
String path = exchange.getRequest().getURI().getPath();
|
||||
String ip = exchange.getRequest().getRemoteAddress() != null
|
||||
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
|
||||
: "unknown";
|
||||
return Mono.just("gateway:ratelimit:" + path + ":" + ip);
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 后端服务适配
|
||||
|
||||
#### 3.6 hzhub-ai 的 SecurityConfig 改造
|
||||
|
||||
在 hzhub-ai 的 `SecurityConfig` 中新增:当请求来自 Gateway 时,跳过 JWT 验证。
|
||||
|
||||
```java
|
||||
// hzhub-ai 的 SecurityConfig.java 中修改
|
||||
private boolean isFromGateway(ServerHttpRequest request) {
|
||||
String verified = request.getHeaders().getFirst("X-Gateway-Verified");
|
||||
return "true".equals(verified);
|
||||
}
|
||||
```
|
||||
|
||||
或者更简单:在 Sa-Token 拦截器中添加检查:
|
||||
```java
|
||||
// 如果 Gateway 已验证,直接放行
|
||||
if ("true".equals(request.getHeader("X-Gateway-Verified"))) {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.7 hzhub-erp 同理
|
||||
|
||||
在 hzhub-erp 的 `SecurityConfig.java` 中添加同样的 Gateway 信任逻辑。
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 前端路由切换
|
||||
|
||||
#### 3.8 hzhub-admin(管理后台)
|
||||
|
||||
修改 `.env.development`:
|
||||
```env
|
||||
VITE_GLOB_API_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
修改 `vite.config.ts` 代理:
|
||||
```typescript
|
||||
'/api': {
|
||||
target: 'http://localhost:8080', // 改为 Gateway
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api/, ''),
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.9 hzhub-portal-employee(员工门户)
|
||||
|
||||
修改 `.env.development`:
|
||||
```env
|
||||
VITE_API_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
`request.ts` 中已自动使用此 baseURL,无需改动。
|
||||
|
||||
注意:原来直连 ERP 的请求(`/erp/customer/**`)也需经过 Gateway,Gateway 路由已覆盖 `/erp/**`。
|
||||
|
||||
#### 3.10 hzhub-portal-dealer(经销商门户)
|
||||
|
||||
同 employee,统一指向 Gateway。
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Docker Compose 集成
|
||||
|
||||
#### 3.11 新增 hzhub-gateway 服务
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml 新增
|
||||
hzhub-gateway:
|
||||
build:
|
||||
context: ../hzhub-gateway
|
||||
dockerfile: Dockerfile
|
||||
container_name: hzhub-gateway
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: prod
|
||||
REDIS_HOST: hzhub-redis
|
||||
REDIS_PORT: 6379
|
||||
AI_HOST: hzhub-ai
|
||||
AI_PORT: 6039
|
||||
ERP_HOST: hzhub-erp
|
||||
ERP_PORT: 8082
|
||||
JWT_SECRET: ${JWT_SECRET:-abcdefghijklmnopqrstuvwxyz}
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
hzhub-ai:
|
||||
condition: service_started
|
||||
hzhub-erp:
|
||||
condition: service_started
|
||||
networks:
|
||||
- hzhub-network
|
||||
```
|
||||
|
||||
#### 3.12 前端 Nginx 代理改为 Gateway
|
||||
|
||||
```nginx
|
||||
# hzhub-portal-employee 的 Nginx 配置
|
||||
# 原来: proxy_pass http://hzhub-ai:6039;
|
||||
# 改为:
|
||||
location /api/ {
|
||||
proxy_pass http://hzhub-gateway:8080;
|
||||
}
|
||||
location /erp/ {
|
||||
proxy_pass http://hzhub-gateway:8080;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.13 后端服务关闭外部端口暴露
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml 修改
|
||||
hzhub-ai:
|
||||
# ports:
|
||||
# - "6039:6039" # 不再对外暴露,仅内网访问
|
||||
hzhub-erp:
|
||||
# ports:
|
||||
# - "8082:8082" # 不再对外暴露,仅内网访问
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、文件变更清单
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `hzhub-gateway/src/main/java/org/hzhub/gateway/filter/AuthGlobalFilter.java` | JWT 认证过滤器 |
|
||||
| `hzhub-gateway/src/main/java/org/hzhub/gateway/filter/XssGlobalFilter.java` | XSS 过滤 |
|
||||
| `hzhub-gateway/src/main/java/org/hzhub/gateway/config/RateLimiterConfig.java` | 限流 Key 解析器 |
|
||||
| `hzhub-gateway/Dockerfile` | Gateway 容器构建 |
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `hzhub-gateway/pom.xml` | 新增 sa-token-reactor、redis-reactive 依赖 |
|
||||
| `hzhub-gateway/src/main/resources/application.yml` | 重写路由、Redis、Sa-Token 配置 |
|
||||
| `hzhub-gateway/src/main/java/org/hzhub/HzhubGatewayApplication.java` | 无需改动 |
|
||||
| `hzhub-ai/.../SecurityConfig.java` | 添加 Gateway 信任逻辑 |
|
||||
| `hzhub-erp/.../SecurityConfig.java` | 添加 Gateway 信任逻辑 |
|
||||
| `hzhub-admin/apps/web-antd/.env.development` | API 地址改为 Gateway |
|
||||
| `hzhub-admin/apps/web-antd/vite.config.ts` | 代理目标改为 Gateway |
|
||||
| `hzhub-portal-employee/.env.development` | VITE_API_URL 指向 Gateway |
|
||||
| `hzhub-portal-dealer/.env.development` | 新增 API URL |
|
||||
| `hzhub-deploy/docker-compose.yml` | 新增 Gateway 服务,调整网络 |
|
||||
|
||||
---
|
||||
|
||||
## 五、分阶段验证
|
||||
|
||||
### Phase 1 验证
|
||||
```bash
|
||||
# 1. 启动 Gateway
|
||||
cd hzhub-gateway && mvn spring-boot:run
|
||||
|
||||
# 2. 测试路由转发
|
||||
curl http://localhost:8080/erp/test/connection
|
||||
# 应返回 SQL Server 连接信息
|
||||
|
||||
# 3. 测试鉴权(未登录)
|
||||
curl http://localhost:8080/ai/chat/message
|
||||
# 应返回 401
|
||||
|
||||
# 4. 测试鉴权(带 Token)
|
||||
curl -H "Authorization: Bearer <token>" http://localhost:8080/ai/chat/message
|
||||
# 应正常返回结果
|
||||
```
|
||||
|
||||
### Phase 2 验证
|
||||
```bash
|
||||
# 测试 XSS 过滤
|
||||
curl -X POST http://localhost:8080/ai/xxx \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"content": "<script>alert(1)</script>"}'
|
||||
# 请求体应被清洗
|
||||
|
||||
# 测试限流(快速连续请求)
|
||||
for i in {1..30}; do curl -s http://localhost:8080/ai/xxx; done
|
||||
# 超过阈值应返回 429
|
||||
```
|
||||
|
||||
### Phase 3 验证
|
||||
```bash
|
||||
# 前端登录后,所有请求走 Gateway
|
||||
# 确认 AI 接口、ERP 接口均正常工作
|
||||
# 确认 Token 过期后自动跳转登录页
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、风险与注意事项
|
||||
|
||||
| 风险 | 应对 |
|
||||
|------|------|
|
||||
| Sa-Token JWT 验证方式与后端不一致 | 统一使用 `StpLogicJwtForSimple`,共享 `jwt-secret-key` |
|
||||
| Gateway 成为单点故障 | 后续可部署多实例 + Nginx 负载均衡 |
|
||||
| SSE 流式响应经过 Gateway 可能超时 | 需在 Gateway 配置路由超时时间(默认无超时) |
|
||||
| CORS 配置冲突 | Gateway 统一处理 CORS,后端服务关闭 CORS |
|
||||
| 开发环境直连 vs 生产走 Gateway | 后端服务保留双重模式:有 `X-Gateway-Verified` 跳过验证,否则自行验证 |
|
||||
|
||||
---
|
||||
|
||||
## 七、后续优化方向
|
||||
|
||||
1. **服务注册与发现**:引入 Nacos,替代硬编码的服务地址
|
||||
2. **熔断降级**:引入 Resilience4j,对下游服务做熔断保护
|
||||
3. **链路追踪**:引入 SkyWalking 或 Micrometer Trace
|
||||
4. **灰度发布**:基于 Header 的蓝绿部署路由
|
||||
5. **API 文档聚合**:Gateway 聚合 Swagger/Knife4j 文档
|
||||
51
erp-api-verify.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# ERP API 管理平台快速验证脚本
|
||||
# 用途:通过API快速创建测试数据并验证功能
|
||||
|
||||
echo "========================================="
|
||||
echo " ERP API 管理平台 - 快速功能验证"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# 检查ERP服务健康状态
|
||||
echo "1️⃣ 检查ERP服务健康状态..."
|
||||
HEALTH=$(curl -s http://localhost:8082/actuator/health | jq -r '.status')
|
||||
if [ "$HEALTH" = "UP" ]; then
|
||||
echo "✅ ERP服务运行正常 (status: $HEALTH)"
|
||||
else
|
||||
echo "❌ ERP服务异常 (status: $HEALTH)"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 测试SQL Server连接
|
||||
echo "2️⃣ 测试SQL Server连接..."
|
||||
CONN_TEST=$(curl -s http://localhost:8082/erp/test/connection)
|
||||
echo "响应: $CONN_TEST"
|
||||
echo ""
|
||||
|
||||
# 提供手动测试说明
|
||||
echo "3️⃣ 下一步:手动测试"
|
||||
echo "---"
|
||||
echo "请按以下步骤操作:"
|
||||
echo ""
|
||||
echo "方法一:通过管理后台(推荐)"
|
||||
echo " 1. 访问:http://192.168.120.60:5666 或 http://localhost:5666"
|
||||
echo " 2. 登录后查看左侧菜单 'ERP管理 > API配置'"
|
||||
echo " 3. 点击'新增'创建测试API:"
|
||||
echo " - API名称: 测试系统表查询"
|
||||
echo " - API路径: /erp/dynamic/v1/test_system_tables"
|
||||
echo " - HTTP方法: GET"
|
||||
echo " - SQL模板: SELECT TOP 5 TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'"
|
||||
echo " - 结果类型: LIST"
|
||||
echo " - 支持分页: 否"
|
||||
echo " 4. 保存后点击'测试'按钮,输入空参数 {}"
|
||||
echo ""
|
||||
echo "方法二:通过curl直接测试"
|
||||
echo " # 先通过管理后台创建API配置,然后执行:"
|
||||
echo " curl http://localhost:8082/erp/dynamic/v1/test_system_tables"
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " 测试指南已保存到:"
|
||||
echo " /data/hzhub/docs/erp-api-quick-test-guide.md"
|
||||
echo "========================================="
|
||||
220
hzhub-admin/apps/web-antd/src/api/erp/api/index.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* ERP API 配置管理接口定义
|
||||
*/
|
||||
import type { ID, IDS, PageQuery, PageResult } from '#/api/common';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
enum Api {
|
||||
root = '/erp/api/config',
|
||||
test = '/erp/api/config/test',
|
||||
preview = '/erp/api/config/preview',
|
||||
import = '/erp/api/config/importFromTable',
|
||||
stats = '/erp/api/config/stats',
|
||||
errorLog = '/erp/api/config/errorLog',
|
||||
cache = '/erp/api/config/cache',
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询API配置列表
|
||||
*/
|
||||
export function apiConfigList(params?: PageQuery) {
|
||||
return requestClient.get<PageResult<ErpApiConfigVO>>(Api.root + '/list', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API配置详情
|
||||
*/
|
||||
export function apiConfigInfo(apiId: ID) {
|
||||
return requestClient.get<ErpApiConfigInfoResponse>(`${Api.root}/${apiId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增API配置
|
||||
*/
|
||||
export function apiConfigAdd(data: Partial<ErpApiConfig>) {
|
||||
return requestClient.postWithMsg<void>(Api.root, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改API配置
|
||||
*/
|
||||
export function apiConfigEdit(data: Partial<ErpApiConfig>) {
|
||||
return requestClient.putWithMsg<void>(Api.root, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除API配置
|
||||
*/
|
||||
export function apiConfigRemove(apiIds: IDS) {
|
||||
return requestClient.deleteWithMsg<void>(`${Api.root}/${apiIds}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新API状态
|
||||
*/
|
||||
export function apiConfigChangeStatus(data: Partial<ErpApiConfig>) {
|
||||
return requestClient.putWithMsg<void>(`${Api.root}/changeStatus`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从表导入
|
||||
*/
|
||||
export function apiConfigImportFromTable(data: ImportTableRequest) {
|
||||
return requestClient.postWithMsg<void>(Api.import, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步表结构
|
||||
*/
|
||||
export function apiConfigSyncTable(apiId: ID) {
|
||||
return requestClient.get<void>(`${Api.root}/syncTable/${apiId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* API测试
|
||||
*/
|
||||
export function apiConfigTest(apiId: ID, params: Record<string, any>) {
|
||||
return requestClient.post<ApiTestResultVO>(`${Api.test}/${apiId}`, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* API文档预览
|
||||
*/
|
||||
export function apiConfigPreview(apiId: ID) {
|
||||
return requestClient.get<Record<string, string>>(`${Api.preview}/${apiId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询调用统计
|
||||
*/
|
||||
export function apiConfigStats(apiId: ID, startTime?: string, endTime?: string) {
|
||||
return requestClient.get<ApiStatsResponse>(`${Api.stats}/${apiId}`, {
|
||||
params: { startTime, endTime },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询错误日志
|
||||
*/
|
||||
export function apiConfigErrorLog(apiId: ID, limit?: number) {
|
||||
return requestClient.get<ApiErrorLogItem[]>(`${Api.errorLog}/${apiId}`, {
|
||||
params: { limit: limit || 10 },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
export function apiConfigClearCache(apiId: ID) {
|
||||
return requestClient.deleteWithMsg<void>(`${Api.cache}/${apiId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* API配置VO
|
||||
*/
|
||||
export interface ErpApiConfigVO {
|
||||
apiId: number;
|
||||
apiName: string;
|
||||
apiPath: string;
|
||||
apiMethod: string;
|
||||
apiDesc: string;
|
||||
apiVersion: string;
|
||||
dataSource: string;
|
||||
sqlTemplate: string;
|
||||
resultType: string;
|
||||
supportPagination: number;
|
||||
pageParamName: string;
|
||||
sizeParamName: string;
|
||||
requireAuth: number;
|
||||
permissionCode: string;
|
||||
enableCache: number;
|
||||
cacheKeyTemplate: string;
|
||||
cacheTtl: number;
|
||||
sourceTable: string;
|
||||
sourceTableComment: string;
|
||||
status: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
createBy: string;
|
||||
updateBy: string;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API配置实体
|
||||
*/
|
||||
export interface ErpApiConfig extends ErpApiConfigVO {
|
||||
params?: ErpApiParam[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API参数配置
|
||||
*/
|
||||
export interface ErpApiParam {
|
||||
paramId: number;
|
||||
apiId: number;
|
||||
paramName: string;
|
||||
paramDesc: string;
|
||||
paramType: string;
|
||||
paramPosition: string;
|
||||
isRequired: number;
|
||||
defaultValue: string;
|
||||
sqlParamName: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* API配置详情响应
|
||||
*/
|
||||
export interface ErpApiConfigInfoResponse {
|
||||
info: ErpApiConfig;
|
||||
params: ErpApiParam[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API测试结果
|
||||
*/
|
||||
export interface ApiTestResultVO {
|
||||
apiPath: string;
|
||||
testMethod: string;
|
||||
requestParams: Record<string, any>;
|
||||
success: boolean;
|
||||
data: any;
|
||||
executionTime: number;
|
||||
executedSql: string;
|
||||
errorMessage: string;
|
||||
errorStack: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API统计响应
|
||||
*/
|
||||
export interface ApiStatsResponse {
|
||||
totalCalls: number;
|
||||
avgResponseTime: number;
|
||||
errorRate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* API错误日志项
|
||||
*/
|
||||
export interface ApiErrorLogItem {
|
||||
statsId: number;
|
||||
apiId: number;
|
||||
callTime: string;
|
||||
callParams: string;
|
||||
responseTime: number;
|
||||
callStatus: string;
|
||||
errorMessage: string;
|
||||
errorStack: string;
|
||||
clientIp: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从表导入请求
|
||||
*/
|
||||
export interface ImportTableRequest {
|
||||
tableNames: string[];
|
||||
dataSource?: string;
|
||||
}
|
||||
97
hzhub-admin/apps/web-antd/src/api/erp/api/model.d.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* ERP动态API配置类型定义
|
||||
*/
|
||||
|
||||
export interface ErpApiConfig {
|
||||
apiId: number;
|
||||
apiName: string;
|
||||
apiPath: string;
|
||||
apiMethod: string;
|
||||
apiDesc?: string;
|
||||
apiVersion: string;
|
||||
|
||||
// 数据源配置
|
||||
dataSource: string;
|
||||
|
||||
// SQL配置
|
||||
sqlTemplate: string;
|
||||
resultType: string;
|
||||
|
||||
// 分页配置
|
||||
supportPagination: number;
|
||||
pageParamName?: string;
|
||||
sizeParamName?: string;
|
||||
|
||||
// 权限配置
|
||||
requireAuth: number;
|
||||
permissionCode?: string;
|
||||
|
||||
// 缓存配置
|
||||
enableCache: number;
|
||||
cacheKeyTemplate?: string;
|
||||
cacheTtl?: number;
|
||||
|
||||
// 来源表信息
|
||||
sourceTable?: string;
|
||||
sourceTableComment?: string;
|
||||
|
||||
// 状态
|
||||
status: number;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
createBy?: string;
|
||||
updateBy?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ErpApiParam {
|
||||
paramId: number;
|
||||
apiId: number;
|
||||
|
||||
// 参数基本信息
|
||||
paramName: string;
|
||||
paramDesc?: string;
|
||||
paramType: string;
|
||||
|
||||
// 参数位置
|
||||
paramPosition: string;
|
||||
|
||||
// 参数验证
|
||||
isRequired: number;
|
||||
defaultValue?: string;
|
||||
|
||||
// SQL映射
|
||||
sqlParamName?: string;
|
||||
|
||||
// 排序
|
||||
sort?: number;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
export interface ApiTestResult {
|
||||
apiPath: string;
|
||||
testMethod: string;
|
||||
requestParams?: Record<string, any>;
|
||||
success: boolean;
|
||||
data?: any;
|
||||
executionTime?: number;
|
||||
executedSql?: string;
|
||||
errorMessage?: string;
|
||||
errorStack?: string;
|
||||
}
|
||||
|
||||
export interface ApiStats {
|
||||
totalCalls: number;
|
||||
successCalls: number;
|
||||
errorCalls: number;
|
||||
avgResponseTime: number;
|
||||
maxResponseTime: number;
|
||||
minResponseTime: number;
|
||||
errorRate: number;
|
||||
}
|
||||
|
||||
export interface ApiConfigDetail {
|
||||
info: ErpApiConfig;
|
||||
params: ErpApiParam[];
|
||||
}
|
||||
@@ -60,3 +60,10 @@ export function deptUpdate(data: Partial<Dept>) {
|
||||
export function deptRemove(deptId: ID) {
|
||||
return requestClient.deleteWithMsg<void>(`${Api.root}/${deptId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从企业微信同步部门
|
||||
*/
|
||||
export function deptSyncFromWecom() {
|
||||
return requestClient.postWithMsg<string>(`${Api.root}/syncFromWecom`);
|
||||
}
|
||||
|
||||
@@ -169,3 +169,10 @@ export function getDeptTree() {
|
||||
export function listUserByDeptId(deptId: ID) {
|
||||
return requestClient.get<User[]>(`${Api.listDeptUsers}/${deptId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从企业微信同步用户
|
||||
*/
|
||||
export function userSyncFromWecom() {
|
||||
return requestClient.postWithMsg<string>(`${Api.root}/syncFromWecom`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
enum Api {
|
||||
syncFull = '/wecom/approval/sync/full',
|
||||
syncCurrent = '/wecom/approval/sync/current',
|
||||
syncLogs = '/wecom/approval/sync/logs',
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量同步审批数据(管理员)
|
||||
* @param daysBack 同步近 N 天数据,默认30天
|
||||
*/
|
||||
export function wecomApprovalSyncFull(daysBack = 30) {
|
||||
return requestClient.post<number>(`${Api.syncFull}`, null, {
|
||||
params: { daysBack },
|
||||
// 设置较长的超时时间(60秒),因为即使异步,启动同步也需要时间
|
||||
timeout: 60000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步当前用户审批数据
|
||||
*/
|
||||
export function wecomApprovalSyncCurrent() {
|
||||
return requestClient.post<string>(Api.syncCurrent, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步审批模板(管理员)
|
||||
*/
|
||||
export function wecomApprovalSyncTemplates() {
|
||||
return requestClient.post<string>('/wecom/approval/templates/sync', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询同步日志
|
||||
*/
|
||||
export function wecomApprovalSyncLogs(params: { pageNum: number; pageSize: number }) {
|
||||
return requestClient.get<{ rows: any[]; total: number }>(Api.syncLogs, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时任务状态
|
||||
*/
|
||||
export function getTaskStatus() {
|
||||
return requestClient.get<{ running: boolean; cron: string }>('/wecom/approval/sync/task/status');
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时任务
|
||||
*/
|
||||
export function startTask() {
|
||||
return requestClient.post<string>('/wecom/approval/sync/task/start', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止定时任务
|
||||
*/
|
||||
export function stopTask() {
|
||||
return requestClient.post<string>('/wecom/approval/sync/task/stop', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置同步频率
|
||||
*/
|
||||
export function setCron(cron: string) {
|
||||
return requestClient.post<string>('/wecom/approval/sync/task/cron', null, { params: { cron } });
|
||||
}
|
||||
29
hzhub-admin/apps/web-antd/src/api/system/wecom-config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
enum Api {
|
||||
root = '/wecom/tenant-config',
|
||||
test = '/wecom/tenant-config/test',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户的企业微信配置
|
||||
*/
|
||||
export function getWecomConfig() {
|
||||
return requestClient.get(Api.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存/更新企业微信配置
|
||||
*/
|
||||
export function saveWecomConfig(data: any) {
|
||||
return requestClient.put(Api.root, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试企业微信连接
|
||||
*/
|
||||
export function testWecomConfig(corpid: string, corpsecret: string) {
|
||||
return requestClient.post(Api.test, null, {
|
||||
params: { corpid, corpsecret },
|
||||
});
|
||||
}
|
||||
421
hzhub-admin/apps/web-antd/src/views/erp/api/api-drawer.vue
Normal file
@@ -0,0 +1,421 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, provide } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { message, Skeleton, Tabs, TabPane } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { apiConfigInfo, apiConfigAdd, apiConfigEdit } from '#/api/erp/api';
|
||||
import type { ErpApiConfig, ErpApiParam } from '#/api/erp/api';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
|
||||
import {
|
||||
apiMethodOptions,
|
||||
apiVersionOptions,
|
||||
resultTypeOptions,
|
||||
dataSourceOptions,
|
||||
} from './data';
|
||||
import SQLTemplate from './edit-tabs/sql-template.vue';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const title = computed(() => {
|
||||
return isUpdate.value ? '编辑API配置' : '新增API配置';
|
||||
});
|
||||
|
||||
const currentTab = ref('basic');
|
||||
const paramsData = ref<ErpApiParam[]>([]);
|
||||
const apiData = ref<ErpApiConfig>({} as ErpApiConfig);
|
||||
|
||||
// 提供数据给子组件
|
||||
provide('apiData', apiData);
|
||||
provide('paramsData', paramsData);
|
||||
|
||||
// 基础设置表单Schema
|
||||
const basicSchema = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiId',
|
||||
label: 'API ID',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: ['apiId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiName',
|
||||
label: 'API名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入API名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiPath',
|
||||
label: 'API路径',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '例如:/erp/dynamic/v1/customer_list',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'apiMethod',
|
||||
label: 'HTTP方法',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: apiMethodOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputTextArea',
|
||||
fieldName: 'apiDesc',
|
||||
label: 'API描述',
|
||||
componentProps: {
|
||||
placeholder: '请输入API描述',
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'apiVersion',
|
||||
label: 'API版本',
|
||||
componentProps: {
|
||||
options: apiVersionOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'dataSource',
|
||||
label: '数据源',
|
||||
componentProps: {
|
||||
options: dataSourceOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'resultType',
|
||||
label: '结果类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: resultTypeOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'supportPagination',
|
||||
label: '支持分页',
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
checkedValue: 1,
|
||||
unCheckedValue: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'pageParamName',
|
||||
label: '页码参数名',
|
||||
dependencies: {
|
||||
show: (values) => values.supportPagination === 1,
|
||||
triggerFields: ['supportPagination'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '默认:pageNum',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'sizeParamName',
|
||||
label: '页大小参数名',
|
||||
dependencies: {
|
||||
show: (values) => values.supportPagination === 1,
|
||||
triggerFields: ['supportPagination'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '默认:pageSize',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'requireAuth',
|
||||
label: '需要认证',
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
checkedValue: 1,
|
||||
unCheckedValue: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'permissionCode',
|
||||
label: '权限标识',
|
||||
dependencies: {
|
||||
show: (values) => values.requireAuth === 1,
|
||||
triggerFields: ['requireAuth'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '例如:erp:customer:list',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'enableCache',
|
||||
label: '启用缓存',
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
checkedValue: 1,
|
||||
unCheckedValue: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'cacheKeyTemplate',
|
||||
label: '缓存键模板',
|
||||
dependencies: {
|
||||
show: (values) => values.enableCache === 1,
|
||||
triggerFields: ['enableCache'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '支持参数占位符',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'cacheTtl',
|
||||
label: '缓存过期时间',
|
||||
dependencies: {
|
||||
show: (values) => values.enableCache === 1,
|
||||
triggerFields: ['enableCache'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '秒',
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'sourceTable',
|
||||
label: '来源表名',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
placeholder: '如果是从表导入生成的,会显示来源表名',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputTextArea',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelWidth: 120,
|
||||
},
|
||||
schema: basicSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
const loading = ref(false);
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleConfirm,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
drawerApi.drawerLoading(true);
|
||||
loading.value = true;
|
||||
|
||||
const { apiId } = drawerApi.getData() as { apiId?: number | string };
|
||||
isUpdate.value = !!apiId;
|
||||
|
||||
if (isUpdate.value && apiId) {
|
||||
const response = await apiConfigInfo(Number(apiId));
|
||||
await formApi.setValues(response.info);
|
||||
apiData.value = response.info;
|
||||
paramsData.value = response.params || [];
|
||||
} else {
|
||||
// 设置默认值
|
||||
const defaultData = {
|
||||
apiMethod: 'GET',
|
||||
apiVersion: 'v1',
|
||||
dataSource: 'erp',
|
||||
resultType: 'LIST',
|
||||
supportPagination: 0,
|
||||
pageParamName: 'pageNum',
|
||||
sizeParamName: 'pageSize',
|
||||
requireAuth: 0,
|
||||
enableCache: 0,
|
||||
cacheTtl: 300,
|
||||
status: 1,
|
||||
sqlTemplate: '',
|
||||
};
|
||||
await formApi.setValues(defaultData);
|
||||
apiData.value = defaultData as ErpApiConfig;
|
||||
paramsData.value = [];
|
||||
}
|
||||
|
||||
await markInitialized();
|
||||
|
||||
drawerApi.drawerLoading(false);
|
||||
loading.value = false;
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
drawerApi.lock(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
currentTab.value = 'basic';
|
||||
return;
|
||||
}
|
||||
|
||||
const formValues = cloneDeep(await formApi.getValues());
|
||||
|
||||
// 验证必填字段
|
||||
if (!formValues.apiName || !formValues.apiPath) {
|
||||
message.error('请填写必填字段:API名称、API路径');
|
||||
currentTab.value = 'basic';
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证SQL模板
|
||||
if (!apiData.value.sqlTemplate) {
|
||||
message.error('请填写SQL模板');
|
||||
currentTab.value = 'sql';
|
||||
return;
|
||||
}
|
||||
|
||||
// 合并数据(formApi数据 + apiData中的sqlTemplate)
|
||||
const submitData = {
|
||||
...formValues,
|
||||
sqlTemplate: apiData.value.sqlTemplate,
|
||||
params: paramsData.value,
|
||||
};
|
||||
|
||||
if (isUpdate.value) {
|
||||
await apiConfigEdit(submitData);
|
||||
message.success('修改成功');
|
||||
} else {
|
||||
await apiConfigAdd(submitData);
|
||||
message.success('新增成功');
|
||||
}
|
||||
|
||||
resetInitialized();
|
||||
emit('reload');
|
||||
drawerApi.close();
|
||||
} catch (error: any) {
|
||||
message.error('保存失败: ' + (error.message || '未知错误'));
|
||||
} finally {
|
||||
drawerApi.lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
await formApi.resetForm();
|
||||
paramsData.value = [];
|
||||
resetInitialized();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :title="title" class="w-[800px]">
|
||||
<Skeleton v-if="loading" active />
|
||||
<Tabs v-show="!loading" v-model:activeKey="currentTab">
|
||||
<TabPane key="basic" tab="基础设置">
|
||||
<BasicForm />
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="params" tab="参数配置">
|
||||
<div class="p-4">
|
||||
<div class="mb-2">
|
||||
<a-button type="primary" @click="paramsData.push({
|
||||
paramId: Date.now(),
|
||||
apiId: 0,
|
||||
paramName: '',
|
||||
paramType: 'String',
|
||||
paramPosition: 'QUERY',
|
||||
isRequired: 0,
|
||||
defaultValue: '',
|
||||
paramDesc: '',
|
||||
sort: paramsData.length,
|
||||
})">
|
||||
新增参数
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table :data-source="paramsData" :columns="[
|
||||
{ title: '参数名称', dataIndex: 'paramName', width: 150 },
|
||||
{ title: '参数类型', dataIndex: 'paramType', width: 120 },
|
||||
{ title: '参数位置', dataIndex: 'paramPosition', width: 100 },
|
||||
{ title: '必填', dataIndex: 'isRequired', width: 80 },
|
||||
{ title: '默认值', dataIndex: 'defaultValue', width: 120 },
|
||||
{ title: '参数描述', dataIndex: 'paramDesc', width: 200 },
|
||||
{ title: '操作', dataIndex: 'action', width: 100, fixed: 'right' },
|
||||
]" :pagination="false" size="small">
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.dataIndex === 'paramName'">
|
||||
<a-input v-model:value="record.paramName" size="small" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'paramType'">
|
||||
<a-select v-model:value="record.paramType" size="small" :options="[
|
||||
{ label: 'String', value: 'String' },
|
||||
{ label: 'Integer', value: 'Integer' },
|
||||
{ label: 'Long', value: 'Long' },
|
||||
{ label: 'Date', value: 'Date' },
|
||||
]" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'paramPosition'">
|
||||
<a-select v-model:value="record.paramPosition" size="small" :options="[
|
||||
{ label: 'QUERY', value: 'QUERY' },
|
||||
{ label: 'BODY', value: 'BODY' },
|
||||
]" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'isRequired'">
|
||||
<a-switch v-model:checked="record.isRequired" :checked-value="1" :un-checked-value="0" size="small" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'defaultValue'">
|
||||
<a-input v-model:value="record.defaultValue" size="small" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'paramDesc'">
|
||||
<a-input v-model:value="record.paramDesc" size="small" />
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-button type="link" danger size="small" @click="paramsData.splice(index, 1)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="sql" tab="SQL模板">
|
||||
<SQLTemplate />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
240
hzhub-admin/apps/web-antd/src/views/erp/api/data.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* ERP API 配置管理 - 数据定义
|
||||
*/
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
/**
|
||||
* 搜索表单 Schema
|
||||
*/
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiName',
|
||||
label: 'API名称',
|
||||
componentProps: {
|
||||
placeholder: '请输入API名称',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiPath',
|
||||
label: 'API路径',
|
||||
componentProps: {
|
||||
placeholder: '请输入API路径',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'apiMethod',
|
||||
label: 'HTTP方法',
|
||||
componentProps: {
|
||||
placeholder: '请选择HTTP方法',
|
||||
options: [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
],
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'status',
|
||||
label: '状态',
|
||||
componentProps: {
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '启用', value: '1' },
|
||||
{ label: '禁用', value: '0' },
|
||||
],
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 表格列定义
|
||||
*/
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'apiName',
|
||||
title: 'API名称',
|
||||
minWidth: 150,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'apiPath',
|
||||
title: 'API路径',
|
||||
minWidth: 200,
|
||||
showOverflow: 'tooltip',
|
||||
},
|
||||
{
|
||||
field: 'apiMethod',
|
||||
title: 'HTTP方法',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'apiVersion',
|
||||
title: '版本',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
field: 'resultType',
|
||||
title: '结果类型',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
field: 'supportPagination',
|
||||
title: '分页',
|
||||
width: 80,
|
||||
slots: { default: 'pagination' },
|
||||
},
|
||||
{
|
||||
field: 'requireAuth',
|
||||
title: '认证',
|
||||
width: 80,
|
||||
slots: { default: 'auth' },
|
||||
},
|
||||
{
|
||||
field: 'enableCache',
|
||||
title: '缓存',
|
||||
width: 80,
|
||||
slots: { default: 'cache' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 100,
|
||||
slots: { default: 'status' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 150,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 250,
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* API方法选项
|
||||
*/
|
||||
export const apiMethodOptions = [
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
];
|
||||
|
||||
/**
|
||||
* API版本选项
|
||||
*/
|
||||
export const apiVersionOptions = [
|
||||
{ label: 'v1', value: 'v1' },
|
||||
{ label: 'v2', value: 'v2' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 结果类型选项
|
||||
*/
|
||||
export const resultTypeOptions = [
|
||||
{ label: '列表', value: 'LIST' },
|
||||
{ label: '单条', value: 'SINGLE' },
|
||||
{ label: '计数', value: 'COUNT' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 参数类型选项
|
||||
*/
|
||||
export const paramTypeOptions = [
|
||||
{ label: 'String', value: 'String' },
|
||||
{ label: 'Integer', value: 'Integer' },
|
||||
{ label: 'Long', value: 'Long' },
|
||||
{ label: 'Double', value: 'Double' },
|
||||
{ label: 'Date', value: 'Date' },
|
||||
{ label: 'DateTime', value: 'DateTime' },
|
||||
{ label: 'Boolean', value: 'Boolean' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 参数位置选项
|
||||
*/
|
||||
export const paramPositionOptions = [
|
||||
{ label: 'QUERY', value: 'QUERY' },
|
||||
{ label: 'BODY', value: 'BODY' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 数据源选项(待扩展)
|
||||
*/
|
||||
export const dataSourceOptions = [
|
||||
{ label: 'ERP数据源', value: 'erp' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 参数配置表格列定义
|
||||
*/
|
||||
export const paramColumns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'paramName',
|
||||
title: '参数名称',
|
||||
minWidth: 150,
|
||||
editRender: { name: 'input' },
|
||||
},
|
||||
{
|
||||
field: 'paramType',
|
||||
title: '参数类型',
|
||||
width: 120,
|
||||
editRender: {
|
||||
name: 'select',
|
||||
options: paramTypeOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'paramPosition',
|
||||
title: '参数位置',
|
||||
width: 100,
|
||||
editRender: {
|
||||
name: 'select',
|
||||
options: paramPositionOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'isRequired',
|
||||
title: '必填',
|
||||
width: 80,
|
||||
editRender: { name: 'checkbox' },
|
||||
},
|
||||
{
|
||||
field: 'defaultValue',
|
||||
title: '默认值',
|
||||
minWidth: 120,
|
||||
editRender: { name: 'input' },
|
||||
},
|
||||
{
|
||||
field: 'paramDesc',
|
||||
title: '参数描述',
|
||||
minWidth: 200,
|
||||
editRender: { name: 'input' },
|
||||
},
|
||||
{
|
||||
field: 'sort',
|
||||
title: '排序',
|
||||
width: 80,
|
||||
editRender: { name: 'input', attrs: { type: 'number' } },
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 100,
|
||||
slots: { default: 'paramAction' },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Tabs, TabPane, Spin, message, Card } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { apiConfigPreview } from '#/api/erp/api';
|
||||
|
||||
const apiId = ref<number>();
|
||||
const apiName = ref<string>();
|
||||
const docData = ref<Record<string, string>>({});
|
||||
const loading = ref(false);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const data = modalApi.getData() as { apiId: number; apiName: string };
|
||||
apiId.value = data.apiId;
|
||||
apiName.value = data.apiName;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await apiConfigPreview(data.apiId);
|
||||
docData.value = response;
|
||||
} catch (error) {
|
||||
message.error('加载文档失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="`API文档 - ${apiName}`"
|
||||
class="w-[700px]"
|
||||
:footer="null"
|
||||
>
|
||||
<Spin v-if="loading" />
|
||||
<Tabs v-else>
|
||||
<TabPane key="basic" tab="基本信息">
|
||||
<Card>
|
||||
<pre class="doc-output">{{ docData.basic || '无基本信息' }}</pre>
|
||||
</Card>
|
||||
</TabPane>
|
||||
<TabPane key="params" tab="参数说明">
|
||||
<Card>
|
||||
<pre class="doc-output">{{ docData.params || '无参数' }}</pre>
|
||||
</Card>
|
||||
</TabPane>
|
||||
<TabPane key="sql" tab="SQL模板">
|
||||
<Card>
|
||||
<pre class="sql-output">{{ docData.sql || '' }}</pre>
|
||||
</Card>
|
||||
</TabPane>
|
||||
<TabPane key="example" tab="使用示例">
|
||||
<Card>
|
||||
<div v-if="docData.example">
|
||||
<pre class="doc-output">{{ docData.example }}</pre>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>暂无使用示例</p>
|
||||
</div>
|
||||
</Card>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.doc-output,
|
||||
.sql-output {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
149
hzhub-admin/apps/web-antd/src/views/erp/api/edit-api.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { Card, Tabs, TabPane, Button, Space, Spin, message } from 'ant-design-vue';
|
||||
import { ref, onMounted, provide, useTemplateRef } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
import { apiConfigInfo, apiConfigAdd, apiConfigEdit } from '#/api/erp/api';
|
||||
import type { ErpApiConfig, ErpApiParam } from '#/api/erp/api';
|
||||
|
||||
import BasicSetting from './edit-tabs/basic-setting.vue';
|
||||
import ParamsConfig from './edit-tabs/params-config.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const apiId = route.query.apiId as string;
|
||||
const isUpdate = ref<boolean>(!!apiId);
|
||||
|
||||
const apiData = ref<ErpApiConfig>({
|
||||
apiId: undefined,
|
||||
apiName: '',
|
||||
apiPath: '',
|
||||
apiMethod: 'GET',
|
||||
apiDesc: '',
|
||||
apiVersion: 'v1',
|
||||
dataSource: 'erp',
|
||||
sqlTemplate: '',
|
||||
resultType: 'LIST',
|
||||
supportPagination: 0,
|
||||
pageParamName: 'pageNum',
|
||||
sizeParamName: 'pageSize',
|
||||
requireAuth: 0,
|
||||
permissionCode: '',
|
||||
enableCache: 0,
|
||||
cacheKeyTemplate: '',
|
||||
cacheTtl: 300,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const paramsData = ref<ErpApiParam[]>([]);
|
||||
const loading = ref(false);
|
||||
const currentTab = ref('basic');
|
||||
|
||||
// 子组件引用
|
||||
const basicSettingRef = useTemplateRef('basicSettingRef');
|
||||
const paramsConfigRef = useTemplateRef('paramsConfigRef');
|
||||
|
||||
// 提供数据给子组件
|
||||
provide('apiData', apiData);
|
||||
provide('paramsData', paramsData);
|
||||
|
||||
// 加载API配置详情
|
||||
async function loadApiInfo() {
|
||||
if (!apiId) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await apiConfigInfo(Number(apiId));
|
||||
apiData.value = response.info;
|
||||
paramsData.value = response.params || [];
|
||||
} catch (error) {
|
||||
message.error('加载API配置失败');
|
||||
router.push({ path: '/erp/api' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApiInfo();
|
||||
});
|
||||
|
||||
// 保存配置
|
||||
async function handleSave() {
|
||||
try {
|
||||
// 验证表单
|
||||
const valid = await basicSettingRef.value?.validateForm();
|
||||
if (!valid) {
|
||||
message.error('请填写必填字段');
|
||||
currentTab.value = 'basic';
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取表单数据
|
||||
const formValues = await basicSettingRef.value?.getFormValues();
|
||||
if (!formValues) {
|
||||
message.error('获取表单数据失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (!formValues.apiName || !formValues.apiPath || !formValues.sqlTemplate) {
|
||||
message.error('请填写必填字段:API名称、API路径、SQL模板');
|
||||
currentTab.value = 'basic';
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取参数配置数据
|
||||
const params = paramsConfigRef.value?.getTableData() || [];
|
||||
|
||||
// 合并数据
|
||||
const submitData = {
|
||||
...formValues,
|
||||
params,
|
||||
};
|
||||
|
||||
if (isUpdate.value) {
|
||||
await apiConfigEdit(submitData);
|
||||
message.success('修改成功');
|
||||
} else {
|
||||
await apiConfigAdd(submitData);
|
||||
message.success('新增成功');
|
||||
}
|
||||
|
||||
router.push({ path: '/erp/api' });
|
||||
} catch (error: any) {
|
||||
message.error('保存失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
function handleBack() {
|
||||
router.push({ path: '/erp/api' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<Card>
|
||||
<Spin v-if="loading" />
|
||||
<Tabs v-else v-model:activeKey="currentTab">
|
||||
<template #rightExtra>
|
||||
<Space>
|
||||
<Button @click="handleBack">返回</Button>
|
||||
<Button type="primary" @click="handleSave">保存配置</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<TabPane key="basic" tab="基础设置">
|
||||
<BasicSetting ref="basicSettingRef" />
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="params" tab="参数配置">
|
||||
<ParamsConfig ref="paramsConfigRef" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,240 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { inject, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
apiMethodOptions,
|
||||
apiVersionOptions,
|
||||
resultTypeOptions,
|
||||
dataSourceOptions,
|
||||
} from '../data';
|
||||
import type { ErpApiConfig } from '#/api/erp/api';
|
||||
|
||||
const apiData = inject<ref<ErpApiConfig>>('apiData')!;
|
||||
|
||||
const schema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiId',
|
||||
label: 'API ID',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: ['apiId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiName',
|
||||
label: 'API名称',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入API名称',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'apiPath',
|
||||
label: 'API路径',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '例如:/erp/dynamic/v1/customer_list',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'apiMethod',
|
||||
label: 'HTTP方法',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: apiMethodOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputTextArea',
|
||||
fieldName: 'apiDesc',
|
||||
label: 'API描述',
|
||||
componentProps: {
|
||||
placeholder: '请输入API描述',
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'apiVersion',
|
||||
label: 'API版本',
|
||||
componentProps: {
|
||||
options: apiVersionOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'dataSource',
|
||||
label: '数据源',
|
||||
componentProps: {
|
||||
options: dataSourceOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputTextArea',
|
||||
fieldName: 'sqlTemplate',
|
||||
label: 'SQL模板',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
placeholder: '请输入SQL模板,支持参数占位符 #{paramName}',
|
||||
rows: 6,
|
||||
},
|
||||
helpMessage: '使用 #{paramName} 作为参数占位符,例如:SELECT * FROM table WHERE #{id} IS NOT NULL THEN id = #{id}',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'resultType',
|
||||
label: '结果类型',
|
||||
rules: 'required',
|
||||
componentProps: {
|
||||
options: resultTypeOptions,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'supportPagination',
|
||||
label: '支持分页',
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'pageParamName',
|
||||
label: '页码参数名',
|
||||
dependencies: {
|
||||
show: (values) => values.supportPagination === 1,
|
||||
triggerFields: ['supportPagination'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '默认:pageNum',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'sizeParamName',
|
||||
label: '页大小参数名',
|
||||
dependencies: {
|
||||
show: (values) => values.supportPagination === 1,
|
||||
triggerFields: ['supportPagination'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '默认:pageSize',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'requireAuth',
|
||||
label: '需要认证',
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'permissionCode',
|
||||
label: '权限标识',
|
||||
dependencies: {
|
||||
show: (values) => values.requireAuth === 1,
|
||||
triggerFields: ['requireAuth'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '例如:erp:customer:list',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Switch',
|
||||
fieldName: 'enableCache',
|
||||
label: '启用缓存',
|
||||
componentProps: {
|
||||
checkedChildren: '是',
|
||||
unCheckedChildren: '否',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'cacheKeyTemplate',
|
||||
label: '缓存键模板',
|
||||
dependencies: {
|
||||
show: (values) => values.enableCache === 1,
|
||||
triggerFields: ['enableCache'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '支持参数占位符',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'cacheTtl',
|
||||
label: '缓存过期时间',
|
||||
dependencies: {
|
||||
show: (values) => values.enableCache === 1,
|
||||
triggerFields: ['enableCache'],
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '秒',
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'sourceTable',
|
||||
label: '来源表名',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
placeholder: '如果是从表导入生成的,会显示来源表名',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputTextArea',
|
||||
fieldName: 'remark',
|
||||
label: '备注',
|
||||
componentProps: {
|
||||
placeholder: '请输入备注',
|
||||
rows: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelWidth: 120,
|
||||
},
|
||||
schema: schema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
// 监听数据变化,同步到表单
|
||||
watch(apiData, (newData) => {
|
||||
if (newData) {
|
||||
formApi.setValues(newData);
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 获取表单数据(供父组件调用)
|
||||
function getFormValues() {
|
||||
return formApi.getValues();
|
||||
}
|
||||
|
||||
// 验证表单(供父组件调用)
|
||||
async function validateForm() {
|
||||
const { valid } = await formApi.validate();
|
||||
return valid;
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
getFormValues,
|
||||
validateForm,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicForm />
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { Button, Space, message } from 'ant-design-vue';
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import { paramColumns } from '../data';
|
||||
import type { ErpApiParam } from '#/api/erp/api';
|
||||
|
||||
const paramsData = inject<ref<ErpApiParam[]>>('paramsData')!;
|
||||
|
||||
const gridOptions: VxeGridProps<ErpApiParam> = {
|
||||
columns: paramColumns,
|
||||
data: paramsData.value,
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
showStatus: true,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'paramId',
|
||||
},
|
||||
id: 'erp-api-params-config',
|
||||
};
|
||||
|
||||
const [ParamsTable, tableApi] = useVbenVxeGrid({
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 新增参数
|
||||
function handleAddParam() {
|
||||
const newParam: ErpApiParam = {
|
||||
paramId: Date.now(), // 临时ID
|
||||
apiId: 0,
|
||||
paramName: '',
|
||||
paramType: 'String',
|
||||
paramPosition: 'QUERY',
|
||||
isRequired: 0,
|
||||
defaultValue: '',
|
||||
paramDesc: '',
|
||||
sort: paramsData.value.length,
|
||||
};
|
||||
|
||||
tableApi.insert(newParam);
|
||||
}
|
||||
|
||||
// 删除选中参数
|
||||
async function handleDeleteParam() {
|
||||
const selectRows = tableApi.getSelectRows();
|
||||
if (selectRows.length === 0) {
|
||||
message.warning('请选择要删除的参数');
|
||||
return;
|
||||
}
|
||||
|
||||
await tableApi.remove(selectRows);
|
||||
message.success('删除成功');
|
||||
}
|
||||
|
||||
// 获取表格数据(供父组件调用)
|
||||
function getTableData(): ErpApiParam[] {
|
||||
return tableApi.getTableData().fullData;
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
getTableData,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="params-config-container">
|
||||
<div class="toolbar mb-2">
|
||||
<Space>
|
||||
<Button type="primary" @click="handleAddParam">新增参数</Button>
|
||||
<Button danger @click="handleDeleteParam">删除选中</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<ParamsTable>
|
||||
<template #paramAction="{ row }">
|
||||
<Button size="small" type="link" danger @click="tableApi.remove(row)">
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</ParamsTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.params-config-container {
|
||||
padding: 16px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,293 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { Button, Card, Col, Divider, Input, Row, Space, Spin, message } from 'ant-design-vue';
|
||||
|
||||
import { inject } from 'vue';
|
||||
import type { ErpApiConfig, ErpApiParam } from '#/api/erp/api';
|
||||
|
||||
// 接收父组件注入的数据
|
||||
const apiData = inject<ref<ErpApiConfig>>('apiData')!;
|
||||
const paramsData = inject<ref<ErpApiParam[]>>('paramsData')!;
|
||||
|
||||
// SQL模板内容
|
||||
const sqlTemplate = ref<string>('');
|
||||
|
||||
// 监听数据变化,同步SQL模板
|
||||
watch(apiData, (newData) => {
|
||||
if (newData && newData.sqlTemplate) {
|
||||
sqlTemplate.value = newData.sqlTemplate;
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 监听SQL模板变化,同步回父组件
|
||||
watch(sqlTemplate, (newSql) => {
|
||||
if (apiData.value) {
|
||||
apiData.value.sqlTemplate = newSql;
|
||||
}
|
||||
});
|
||||
|
||||
// 测试参数值(用于预览)
|
||||
const testParamValues = ref<Record<string, any>>({});
|
||||
|
||||
// 初始化测试参数值
|
||||
watch(paramsData, (params) => {
|
||||
const values: Record<string, any> = {};
|
||||
params.forEach((param) => {
|
||||
// 如果参数有默认值,使用默认值;否则使用示例值
|
||||
values[param.paramName] = param.defaultValue || getExampleValue(param.paramType);
|
||||
});
|
||||
testParamValues.value = values;
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// 根据参数类型生成示例值
|
||||
function getExampleValue(paramType: string): any {
|
||||
switch (paramType) {
|
||||
case 'String': return '示例值';
|
||||
case 'Integer': return 123;
|
||||
case 'Long': return 123456789;
|
||||
case 'Double': return 123.45;
|
||||
case 'Date': return '2026-04-30';
|
||||
case 'Boolean': return true;
|
||||
default: return '示例';
|
||||
}
|
||||
}
|
||||
|
||||
// 参数注入后的SQL预览
|
||||
const previewSql = computed(() => {
|
||||
let sql = sqlTemplate.value;
|
||||
|
||||
// 替换 #{paramName} 为参数值
|
||||
const paramPattern = /#\{(\w+)\}/g;
|
||||
sql = sql.replace(paramPattern, (match, paramName) => {
|
||||
const value = testParamValues.value[paramName];
|
||||
if (value === undefined || value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
// 根据参数值类型格式化
|
||||
if (typeof value === 'string') {
|
||||
return `'${value}'`;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? '1' : '0';
|
||||
} else {
|
||||
return String(value);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理 #{param} IS NOT NULL THEN ... 逻辑
|
||||
// 移除参数为null的条件
|
||||
const conditionPattern = /AND\s+#\{(\w+)\}\s+IS\s+NOT\s+NULL\s+THEN\s+(.+?)(?=\s+AND|#|$)/g;
|
||||
sql = sql.replace(conditionPattern, (match, paramName, condition) => {
|
||||
const value = testParamValues.value[paramName];
|
||||
if (value !== undefined && value !== null) {
|
||||
// 参数有值,保留条件(替换 #{param} 为实际值)
|
||||
return `AND ${condition.replace(new RegExp(`#\\{${paramName}\\}`, 'g'), getFormattedValue(value))}`;
|
||||
} else {
|
||||
// 参数为null,移除整个条件
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
return sql;
|
||||
});
|
||||
|
||||
// 格式化参数值
|
||||
function getFormattedValue(value: any): string {
|
||||
if (typeof value === 'string') {
|
||||
return `'${value}'`;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? '1' : '0';
|
||||
} else {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
// SQL语法验证
|
||||
const validating = ref(false);
|
||||
async function validateSql() {
|
||||
validating.value = true;
|
||||
try {
|
||||
// 简单的SQL关键字验证
|
||||
const sql = sqlTemplate.value.toUpperCase();
|
||||
|
||||
// 必须包含SELECT
|
||||
if (!sql.includes('SELECT')) {
|
||||
message.error('SQL模板必须包含SELECT关键字');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁止危险操作
|
||||
const dangerousKeywords = ['DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE', 'GRANT', 'INSERT', 'UPDATE'];
|
||||
for (const keyword of dangerousKeywords) {
|
||||
if (sql.includes(keyword)) {
|
||||
message.error(`SQL模板不能包含危险关键字:${keyword}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
message.success('SQL语法验证通过');
|
||||
} finally {
|
||||
validating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制SQL
|
||||
function copySql() {
|
||||
navigator.clipboard.writeText(sqlTemplate.value);
|
||||
message.success('SQL模板已复制');
|
||||
}
|
||||
|
||||
// 复制预览SQL
|
||||
function copyPreviewSql() {
|
||||
navigator.clipboard.writeText(previewSql.value);
|
||||
message.success('参数注入后的SQL已复制');
|
||||
}
|
||||
|
||||
// SQL模板片段快速插入
|
||||
const sqlSnippets = [
|
||||
{ label: 'SELECT *', value: 'SELECT *\nFROM table_name' },
|
||||
{ label: 'WHERE条件', value: 'WHERE #{param} IS NOT NULL THEN field = #{param}' },
|
||||
{ label: 'ORDER BY', value: 'ORDER BY field_name ASC' },
|
||||
{ label: '参数占位符', value: '#{}' },
|
||||
];
|
||||
|
||||
function insertSnippet(snippet: string) {
|
||||
sqlTemplate.value += '\n' + snippet;
|
||||
message.success('模板片段已插入');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sql-editor-container">
|
||||
<Row :gutter="16">
|
||||
<!-- 左侧:SQL模板编辑 -->
|
||||
<Col :span="12">
|
||||
<Card title="SQL模板" size="small">
|
||||
<template #extra>
|
||||
<Space>
|
||||
<Button size="small" :loading="validating" @click="validateSql">
|
||||
验证SQL
|
||||
</Button>
|
||||
<Button size="small" @click="copySql">
|
||||
复制
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<!-- 快速插入 -->
|
||||
<div class="mb-2">
|
||||
<Space>
|
||||
<span class="text-sm text-gray-500">快速插入:</span>
|
||||
<Button
|
||||
v-for="snippet in sqlSnippets"
|
||||
:key="snippet.label"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="insertSnippet(snippet.value)"
|
||||
>
|
||||
{{ snippet.label }}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- SQL编辑器 -->
|
||||
<Input.TextArea
|
||||
v-model:value="sqlTemplate"
|
||||
:rows="15"
|
||||
placeholder="请输入SQL模板,使用 #{paramName} 作为参数占位符"
|
||||
class="sql-editor"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<!-- 参数列表提示 -->
|
||||
<div class="text-sm text-gray-600">
|
||||
<strong>可用参数:</strong>
|
||||
<div v-if="paramsData.length > 0" class="mt-2">
|
||||
<Space wrap>
|
||||
<span v-for="param in paramsData" :key="param.paramName">
|
||||
<code class="bg-gray-100 px-1 rounded">#{{ '{' + param.paramName + '}' }}</code>
|
||||
<span class="text-gray-500">({{ param.paramType }})</span>
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
<div v-else class="text-warning">
|
||||
请在"参数配置"Tab中添加参数
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧:参数注入预览 -->
|
||||
<Col :span="12">
|
||||
<Card title="参数注入预览" size="small">
|
||||
<template #extra>
|
||||
<Space>
|
||||
<Button size="small" @click="copyPreviewSql">
|
||||
复制预览SQL
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<!-- 参数值输入 -->
|
||||
<div v-if="paramsData.length > 0" class="mb-4">
|
||||
<div class="text-sm text-gray-600 mb-2">参数值设置:</div>
|
||||
<div class="param-inputs">
|
||||
<Row :gutter="[8, 8]">
|
||||
<Col v-for="param in paramsData" :key="param.paramName" :span="12">
|
||||
<div class="flex items-center">
|
||||
<span class="w-24 text-sm">{{ param.paramName }}:</span>
|
||||
<Input
|
||||
v-model:value="testParamValues[param.paramName]"
|
||||
size="small"
|
||||
:placeholder="`示例: ${getExampleValue(param.paramType)}`"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览SQL -->
|
||||
<Input.TextArea
|
||||
:value="previewSql"
|
||||
:rows="15"
|
||||
readonly
|
||||
placeholder="设置参数值后,将显示参数注入后的实际SQL"
|
||||
class="sql-preview bg-gray-50"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<!-- 说明 -->
|
||||
<div class="text-sm text-gray-500">
|
||||
<strong>说明:</strong>
|
||||
<ul class="mt-1 pl-4 list-disc">
|
||||
<li>左侧编辑SQL模板,使用 #{{ '{paramName}' }} 作为参数占位符</li>
|
||||
<li>右侧设置参数值,实时预览参数注入后的实际SQL</li>
|
||||
<li>#{param} IS NOT NULL THEN ... 条件会在参数为NULL时自动移除</li>
|
||||
<li>字符串类型参数会自动添加单引号</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sql-editor-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sql-editor,
|
||||
.sql-preview {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #faad14;
|
||||
}
|
||||
</style>
|
||||
248
hzhub-admin/apps/web-antd/src/views/erp/api/index.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { Button, Popconfirm, Space, Switch, Tag, message } from 'ant-design-vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
apiConfigList,
|
||||
apiConfigRemove,
|
||||
apiConfigChangeStatus,
|
||||
apiConfigTest,
|
||||
apiConfigClearCache,
|
||||
} from '#/api/erp/api';
|
||||
import type { ErpApiConfigVO } from '#/api/erp/api';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import TestModal from './test-modal.vue';
|
||||
import DocPreviewModal from './doc-preview-modal.vue';
|
||||
import ApiDrawer from './api-drawer.vue';
|
||||
|
||||
// 搜索表单配置
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
// 表格配置
|
||||
const gridOptions: VxeGridProps<ErpApiConfigVO> = {
|
||||
checkboxConfig: {
|
||||
highlight: true,
|
||||
reserve: true,
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
const response = await apiConfigList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'apiId',
|
||||
},
|
||||
id: 'erp-api-config-list',
|
||||
};
|
||||
|
||||
// 初始化表格
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 测试弹窗
|
||||
const [TestModalComp, testModalApi] = useVbenModal({
|
||||
connectedComponent: TestModal,
|
||||
});
|
||||
|
||||
// 文档预览弹窗
|
||||
const [DocModalComp, docModalApi] = useVbenModal({
|
||||
connectedComponent: DocPreviewModal,
|
||||
});
|
||||
|
||||
// 新增/编辑Drawer
|
||||
const [ApiDrawerComp, apiDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: ApiDrawer,
|
||||
});
|
||||
|
||||
// 新增API配置
|
||||
function handleAdd() {
|
||||
apiDrawerApi.setData({});
|
||||
apiDrawerApi.open();
|
||||
}
|
||||
|
||||
// 编辑API配置
|
||||
function handleEdit(record: ErpApiConfigVO) {
|
||||
apiDrawerApi.setData({ apiId: record.apiId });
|
||||
apiDrawerApi.open();
|
||||
}
|
||||
|
||||
// 测试API
|
||||
async function handleTest(record: ErpApiConfigVO) {
|
||||
testModalApi.setData({ apiId: record.apiId, apiName: record.apiName });
|
||||
testModalApi.open();
|
||||
}
|
||||
|
||||
// 文档预览
|
||||
async function handlePreview(record: ErpApiConfigVO) {
|
||||
docModalApi.setData({ apiId: record.apiId, apiName: record.apiName });
|
||||
docModalApi.open();
|
||||
}
|
||||
|
||||
// 删除API配置
|
||||
async function handleDelete(record: ErpApiConfigVO) {
|
||||
await apiConfigRemove([record.apiId]);
|
||||
message.success('删除成功');
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
async function handleMultiDelete() {
|
||||
const rows = tableApi.getSelectRows();
|
||||
if (!rows.length) {
|
||||
message.warning('请选择要删除的数据');
|
||||
return;
|
||||
}
|
||||
const apiIds = rows.map((row) => row.apiId);
|
||||
await apiConfigRemove(apiIds);
|
||||
message.success('删除成功');
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
async function handleStatusChange(record: ErpApiConfigVO, status: boolean) {
|
||||
await apiConfigChangeStatus({
|
||||
apiId: record.apiId,
|
||||
status: status ? 1 : 0,
|
||||
});
|
||||
message.success('状态更新成功');
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
async function handleClearCache(record: ErpApiConfigVO) {
|
||||
await apiConfigClearCache(record.apiId);
|
||||
message.success('缓存清除成功');
|
||||
}
|
||||
|
||||
// 从表导入(待实现)
|
||||
function handleImportFromTable() {
|
||||
message.info('从表导入功能正在开发中');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="API配置列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<Button v-access:code="['erp:api:add']" type="primary" @click="handleAdd">
|
||||
新增
|
||||
</Button>
|
||||
<Button v-access:code="['erp:api:add']" @click="handleImportFromTable">
|
||||
从表导入
|
||||
</Button>
|
||||
<Button
|
||||
v-access:code="['erp:api:remove']"
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
||||
<template #pagination="{ row }">
|
||||
<Tag v-if="row.supportPagination === 1" color="success">是</Tag>
|
||||
<Tag v-else color="default">否</Tag>
|
||||
</template>
|
||||
|
||||
<template #auth="{ row }">
|
||||
<Tag v-if="row.requireAuth === 1" color="warning">需要</Tag>
|
||||
<Tag v-else color="default">无需</Tag>
|
||||
</template>
|
||||
|
||||
<template #cache="{ row }">
|
||||
<Tag v-if="row.enableCache === 1" color="processing">启用</Tag>
|
||||
<Tag v-else color="default">未启用</Tag>
|
||||
</template>
|
||||
|
||||
<template #status="{ row }">
|
||||
<Switch
|
||||
:checked="row.status === 1"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
@change="(checked: boolean) => handleStatusChange(row, checked)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<Button
|
||||
v-access:code="['erp:api:test']"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleTest(row)"
|
||||
>
|
||||
测试
|
||||
</Button>
|
||||
<Button size="small" type="link" @click="handlePreview(row)">
|
||||
文档
|
||||
</Button>
|
||||
<Button
|
||||
v-access:code="['erp:api:edit']"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除此API配置?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<Button
|
||||
v-access:code="['erp:api:remove']"
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button
|
||||
v-if="row.enableCache === 1"
|
||||
v-access:code="['erp:api:cache']"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleClearCache(row)"
|
||||
>
|
||||
清缓存
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<TestModalComp @success="tableApi.query()" />
|
||||
<DocModalComp />
|
||||
<ApiDrawerComp @reload="tableApi.query()" />
|
||||
</Page>
|
||||
</template>
|
||||
203
hzhub-admin/apps/web-antd/src/views/erp/api/test-modal.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { Button, Descriptions, DescriptionsItem, Input, message, Spin, Alert, Tabs, TabPane } from 'ant-design-vue';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import { apiConfigTest, apiConfigInfo } from '#/api/erp/api';
|
||||
import type { ApiTestResultVO, ErpApiParam } from '#/api/erp/api';
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const apiId = ref<number>();
|
||||
const apiName = ref<string>();
|
||||
const params = ref<Record<string, any>>({});
|
||||
const testResult = ref<ApiTestResultVO>();
|
||||
const loading = ref(false);
|
||||
const apiParamsConfig = ref<ErpApiParam[]>([]);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = modalApi.getData() as { apiId: number; apiName: string };
|
||||
apiId.value = data.apiId;
|
||||
apiName.value = data.apiName;
|
||||
|
||||
// 加载参数配置
|
||||
try {
|
||||
const response = await apiConfigInfo(data.apiId);
|
||||
apiParamsConfig.value = response.params || [];
|
||||
|
||||
// 初始化参数默认值
|
||||
const defaultParams: Record<string, any> = {};
|
||||
apiParamsConfig.value.forEach((param) => {
|
||||
if (param.defaultValue) {
|
||||
defaultParams[param.paramName] = param.defaultValue;
|
||||
}
|
||||
});
|
||||
params.value = defaultParams;
|
||||
} catch (error) {
|
||||
message.error('加载参数配置失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function resetState() {
|
||||
testResult.value = undefined;
|
||||
params.value = {};
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function handleExecute() {
|
||||
if (!apiId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
testResult.value = undefined;
|
||||
|
||||
try {
|
||||
const result = await apiConfigTest(apiId.value, params.value);
|
||||
testResult.value = result;
|
||||
|
||||
if (result.success) {
|
||||
message.success('测试成功');
|
||||
} else {
|
||||
message.error('测试失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error('测试执行失败: ' + (error.message || '未知错误'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const resultJson = computed(() => {
|
||||
if (!testResult.value?.data) return '';
|
||||
return JSON.stringify(testResult.value.data, null, 2);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="`API测试 - ${apiName}`"
|
||||
class="w-[800px]"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="test-container">
|
||||
<!-- 参数输入区 -->
|
||||
<div class="params-section mb-4">
|
||||
<h4 class="mb-2">输入参数</h4>
|
||||
<div v-if="apiParamsConfig.length > 0" class="param-inputs">
|
||||
<div v-for="param in apiParamsConfig" :key="param.paramId" class="param-item">
|
||||
<label class="mb-1">{{ param.paramName }} ({{ param.paramType }})</label>
|
||||
<Input
|
||||
v-model:value="params[param.paramName]"
|
||||
:placeholder="param.paramDesc || `请输入${param.paramName}`"
|
||||
class="mb-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Alert type="info" message="此API无参数配置,可直接执行" />
|
||||
</div>
|
||||
<Button type="primary" :loading="loading" @click="handleExecute" class="mt-2">
|
||||
执行测试
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 执行结果区 -->
|
||||
<div v-if="testResult" class="result-section">
|
||||
<h4 class="mb-2">执行结果</h4>
|
||||
|
||||
<!-- 结果元信息 -->
|
||||
<Descriptions bordered :column="2" class="mb-3">
|
||||
<DescriptionsItem label="执行状态">
|
||||
<Alert :type="testResult.success ? 'success' : 'error'">
|
||||
{{ testResult.success ? '成功' : '失败' }}
|
||||
</Alert>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="执行时间">
|
||||
{{ testResult.executionTime }} ms
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="API路径" :span="2">
|
||||
{{ testResult.apiPath }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请求参数" :span="2">
|
||||
<pre class="params-output">{{ JSON.stringify(testResult.requestParams, null, 2) }}</pre>
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
|
||||
<!-- 成功结果 -->
|
||||
<div v-if="testResult.success">
|
||||
<Tabs>
|
||||
<TabPane key="data" tab="返回数据">
|
||||
<pre class="json-output">{{ resultJson }}</pre>
|
||||
</TabPane>
|
||||
<TabPane key="sql" tab="执行的SQL">
|
||||
<pre class="sql-output">{{ testResult.executedSql }}</pre>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-else>
|
||||
<Alert type="error" class="mb-2">
|
||||
<template #message>
|
||||
<strong>错误信息:</strong>
|
||||
<div>{{ testResult.errorMessage }}</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<details>
|
||||
<summary>查看详细错误堆栈</summary>
|
||||
<pre class="error-stack">{{ testResult.errorStack }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spin v-else-if="loading" class="loading-spinner" />
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.test-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.param-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-stack {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.json-output,
|
||||
.sql-output,
|
||||
.params-output {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { Modal, Descriptions, Tag, Divider, Alert, Collapse, CollapsePanel } from 'ant-design-vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface ErrorDetail {
|
||||
statsId: number;
|
||||
apiId: number;
|
||||
callTime: string;
|
||||
callParams: string;
|
||||
executedSql: string;
|
||||
responseTime: number;
|
||||
callStatus: string;
|
||||
errorMessage: string;
|
||||
errorStack: string;
|
||||
clientIp: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record: ErrorDetail | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const modalVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value),
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// 格式化JSON参数
|
||||
function formatParams(paramsStr: string) {
|
||||
try {
|
||||
const params = JSON.parse(paramsStr);
|
||||
return JSON.stringify(params, null, 2);
|
||||
} catch {
|
||||
return paramsStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 简化错误堆栈(只显示前10行)
|
||||
function simplifyStack(stack: string) {
|
||||
if (!stack) return '';
|
||||
const lines = stack.split('\n').slice(0, 10);
|
||||
return lines.join('\n');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
title="错误详情"
|
||||
width="80%"
|
||||
:footer="null"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<div v-if="record">
|
||||
<!-- 基本信息 -->
|
||||
<Descriptions title="调用信息" bordered :column="2" size="small" class="mb-4">
|
||||
<Descriptions.Item label="调用时间">
|
||||
{{ record.callTime }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="响应时间">
|
||||
<Tag color="error">{{ record.responseTime }}ms</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="客户端IP">
|
||||
{{ record.clientIp }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="用户ID">
|
||||
{{ record.userId }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="调用状态">
|
||||
<Tag color="error">ERROR</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="统计ID">
|
||||
{{ record.statsId }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider />
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<Alert
|
||||
type="error"
|
||||
:message="record.errorMessage"
|
||||
class="mb-4"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<!-- 调用参数 -->
|
||||
<Collapse class="mb-4">
|
||||
<CollapsePanel key="params" header="调用参数">
|
||||
<pre class="code-block">{{ formatParams(record.callParams) }}</pre>
|
||||
</CollapsePanel>
|
||||
</Collapse>
|
||||
|
||||
<!-- 执行的SQL -->
|
||||
<Collapse class="mb-4">
|
||||
<CollapsePanel key="sql" header="执行的SQL">
|
||||
<pre class="code-block">{{ record.executedSql }}</pre>
|
||||
</CollapsePanel>
|
||||
</Collapse>
|
||||
|
||||
<!-- 错误堆栈 -->
|
||||
<Collapse>
|
||||
<CollapsePanel key="stack" header="错误堆栈(前10行)">
|
||||
<pre class="code-block error-stack">{{ simplifyStack(record.errorStack) }}</pre>
|
||||
</CollapsePanel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
<Alert v-else type="warning" message="未加载错误详情数据" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #f5f5f5;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.error-stack {
|
||||
color: #cf1322;
|
||||
background: #fff1f0;
|
||||
}
|
||||
</style>
|
||||
588
hzhub-admin/apps/web-antd/src/views/erp/stats/index.vue
Normal file
@@ -0,0 +1,588 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
Row,
|
||||
Statistic,
|
||||
Table,
|
||||
Tabs,
|
||||
TabPane,
|
||||
DatePicker,
|
||||
Space,
|
||||
Tag,
|
||||
Progress,
|
||||
Empty,
|
||||
Alert,
|
||||
Divider,
|
||||
Select,
|
||||
Button,
|
||||
Spin,
|
||||
message,
|
||||
Switch,
|
||||
InputNumber,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import { ref, onMounted, computed, watch, onUnmounted } from 'vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { apiConfigList, apiConfigStats, apiConfigErrorLog } from '#/api/erp/api';
|
||||
import type { ErpApiConfigVO } from '#/api/erp/api';
|
||||
import ErrorDetailModal from './error-detail-modal.vue';
|
||||
|
||||
// 时间范围选择
|
||||
const timeRange = ref<any[]>([]);
|
||||
const startTime = computed(() => timeRange.value[0] || undefined);
|
||||
const endTime = computed(() => timeRange.value[1] || undefined);
|
||||
|
||||
// API列表(用于选择查看统计)
|
||||
const apiList = ref<ErpApiConfigVO[]>([]);
|
||||
const selectedApiId = ref<number>();
|
||||
|
||||
// Loading状态
|
||||
const loading = ref(false);
|
||||
|
||||
// 慢查询阈值(毫秒)
|
||||
const slowQueryThreshold = ref<number>(1000);
|
||||
|
||||
// 自动刷新配置
|
||||
const autoRefreshEnabled = ref(false);
|
||||
const refreshInterval = ref<number>(30); // 刷新间隔(秒)
|
||||
let refreshTimer: number | null = null;
|
||||
|
||||
// 错误详情弹窗
|
||||
const errorDetailVisible = ref(false);
|
||||
const selectedErrorRecord = ref<any>(null);
|
||||
|
||||
// 加载API列表
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await apiConfigList({ pageNum: 1, pageSize: 100 });
|
||||
apiList.value = response.rows || [];
|
||||
if (apiList.value.length > 0) {
|
||||
selectedApiId.value = apiList.value[0].apiId;
|
||||
await loadStats();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载API列表失败', error);
|
||||
message.error('加载API列表失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
const statsData = ref({
|
||||
totalCalls: 0,
|
||||
successCalls: 0,
|
||||
errorCalls: 0,
|
||||
avgResponseTime: 0,
|
||||
maxResponseTime: 0,
|
||||
minResponseTime: 0,
|
||||
errorRate: 0,
|
||||
slowCalls: 0, // 慢查询数量
|
||||
cacheEnabled: false, // 是否启用缓存
|
||||
});
|
||||
|
||||
// 加载统计数据
|
||||
async function loadStats() {
|
||||
if (!selectedApiId.value) {
|
||||
message.warning('请先选择API');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const stats = await apiConfigStats(
|
||||
selectedApiId.value,
|
||||
startTime.value,
|
||||
endTime.value
|
||||
);
|
||||
|
||||
console.log('统计数据:', stats);
|
||||
|
||||
// 获取选中的API配置,检查是否启用缓存
|
||||
const selectedApi = apiList.value.find(api => api.apiId === selectedApiId.value);
|
||||
|
||||
statsData.value = {
|
||||
totalCalls: stats.totalCalls || 0,
|
||||
successCalls: stats.successCalls || 0,
|
||||
errorCalls: stats.errorCalls || 0,
|
||||
avgResponseTime: stats.avgResponseTime || 0,
|
||||
maxResponseTime: stats.maxResponseTime || 0,
|
||||
minResponseTime: stats.minResponseTime || 0,
|
||||
errorRate: stats.errorRate || 0,
|
||||
slowCalls: stats.slowCalls || 0, // TODO: 后端需要添加慢查询统计
|
||||
cacheEnabled: selectedApi?.enableCache === 1,
|
||||
};
|
||||
|
||||
// 同时加载错误日志
|
||||
await loadErrorLog();
|
||||
|
||||
message.success('统计数据已更新');
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败', error);
|
||||
message.error('加载统计数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 错误日志表格配置
|
||||
const errorLogColumns = [
|
||||
{ title: '调用时间', dataIndex: 'callTime', width: 180 },
|
||||
{ title: '响应时间', dataIndex: 'responseTime', width: 100, customRender: ({ text }) => `${text}ms` },
|
||||
{ title: '客户端IP', dataIndex: 'clientIp', width: 150 },
|
||||
{ title: '用户ID', dataIndex: 'userId', width: 120 },
|
||||
{ title: '错误消息', dataIndex: 'errorMessage', ellipsis: true, width: 200 },
|
||||
{ title: '操作', dataIndex: 'action', width: 80, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const errorLogData = ref<any[]>([]);
|
||||
|
||||
// 加载错误日志
|
||||
async function loadErrorLog() {
|
||||
if (!selectedApiId.value) return;
|
||||
|
||||
try {
|
||||
const logs = await apiConfigErrorLog(selectedApiId.value, 50);
|
||||
console.log('错误日志:', logs);
|
||||
errorLogData.value = logs || [];
|
||||
} catch (error) {
|
||||
console.error('加载错误日志失败', error);
|
||||
errorLogData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// API选择变化
|
||||
function handleApiChange(apiId: number) {
|
||||
selectedApiId.value = apiId;
|
||||
}
|
||||
|
||||
// 查询按钮
|
||||
function handleQuery() {
|
||||
loadStats();
|
||||
}
|
||||
|
||||
// 刷新按钮
|
||||
async function handleRefresh() {
|
||||
message.info('正在刷新统计数据...');
|
||||
await loadStats();
|
||||
}
|
||||
|
||||
// 重置时间范围
|
||||
function handleResetTimeRange() {
|
||||
timeRange.value = [];
|
||||
loadStats();
|
||||
}
|
||||
|
||||
// 查看错误详情
|
||||
function viewErrorDetail(record: any) {
|
||||
selectedErrorRecord.value = record;
|
||||
errorDetailVisible.value = true;
|
||||
}
|
||||
|
||||
// 关闭错误详情弹窗
|
||||
function closeErrorDetail() {
|
||||
errorDetailVisible.value = false;
|
||||
selectedErrorRecord.value = null;
|
||||
}
|
||||
|
||||
// 开启/关闭自动刷新
|
||||
function toggleAutoRefresh(enabled: boolean) {
|
||||
autoRefreshEnabled.value = enabled;
|
||||
|
||||
if (enabled) {
|
||||
startAutoRefresh();
|
||||
message.success(`已开启自动刷新,每${refreshInterval.value}秒刷新一次`);
|
||||
} else {
|
||||
stopAutoRefresh();
|
||||
message.info('已关闭自动刷新');
|
||||
}
|
||||
}
|
||||
|
||||
// 启动自动刷新定时器
|
||||
function startAutoRefresh() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
}
|
||||
|
||||
refreshTimer = window.setInterval(() => {
|
||||
if (selectedApiId.value && !loading.value) {
|
||||
loadStats();
|
||||
}
|
||||
}, refreshInterval.value * 1000);
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
function stopAutoRefresh() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听刷新间隔变化
|
||||
watch(refreshInterval, (newInterval) => {
|
||||
if (autoRefreshEnabled.value) {
|
||||
stopAutoRefresh();
|
||||
startAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onMounted(() => {
|
||||
// ... existing code
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh();
|
||||
});
|
||||
|
||||
// 当前Tab
|
||||
const currentTab = ref('overview');
|
||||
|
||||
// 汇总统计(所有API)- 使用computed自动计算
|
||||
const overallStats = computed(() => {
|
||||
return {
|
||||
totalApis: apiList.value.length,
|
||||
enabledApis: apiList.value.filter(api => api.status === 1).length,
|
||||
disabledApis: apiList.value.filter(api => api.status === 0).length,
|
||||
cachedApis: apiList.value.filter(api => api.enableCache === 1).length,
|
||||
};
|
||||
});
|
||||
|
||||
// 计算性能健康度(0-100)
|
||||
function calculateHealthScore(): number {
|
||||
if (statsData.value.totalCalls === 0) return 100;
|
||||
|
||||
// 综合考虑错误率和响应时间
|
||||
const errorScore = Math.max(0, 100 - statsData.value.errorRate * 10); // 错误率影响(每1%扣10分)
|
||||
const timeScore = Math.max(0, 100 - (statsData.value.avgResponseTime / slowQueryThreshold.value) * 20); // 响应时间影响
|
||||
|
||||
return Math.round((errorScore + timeScore) / 2);
|
||||
}
|
||||
|
||||
// 获取健康度颜色
|
||||
function getHealthColor(): string {
|
||||
const score = calculateHealthScore();
|
||||
if (score >= 80) return '#3f8600'; // 优秀
|
||||
if (score >= 60) return '#faad14'; // 一般
|
||||
return '#cf1322'; // 差
|
||||
}
|
||||
|
||||
// 获取健康度描述
|
||||
function getHealthDescription(): string {
|
||||
const score = calculateHealthScore();
|
||||
if (score >= 80) return '性能优秀,系统运行正常';
|
||||
if (score >= 60) return '性能一般,建议优化';
|
||||
if (score >= 40) return '性能较差,需要优化';
|
||||
return '性能很差,紧急优化';
|
||||
}
|
||||
|
||||
// 计算响应时间评分(0-100)
|
||||
function calculateResponseTimeScore(): number {
|
||||
if (statsData.value.maxResponseTime === 0) return 100;
|
||||
|
||||
const avgRatio = statsData.value.avgResponseTime / slowQueryThreshold.value;
|
||||
return Math.max(0, Math.min(100, 100 - avgRatio * 50));
|
||||
}
|
||||
|
||||
// 获取响应时间颜色
|
||||
function getResponseTimeColor(): string {
|
||||
const score = calculateResponseTimeScore();
|
||||
if (score >= 80) return '#3f8600';
|
||||
if (score >= 60) return '#1890ff';
|
||||
if (score >= 40) return '#faad14';
|
||||
return '#cf1322';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<!-- 上方:汇总统计卡片 -->
|
||||
<Card title="API概览" size="small" class="mb-4">
|
||||
<Row :gutter="16">
|
||||
<Col :span="6">
|
||||
<Statistic title="API总数" :value="overallStats.totalApis" />
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Statistic title="启用API" :value="overallStats.enabledApis" :value-style="{ color: '#3f8600' }" />
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Statistic title="禁用API" :value="overallStats.disabledApis" :value-style="{ color: '#cf1322' }" />
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Statistic title="启用缓存" :value="overallStats.cachedApis" :value-style="{ color: '#1890ff' }" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<!-- 下方:详细统计 -->
|
||||
<Card size="small">
|
||||
<!-- API选择和时间范围 -->
|
||||
<div class="filter-section mb-4">
|
||||
<Space wrap>
|
||||
<span>选择API:</span>
|
||||
<Select
|
||||
v-model:value="selectedApiId"
|
||||
style="width: 300px"
|
||||
placeholder="请选择API"
|
||||
>
|
||||
<Select.Option v-for="api in apiList" :key="api.apiId" :value="api.apiId">
|
||||
{{ api.apiName }} ({{ api.apiPath }})
|
||||
</Select.Option>
|
||||
</Select>
|
||||
|
||||
<span>时间范围:</span>
|
||||
<DatePicker.RangePicker
|
||||
v-model:value="timeRange"
|
||||
style="width: 240px"
|
||||
/>
|
||||
|
||||
<Button type="primary" :loading="loading" @click="handleQuery">
|
||||
查询
|
||||
</Button>
|
||||
|
||||
<Button :loading="loading" @click="handleRefresh">
|
||||
刷新
|
||||
</Button>
|
||||
|
||||
<Button @click="handleResetTimeRange">
|
||||
重置
|
||||
</Button>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Tooltip title="开启后将按设定的间隔自动刷新统计数据">
|
||||
<Space>
|
||||
<span>自动刷新:</span>
|
||||
<Switch v-model:checked="autoRefreshEnabled" @change="toggleAutoRefresh" />
|
||||
</Space>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="自动刷新的时间间隔(秒)">
|
||||
<Space v-if="autoRefreshEnabled">
|
||||
<span>间隔:</span>
|
||||
<InputNumber
|
||||
v-model:value="refreshInterval"
|
||||
:min="10"
|
||||
:max="300"
|
||||
:step="10"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span>秒</span>
|
||||
</Space>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="响应时间超过此阈值的调用视为慢查询">
|
||||
<Space>
|
||||
<span>慢查询阈值:</span>
|
||||
<InputNumber
|
||||
v-model:value="slowQueryThreshold"
|
||||
:min="100"
|
||||
:max="10000"
|
||||
:step="100"
|
||||
style="width: 100px"
|
||||
/>
|
||||
<span>ms</span>
|
||||
</Space>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- Loading提示 -->
|
||||
<Spin :spinning="loading" tip="正在加载统计数据...">
|
||||
<!-- 统计卡片 -->
|
||||
<Row :gutter="16" class="mb-4">
|
||||
<Col :span="3">
|
||||
<Statistic title="总调用次数" :value="statsData.totalCalls" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="成功次数" :value="statsData.successCalls" :value-style="{ color: '#3f8600' }" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="错误次数" :value="statsData.errorCalls" :value-style="{ color: '#cf1322' }" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="错误率" :value="statsData.errorRate.toFixed(2)" suffix="%" :value-style="{ color: statsData.errorRate > 5 ? '#cf1322' : '#3f8600' }" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="平均响应时间" :value="statsData.avgResponseTime" suffix="ms" :value-style="{ color: statsData.avgResponseTime > slowQueryThreshold ? '#faad14' : '#1890ff' }" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="最大响应时间" :value="statsData.maxResponseTime" suffix="ms" :value-style="{ color: statsData.maxResponseTime > slowQueryThreshold ? '#cf1322' : '#1890ff' }" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="最小响应时间" :value="statsData.minResponseTime" suffix="ms" />
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<Statistic title="慢查询次数" :value="statsData.slowCalls" :value-style="{ color: '#faad14' }">
|
||||
<template #suffix>
|
||||
<Tooltip title="响应时间超过阈值的调用">
|
||||
<Tag color="warning">>{{ slowQueryThreshold }}ms</Tag>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</Statistic>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 性能指标卡片 -->
|
||||
<Row :gutter="16" class="mb-4">
|
||||
<Col :span="6">
|
||||
<Card size="small" title="性能健康度" :bordered="false">
|
||||
<Progress
|
||||
:percent="calculateHealthScore()"
|
||||
:stroke-color="getHealthColor()"
|
||||
trail-color="#f0f0f0"
|
||||
/>
|
||||
<div class="metric-desc">{{ getHealthDescription() }}</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card size="small" title="成功率" :bordered="false">
|
||||
<Progress
|
||||
:percent="100 - statsData.errorRate"
|
||||
:stroke-color="{ '0%': '#87d068', '100%': '#3f8600' }"
|
||||
trail-color="#f0f0f0"
|
||||
/>
|
||||
<div class="metric-desc">成功 {{ statsData.successCalls }} / 总计 {{ statsData.totalCalls }}</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card size="small" title="响应时间分布" :bordered="false">
|
||||
<Progress
|
||||
:percent="calculateResponseTimeScore()"
|
||||
:stroke-color="getResponseTimeColor()"
|
||||
trail-color="#f0f0f0"
|
||||
/>
|
||||
<div class="metric-desc">平均 {{ statsData.avgResponseTime }}ms / 最大 {{ statsData.maxResponseTime }}ms</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card size="small" title="缓存状态" :bordered="false">
|
||||
<Statistic :value="statsData.cacheEnabled ? '已启用' : '未启用'" :value-style="{ color: statsData.cacheEnabled ? '#3f8600' : '#8c8c8c' }" />
|
||||
<div class="metric-desc">缓存可提升查询性能</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 错误率进度条 -->
|
||||
<div class="mb-4">
|
||||
<Progress
|
||||
:percent="100 - statsData.errorRate"
|
||||
:stroke-color="{ '0%': '#3f8600', '100%': '#87d068' }"
|
||||
:trail-color="statsData.errorRate > 10 ? '#cf1322' : '#f0f0f0'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tabs切换 -->
|
||||
<Tabs v-model:activeKey="currentTab">
|
||||
<TabPane key="overview" tab="统计概览">
|
||||
<Empty v-if="!selectedApiId" description="请选择API查看统计" />
|
||||
<Alert v-else-if="statsData.totalCalls === 0" type="info">
|
||||
<template #message>
|
||||
<div>
|
||||
<strong>该API暂无调用记录</strong>
|
||||
<br />
|
||||
<small>请先调用动态API触发统计记录,然后刷新监控页面查看数据</small>
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<div v-else>
|
||||
<Alert type="success" message="统计功能已完善,包含性能健康度、成功率、响应时间分布等多维度分析" />
|
||||
|
||||
<!-- 性能建议 -->
|
||||
<Card size="small" title="性能优化建议" class="mt-4" v-if="statsData.totalCalls > 0">
|
||||
<div v-if="statsData.errorRate > 5">
|
||||
<Tag color="error">高错误率</Tag>
|
||||
<span>错误率超过5%,建议检查API配置和SQL语句</span>
|
||||
</div>
|
||||
<div v-if="statsData.avgResponseTime > slowQueryThreshold">
|
||||
<Tag color="warning">慢查询较多</Tag>
|
||||
<span>平均响应时间超过阈值,建议优化SQL查询或启用缓存</span>
|
||||
</div>
|
||||
<div v-if="!statsData.cacheEnabled && statsData.totalCalls > 10">
|
||||
<Tag color="info">未启用缓存</Tag>
|
||||
<span>调用次数较多但未启用缓存,建议启用缓存以提升性能</span>
|
||||
</div>
|
||||
<div v-if="statsData.errorRate <= 1 && statsData.avgResponseTime <= slowQueryThreshold / 2">
|
||||
<Tag color="success">性能优秀</Tag>
|
||||
<span>API运行状态良好,继续保持</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane key="errorLog" tab="错误日志">
|
||||
<Empty v-if="errorLogData.length === 0" description="暂无错误日志" />
|
||||
<div v-else>
|
||||
<Alert type="warning" class="mb-2">
|
||||
<template #message>
|
||||
<span>最近发现 {{ errorLogData.length }} 条错误记录,建议及时处理</span>
|
||||
</template>
|
||||
</Alert>
|
||||
<Table
|
||||
:columns="errorLogColumns"
|
||||
:data-source="errorLogData"
|
||||
:pagination="{ pageSize: 20 }"
|
||||
size="small"
|
||||
:scroll="{ x: 1000 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'callTime'">
|
||||
{{ record.callTime }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'errorMessage'">
|
||||
<Tooltip :title="record.errorMessage">
|
||||
<Tag color="error">{{ record.errorMessage.substring(0, 50) }}...</Tag>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<Button type="link" size="small" @click="viewErrorDetail(record)">
|
||||
详情
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<!-- 错误详情弹窗 -->
|
||||
<ErrorDetailModal
|
||||
v-model:visible="errorDetailVisible"
|
||||
:record="selectedErrorRecord"
|
||||
@close="closeErrorDetail"
|
||||
/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-section {
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.metric-desc {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { Dept } from '#/api/system/dept/model';
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { eachTree, getVxePopupContainer } from '@vben/utils';
|
||||
@@ -12,7 +12,9 @@ import { eachTree, getVxePopupContainer } from '@vben/utils';
|
||||
import { Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deptList, deptRemove } from '#/api/system/dept';
|
||||
import { deptList, deptRemove, deptSyncFromWecom } from '#/api/system/dept';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import deptDrawer from './dept-drawer.vue';
|
||||
@@ -95,6 +97,8 @@ const [DeptDrawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: deptDrawer,
|
||||
});
|
||||
|
||||
const syncLoading = ref(false);
|
||||
|
||||
function handleAdd() {
|
||||
drawerApi.setData({ update: false });
|
||||
drawerApi.open();
|
||||
@@ -116,6 +120,23 @@ async function handleDelete(row: Dept) {
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
async function handleSyncFromWecom() {
|
||||
if (syncLoading.value) return;
|
||||
syncLoading.value = true;
|
||||
const loadingMsg = message.loading('正在从企业微信同步部门,请稍候...', 0);
|
||||
try {
|
||||
const result = await deptSyncFromWecom();
|
||||
loadingMsg();
|
||||
message.success(result || '同步成功');
|
||||
await tableApi.query();
|
||||
} catch {
|
||||
loadingMsg();
|
||||
message.error('同步失败');
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部展开/折叠
|
||||
* @param expand 是否展开
|
||||
@@ -137,6 +158,13 @@ function setExpandOrCollapse(expand: boolean) {
|
||||
<a-button @click="setExpandOrCollapse(true)">
|
||||
{{ $t('pages.common.expand') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:loading="syncLoading"
|
||||
v-access:code="['system:dept:edit']"
|
||||
@click="handleSyncFromWecom"
|
||||
>
|
||||
从企业微信同步
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['system:dept:add']"
|
||||
|
||||
@@ -28,10 +28,13 @@ import {
|
||||
userList,
|
||||
userRemove,
|
||||
userStatusChange,
|
||||
userSyncFromWecom,
|
||||
} from '#/api/system/user';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import DeptTree from './dept-tree.vue';
|
||||
import userDrawer from './user-drawer.vue';
|
||||
@@ -52,6 +55,7 @@ function handleImport() {
|
||||
|
||||
// 左边部门用
|
||||
const selectDeptId = ref<string[]>([]);
|
||||
const syncLoading = ref(false);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: querySchema(),
|
||||
@@ -168,6 +172,23 @@ function handleDownloadExcel() {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSyncFromWecom() {
|
||||
if (syncLoading.value) return;
|
||||
syncLoading.value = true;
|
||||
const loadingMsg = message.loading('正在从企业微信同步用户,请稍候...', 0);
|
||||
try {
|
||||
const result = await userSyncFromWecom();
|
||||
loadingMsg();
|
||||
message.success(result || '同步成功');
|
||||
await tableApi.query();
|
||||
} catch (error: any) {
|
||||
loadingMsg();
|
||||
message.error(error?.message || '同步失败');
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const [UserInfoModal, userInfoModalApi] = useVbenModal({
|
||||
connectedComponent: userInfoModal,
|
||||
});
|
||||
@@ -228,6 +249,13 @@ const { hasAccessByCodes } = useAccess();
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:loading="syncLoading"
|
||||
v-access:code="['system:user:edit']"
|
||||
@click="handleSyncFromWecom"
|
||||
>
|
||||
从企业微信同步
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #avatar="{ row }">
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import {
|
||||
getTaskStatus,
|
||||
setCron as setCronApi,
|
||||
startTask,
|
||||
stopTask,
|
||||
wecomApprovalSyncFull,
|
||||
wecomApprovalSyncLogs,
|
||||
wecomApprovalSyncTemplates,
|
||||
} from '#/api/system/wecom-approval-sync';
|
||||
|
||||
defineOptions({ name: 'WecomApprovalSync' });
|
||||
|
||||
const syncing = ref(false);
|
||||
const syncingTemplates = ref(false);
|
||||
const syncDays = ref(30);
|
||||
const logs = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const loading = ref(false);
|
||||
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
const latestSyncLogId = ref<number | null>(null);
|
||||
|
||||
// 同步状态
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
RUNNING: { text: '同步中', color: 'processing' },
|
||||
COMPLETED: { text: '已完成', color: 'success' },
|
||||
FAILED: { text: '失败', color: 'error' },
|
||||
};
|
||||
|
||||
// 定时任务状态
|
||||
const taskRunning = ref(false);
|
||||
const currentCron = ref('0 0/5 * * * ?');
|
||||
const taskLoading = ref(false);
|
||||
|
||||
const cronOptions = [
|
||||
{ label: '每 1 分钟', value: '0 0/1 * * * ?' },
|
||||
{ label: '每 5 分钟', value: '0 0/5 * * * ?' },
|
||||
{ label: '每 10 分钟', value: '0 0/10 * * * ?' },
|
||||
{ label: '每 30 分钟', value: '0 0/30 * * * ?' },
|
||||
{ label: '每 1 小时', value: '0 0 * * * ?' },
|
||||
{ label: '每天凌晨 2 点', value: '0 0 2 * * ?' },
|
||||
];
|
||||
|
||||
const syncTypeMap: Record<string, { text: string; color: string }> = {
|
||||
FULL: { text: '全量同步', color: 'blue' },
|
||||
INCREMENTAL: { text: '增量同步', color: 'green' },
|
||||
MANUAL: { text: '手动同步', color: 'orange' },
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
customRender: ({ record }: any) => {
|
||||
const info = statusMap[record.status] || { text: '未知', color: 'default' };
|
||||
return h(Tag, { color: info.color }, () => info.text);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '同步类型',
|
||||
dataIndex: 'syncTypeText',
|
||||
key: 'syncTypeText',
|
||||
width: 120,
|
||||
customRender: ({ record }: any) => {
|
||||
const info = syncTypeMap[record.syncType] || { text: record.syncType, color: 'default' };
|
||||
return h(Tag, { color: info.color }, () => info.text);
|
||||
},
|
||||
},
|
||||
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 180 },
|
||||
{ title: '结束时间', dataIndex: 'endTime', key: 'endTime', width: 180 },
|
||||
{ title: '耗时(秒)', dataIndex: 'duration', key: 'duration', width: 100 },
|
||||
{ title: '新增', dataIndex: 'addedCount', key: 'addedCount', width: 80 },
|
||||
{ title: '更新', dataIndex: 'updatedCount', key: 'updatedCount', width: 80 },
|
||||
{ title: '失败', dataIndex: 'errorCount', key: 'errorCount', width: 80 },
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 120 },
|
||||
];
|
||||
|
||||
async function handleFullSync() {
|
||||
Modal.confirm({
|
||||
title: '确认全量同步',
|
||||
icon: () => h(ExclamationCircleOutlined),
|
||||
content: `将同步近 ${syncDays.value} 天的所有审批数据,后台异步执行,请查看同步日志跟踪进度。确定继续吗?`,
|
||||
okText: '确定同步',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
syncing.value = true;
|
||||
try {
|
||||
const logId = await wecomApprovalSyncFull(syncDays.value);
|
||||
latestSyncLogId.value = logId;
|
||||
message.success('已启动同步,请在下方同步日志中查看进度');
|
||||
await loadLogs();
|
||||
// 如果正在运行,启动轮询
|
||||
startPolling();
|
||||
} catch {
|
||||
message.error('启动同步失败');
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollingTimer.value = setInterval(async () => {
|
||||
await loadLogs();
|
||||
// 检查最新同步日志是否还在运行
|
||||
const runningLog = logs.value.find((l: any) => l.status === 'RUNNING');
|
||||
if (!runningLog) {
|
||||
stopPolling();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingTimer.value) {
|
||||
clearInterval(pollingTimer.value);
|
||||
pollingTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await wecomApprovalSyncLogs({
|
||||
pageNum: currentPage.value ?? 1,
|
||||
pageSize: pageSize.value ?? 10,
|
||||
});
|
||||
logs.value = res?.rows ?? [];
|
||||
total.value = res?.total ?? 0;
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.message || error?.response?.data?.msg || '未知错误';
|
||||
message.error(`加载同步日志失败: ${errorMsg}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTaskStatus() {
|
||||
try {
|
||||
const status = await getTaskStatus();
|
||||
taskRunning.value = status.running;
|
||||
currentCron.value = status.cron || '0 0/5 * * * ?';
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
async function handleTaskToggle(checked: boolean) {
|
||||
taskLoading.value = true;
|
||||
try {
|
||||
if (checked) {
|
||||
await startTask();
|
||||
message.success('定时任务已启动');
|
||||
} else {
|
||||
await stopTask();
|
||||
message.success('定时任务已停止');
|
||||
}
|
||||
await loadTaskStatus();
|
||||
} catch {
|
||||
message.error(checked ? '启动失败' : '停止失败');
|
||||
} finally {
|
||||
taskLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCronChange(value: string) {
|
||||
try {
|
||||
await setCronApi(value);
|
||||
message.success('同步频率已更新');
|
||||
currentCron.value = value;
|
||||
} catch {
|
||||
message.error('更新频率失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(pagination: any) {
|
||||
currentPage.value = pagination.current ?? 1;
|
||||
pageSize.value = pagination.pageSize ?? 10;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
async function handleSyncTemplates() {
|
||||
Modal.confirm({
|
||||
title: '确认同步模板',
|
||||
icon: () => h(ExclamationCircleOutlined),
|
||||
content: '将从已同步的审批数据中提取模板ID并同步模板详情,确定继续吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
syncingTemplates.value = true;
|
||||
try {
|
||||
await wecomApprovalSyncTemplates();
|
||||
message.success('模板同步完成');
|
||||
} catch {
|
||||
message.error('模板同步失败');
|
||||
} finally {
|
||||
syncingTemplates.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs();
|
||||
loadTaskStatus();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div style="overflow-y: auto; padding: 16px">
|
||||
<!-- 操作区 -->
|
||||
<Card title="操作" :bordered="false" style="margin-bottom: 16px">
|
||||
<Space>
|
||||
<span>同步近</span>
|
||||
<Select v-model:value="syncDays" style="width: 120px">
|
||||
<Select.Option :value="1">1 天</Select.Option>
|
||||
<Select.Option :value="7">7 天</Select.Option>
|
||||
<Select.Option :value="30">30 天</Select.Option>
|
||||
<Select.Option :value="90">90 天</Select.Option>
|
||||
</Select>
|
||||
<span>的数据</span>
|
||||
<Button type="primary" danger :loading="syncing" @click="handleFullSync">
|
||||
全量同步
|
||||
</Button>
|
||||
<Button :loading="syncingTemplates" @click="handleSyncTemplates">
|
||||
同步模板
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<!-- 定时任务管理 -->
|
||||
<Card title="定时任务" :bordered="false" style="margin-bottom: 16px">
|
||||
<Space>
|
||||
<span>启用定时同步:</span>
|
||||
<Switch
|
||||
v-model:checked="taskRunning"
|
||||
:loading="taskLoading"
|
||||
checked-children="开"
|
||||
un-checked-children="关"
|
||||
@change="handleTaskToggle"
|
||||
/>
|
||||
<span style="margin-left: 24px">同步频率:</span>
|
||||
<Select
|
||||
:value="currentCron"
|
||||
style="width: 180px"
|
||||
:options="cronOptions"
|
||||
@change="handleCronChange"
|
||||
/>
|
||||
<Tag :color="taskRunning ? 'green' : 'default'">
|
||||
{{ taskRunning ? '运行中' : '已停止' }}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<!-- 同步日志表格 -->
|
||||
<Card title="同步日志" :bordered="false">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="logs"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputPassword,
|
||||
message,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
import { LinkOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { getWecomConfig, saveWecomConfig, testWecomConfig } from '#/api/system/wecom-config';
|
||||
|
||||
const saving = ref(false);
|
||||
const testing = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const formState = reactive({
|
||||
corpid: '',
|
||||
corpsecret: '',
|
||||
agentId: '',
|
||||
callbackToken: '',
|
||||
callbackAesKey: '',
|
||||
enabled: false,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
corpid: [{ required: true, message: '请输入企业ID', trigger: 'blur' }],
|
||||
corpsecret: [{ required: true, message: '请输入应用密钥', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const data = await getWecomConfig();
|
||||
if (data) {
|
||||
formState.corpid = data.corpid || '';
|
||||
formState.corpsecret = data.corpsecret || '';
|
||||
formState.agentId = data.agentId || '';
|
||||
formState.callbackToken = data.callbackToken || '';
|
||||
formState.callbackAesKey = data.callbackAesKey || '';
|
||||
formState.enabled = data.enabled === '1';
|
||||
formState.remark = data.remark || '';
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await saveWecomConfig({
|
||||
...formState,
|
||||
enabled: formState.enabled ? '1' : '0',
|
||||
});
|
||||
message.success('保存成功');
|
||||
} catch {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
if (!formState.corpid || !formState.corpsecret) {
|
||||
message.error('企业ID和应用密钥不能为空');
|
||||
return;
|
||||
}
|
||||
testing.value = true;
|
||||
try {
|
||||
const result = await testWecomConfig(formState.corpid, formState.corpsecret);
|
||||
message.success(result || '连接成功');
|
||||
} catch {
|
||||
message.error('测试失败');
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openWeComHelp() {
|
||||
window.open('https://work.weixin.qq.com/wework_admin/frame#profile', '_blank');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<div style="overflow-y: auto; padding: 16px">
|
||||
<Card title="企业微信配置" :bordered="false" style="max-width: 720px">
|
||||
<template #extra>
|
||||
<Button type="link" @click="openWeComHelp">
|
||||
<template #icon><LinkOutlined /></template>
|
||||
如何获取?
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<Alert
|
||||
message="请在企业微信管理后台获取企业ID和应用密钥"
|
||||
type="info"
|
||||
show-icon
|
||||
style="margin-bottom: 24px"
|
||||
/>
|
||||
|
||||
<Form ref="formRef" :model="formState" layout="vertical" style="max-width: 480px">
|
||||
<FormItem label="企业ID(corpid)" name="corpid" :rules="rules.corpid">
|
||||
<Input v-model:value="formState.corpid" placeholder="从企业微信管理后台获取" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="应用密钥(corpsecret)" name="corpsecret" :rules="rules.corpsecret">
|
||||
<InputPassword v-model:value="formState.corpsecret" placeholder="自建应用的Secret" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="应用 AgentID" name="agentId">
|
||||
<Input v-model:value="formState.agentId" placeholder="可选,应用管理页面获取" />
|
||||
</FormItem>
|
||||
|
||||
<Divider>回调配置(可选)</Divider>
|
||||
|
||||
<FormItem label="回调 Token" name="callbackToken">
|
||||
<Input v-model:value="formState.callbackToken" placeholder="审批回调URL验证Token" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="回调 EncodingAESKey" name="callbackAesKey">
|
||||
<InputPassword v-model:value="formState.callbackAesKey" placeholder="消息加密密钥" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="启用状态" name="enabled">
|
||||
<Switch v-model:checked="formState.enabled" checked-children="已启用" un-checked-children="未启用" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="备注" name="remark">
|
||||
<Textarea v-model:value="formState.remark" :rows="2" placeholder="备注信息" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Space>
|
||||
<Button :loading="testing" @click="handleTest">
|
||||
测试连接
|
||||
</Button>
|
||||
<Button type="primary" :loading="saving" @click="handleSave">
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
@@ -68,13 +68,17 @@ export default defineConfig(async () => {
|
||||
'/api': {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// mock代理目标地址
|
||||
target: 'http://127.0.0.1:6039',
|
||||
// Gateway 地址 - 使用局域网地址或环境变量
|
||||
target: process.env.VITE_API_URL || 'http://192.168.120.60:8080',
|
||||
ws: true,
|
||||
},
|
||||
'/resource': {
|
||||
changeOrigin: true,
|
||||
target: 'http://127.0.0.1:6039',
|
||||
target: process.env.VITE_API_URL || 'http://192.168.120.60:8080',
|
||||
},
|
||||
'/auth': {
|
||||
changeOrigin: true,
|
||||
target: process.env.VITE_API_URL || 'http://192.168.120.60:8080',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,9 +16,9 @@ server {
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# API 代理到后端服务
|
||||
# API 代理到 Gateway
|
||||
location /api/ {
|
||||
proxy_pass http://hzhub-ai:6039/;
|
||||
proxy_pass ${UPSTREAM_URL:-http://hzhub-gateway:8080}/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
@@ -30,9 +30,9 @@ server {
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# SSE 支持
|
||||
# SSE 支持(通过 Gateway)
|
||||
location /api/sse/ {
|
||||
proxy_pass http://hzhub-ai:6039/sse/;
|
||||
proxy_pass ${UPSTREAM_URL:-http://hzhub-gateway:8080}/sse/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection '';
|
||||
proxy_buffering off;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/bin/bash
|
||||
# HZHub Admin 前端项目停止脚本
|
||||
# 功能:停止后台运行的 hzhub-admin 前端开发服务器
|
||||
|
||||
PROJECT_NAME="hzhub-admin"
|
||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -41,4 +40,12 @@ fi
|
||||
|
||||
# 清理PID文件
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
# Fallback: 如果端口仍被占用,尝试按端口关闭
|
||||
if ss -tlnp 2>/dev/null | grep -q ':5666 '; then
|
||||
echo "⚠️ 端口5666仍被占用,尝试按端口清理..."
|
||||
fuser -k 5666/tcp 2>/dev/null
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
echo "✅ 服务已停止"
|
||||
0
hzhub-ai/Dockerfile
Normal file → Executable file
0
hzhub-ai/LICENSE
Normal file → Executable file
0
hzhub-ai/README.md
Normal file → Executable file
0
hzhub-ai/README_EN.md
Normal file → Executable file
0
hzhub-ai/docs/docker/ minio/ docker-compose.yml
Normal file → Executable file
0
hzhub-ai/docs/docker/ neo4j/docker-compose.yml
Normal file → Executable file
0
hzhub-ai/docs/docker/hzhub-ai/Dockerfile.backend
Normal file → Executable file
0
hzhub-ai/docs/docker/hzhub-ai/Dockerfile.mysql
Normal file → Executable file
0
hzhub-ai/docs/docker/hzhub-ai/docker-compose-all.yaml
Normal file → Executable file
0
hzhub-ai/docs/docker/hzhub-ai/docker-compose.yaml
Normal file → Executable file
0
hzhub-ai/docs/docker/milvus/docker-compose.yml
Normal file → Executable file
0
hzhub-ai/docs/docker/weaviate/docker-compose.yml
Normal file → Executable file
0
hzhub-ai/docs/image/bibi.png
Normal file → Executable file
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
0
hzhub-ai/docs/image/dy.png
Normal file → Executable file
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
0
hzhub-ai/docs/image/logo.png
Normal file → Executable file
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
0
hzhub-ai/docs/image/qq.png
Normal file → Executable file
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
0
hzhub-ai/docs/image/wx.png
Normal file → Executable file
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
0
hzhub-ai/docs/script/docker/mysql/init/init-db.sh
Normal file → Executable file
0
hzhub-ai/docs/script/leave/leave1.json
Normal file → Executable file
0
hzhub-ai/docs/script/leave/leave2.json
Normal file → Executable file
0
hzhub-ai/docs/script/leave/leave3.json
Normal file → Executable file
0
hzhub-ai/docs/script/leave/leave4.json
Normal file → Executable file
0
hzhub-ai/docs/script/leave/leave5.json
Normal file → Executable file
0
hzhub-ai/docs/script/leave/leave6.json
Normal file → Executable file
0
hzhub-ai/docs/script/sql/hzhub-ai-v3_mysql8.sql
Normal file → Executable file
0
hzhub-ai/docs/script/sql/remove_my_task_menu.sql
Normal file → Executable file
0
hzhub-ai/docs/troubleshooting/rag-failures.md
Normal file → Executable file
0
hzhub-ai/docs/文件上传接口文档.md
Normal file → Executable file
0
hzhub-ai/hzhub-admin/Dockerfile
Normal file → Executable file
19
hzhub-ai/hzhub-admin/pom.xml
Normal file → Executable file
@@ -23,6 +23,11 @@
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <!– mp支持的数据库均支持 只需要增加对应的jdbc依赖即可 –>-->
|
||||
<!-- <!– Oracle –>-->
|
||||
<!-- <dependency>-->
|
||||
@@ -67,26 +72,16 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hzhub</groupId>
|
||||
<artifactId>hzhub-system</artifactId>
|
||||
<artifactId>hzhub-common-oss</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>org.hzhub</groupId>
|
||||
<artifactId>hzhub-generator</artifactId>
|
||||
</dependency>
|
||||
<!-- hzhub-system / hzhub-workflow / hzhub-generator 已迁移至独立服务 -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hzhub</groupId>
|
||||
<artifactId>hzhub-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工作流模块 -->
|
||||
<dependency>
|
||||
<groupId>org.hzhub</groupId>
|
||||
<artifactId>hzhub-workflow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AI流程编排模块 -->
|
||||
<dependency>
|
||||
<groupId>org.hzhub</groupId>
|
||||
|
||||
0
hzhub-ai/hzhub-admin/src/main/java/org/hzhub/HZHubAIApplication.java
Normal file → Executable file
0
hzhub-ai/hzhub-admin/src/main/java/org/hzhub/HZHubAIServletInitializer.java
Normal file → Executable file
0
hzhub-ai/hzhub-admin/src/main/java/org/hzhub/config/MapperConflictResolver.java
Normal file → Executable file
0
hzhub-ai/hzhub-admin/src/main/java/org/hzhub/controller/IndexController.java
Normal file → Executable file
117
hzhub-ai/hzhub-admin/src/main/java/org/hzhub/service/OssServiceImpl.java
Executable file
@@ -0,0 +1,117 @@
|
||||
package org.hzhub.service;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.hzhub.common.core.domain.dto.OssDTO;
|
||||
import org.hzhub.common.core.exception.ServiceException;
|
||||
import org.hzhub.common.core.service.OssService;
|
||||
import org.hzhub.common.core.utils.StringUtils;
|
||||
import org.hzhub.common.core.utils.file.ContentTypeUtil;
|
||||
import org.hzhub.common.oss.core.OssClient;
|
||||
import org.hzhub.common.oss.entity.UploadResult;
|
||||
import org.hzhub.common.oss.factory.OssFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 简易 OSS 服务实现(用于 hzhub-ai)
|
||||
* 上传文件至 OSS,文件信息存储至 sys_oss 表
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OssServiceImpl implements OssService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public OssDTO uploadFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
String originalFileName = file.getOriginalFilename();
|
||||
String suffix = StringUtils.substring(originalFileName, originalFileName.lastIndexOf("."));
|
||||
OssClient storage = OssFactory.instance();
|
||||
UploadResult uploadResult;
|
||||
try {
|
||||
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, ContentTypeUtil.getContentType(suffix, file.getContentType()));
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("上传失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
// 保存到 sys_oss 表
|
||||
Long ossId = saveOssRecord(originalFileName, suffix, storage.getConfigKey(), uploadResult, file.getSize(), file.getContentType());
|
||||
OssDTO dto = new OssDTO();
|
||||
dto.setOssId(ossId);
|
||||
dto.setFileName(uploadResult.getFilename());
|
||||
dto.setOriginalName(originalFileName);
|
||||
dto.setFileSuffix(suffix);
|
||||
dto.setUrl(uploadResult.getUrl());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String selectUrlByIds(String ossIds) {
|
||||
if (StringUtils.isBlank(ossIds)) return "";
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (String idStr : ossIds.split(",")) {
|
||||
idStr = idStr.trim();
|
||||
if (StringUtils.isBlank(idStr)) continue;
|
||||
try {
|
||||
String url = jdbcTemplate.queryForObject(
|
||||
"SELECT url FROM sys_oss WHERE oss_id = ?", String.class, Long.parseLong(idStr));
|
||||
if (url != null) {
|
||||
urls.add(url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询 OSS URL 失败: ossId={}", idStr, e);
|
||||
}
|
||||
}
|
||||
return StringUtils.joinComma(urls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OssDTO> selectByIds(String ossIds) {
|
||||
if (StringUtils.isBlank(ossIds)) return List.of();
|
||||
List<OssDTO> result = new ArrayList<>();
|
||||
for (String idStr : ossIds.split(",")) {
|
||||
idStr = idStr.trim();
|
||||
if (StringUtils.isBlank(idStr)) continue;
|
||||
try {
|
||||
List<OssDTO> rows = jdbcTemplate.query(
|
||||
"SELECT oss_id, file_name, original_name, file_suffix, url FROM sys_oss WHERE oss_id = ?",
|
||||
(rs, rowNum) -> {
|
||||
OssDTO dto = new OssDTO();
|
||||
dto.setOssId(rs.getLong("oss_id"));
|
||||
dto.setFileName(rs.getString("file_name"));
|
||||
dto.setOriginalName(rs.getString("original_name"));
|
||||
dto.setFileSuffix(rs.getString("file_suffix"));
|
||||
dto.setUrl(rs.getString("url"));
|
||||
return dto;
|
||||
},
|
||||
Long.parseLong(idStr));
|
||||
if (!rows.isEmpty()) {
|
||||
result.add(rows.get(0));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("查询 OSS 记录失败: ossId={}", idStr, e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Long saveOssRecord(String originalName, String suffix, String configKey, UploadResult result, long size, String contentType) {
|
||||
Long ossId = RandomUtil.randomLong(Long.MAX_VALUE);
|
||||
String extJson = String.format("{\"fileSize\":%d,\"contentType\":\"%s\"}", size, contentType);
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO sys_oss (oss_id, url, file_name, original_name, file_suffix, service, ext1) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
ossId, result.getUrl(), result.getFilename(), originalName, suffix, configKey, extJson);
|
||||
return ossId;
|
||||
}
|
||||
}
|
||||
0
hzhub-ai/hzhub-admin/src/main/resources/application-dev.yml
Normal file → Executable file
0
hzhub-ai/hzhub-admin/src/main/resources/application-prod.yml
Normal file → Executable file
12
hzhub-ai/hzhub-admin/src/main/resources/application.yml
Normal file → Executable file
@@ -108,8 +108,8 @@ sa-token:
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
||||
is-share: false
|
||||
# jwt秘钥
|
||||
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
|
||||
# jwt秘钥(必须 >= 32 字节)
|
||||
jwt-secret-key: ${JWT_SECRET:Om1fovSeKIA1oLIoHdDPMF-trbqbrPQoDS3H4u1xoRY}
|
||||
|
||||
# security配置
|
||||
security:
|
||||
@@ -210,13 +210,7 @@ springdoc:
|
||||
packages-to-scan: org.hzhub.demo
|
||||
- group: 2.通用模块
|
||||
packages-to-scan: org.hzhub.web
|
||||
- group: 3.系统模块
|
||||
packages-to-scan: org.hzhub.system
|
||||
- group: 4.代码生成模块
|
||||
packages-to-scan: org.hzhub.generator
|
||||
- group: 5.工作流模块
|
||||
packages-to-scan: org.hzhub.workflow
|
||||
- group: 6.MCP模块
|
||||
- group: 3.MCP模块
|
||||
packages-to-scan: org.hzhub.mcp
|
||||
|
||||
# 防止XSS攻击
|
||||
|
||||