@clawhub-smallest-ming-d89e59d6e2
全自动项目生成和启动器 - 生成完整的Spring Boot + Vue3项目,包含前后端完整代码(RBAC+业务实体)、数据库初始化、编译启动、自动浏览器打开。支持完整CRUD(列表、查询、新增、修改、删除、查看)、多条件查询、状态下拉枚举、Redis配置、Swagger文档,使用JSON配置文件自定义参数。
---
name: optimized-one-click-project
description: 全自动项目生成和启动器 - 生成完整的Spring Boot + Vue3项目,包含前后端完整代码(RBAC+业务实体)、数据库初始化、编译启动、自动浏览器打开。支持完整CRUD(列表、查询、新增、修改、删除、查看)、多条件查询、状态下拉枚举、Redis配置、Swagger文档,使用JSON配置文件自定义参数。
---
# 优化版一键项目生成器 v2.1
🚀 **前后端完整代码生成(RBAC+业务实体)· 完整CRUD功能 · 多条件查询 · 状态下拉枚举 · 数据库初始化 · 智能编译启动 · 自动浏览器打开**
---
## 🎯 **核心功能**
### 1️⃣ **完整代码生成**
根据JSON配置文件自动生成完整的项目代码:
#### **后端 (Spring Boot)**
- 📦 **RBAC权限系统** - SysUser、SysRole、SysPermission完整CRUD
- 📦 **业务实体系统** - 根据配置自动生成Entity、Mapper、Service、Controller
- 📦 **完整CRUD接口** - 列表查询、根据ID查询、新增、修改、删除
- 📦 **Redis支持** - 可选Redis缓存配置(RedisConfig、RedisUtil)
- 📦 **Swagger文档** - 可选API文档支持
- 📦 **application.yml** - 使用配置文件中的数据库/Redis参数
#### **前端 (Vue3)**
- 🎨 **RBAC管理页面** - 登录页、用户/角色/权限管理
- 🎨 **业务实体页面** - 根据配置自动生成管理页面
- 🎨 **完整CRUD功能** - 列表、查询、新增、编辑、删除、查看
- 🎨 **多条件查询** - 支持多个字段的条件组合查询
- 🎨 **状态下拉枚举** - 状态字段统一使用下拉选择框显示(启用/禁用)
- 🎨 **分页功能** - 支持分页展示和切换
- 🎨 **路由配置** - 含路由守卫、权限控制
- 🎨 **API封装** - Axios请求封装、接口文件
- 🎨 **Element Plus UI** - 完整的管理系统界面,含图标支持
### 2️⃣ **数据库初始化**
- 🗄️ **RBAC系统表** - 用户、角色、权限、关联表
- 🗄️ **业务实体表** - 根据实体配置自动生成
- 🗄️ **默认数据** - admin/123456、user/123456等
- 🗄️ **物理删除** - 不使用逻辑删除字段
### 3️⃣ **9步全自动流程**
从配置到上线,一键完成所有步骤。
---
## 📁 **项目结构**
```
project-name/
├── backend/ # Spring Boot后端
│ ├── src/main/java/com/example/
│ │ ├── entity/ # 实体类
│ │ │ ├── SysUser.java # 用户实体 (RBAC)
│ │ │ ├── SysRole.java # 角色实体 (RBAC)
│ │ │ ├── SysPermission.java # 权限实体 (RBAC)
│ │ │ └── [业务实体].java # 配置的业务实体
│ │ ├── mapper/ # Mapper接口
│ │ ├── service/ # Service层
│ │ ├── controller/ # Controller层
│ │ │ ├── SysUserController.java # 用户管理API
│ │ │ ├── SysRoleController.java # 角色管理API
│ │ │ ├── SysPermissionController.java # 权限管理API
│ │ │ ├── AuthController.java # 登录认证API
│ │ │ └── [业务实体]Controller.java # 业务实体API
│ │ ├── config/ # 配置类
│ │ │ ├── RedisConfig.java # Redis配置(可选)
│ │ │ └── SwaggerConfig.java # Swagger配置(可选)
│ │ ├── util/ # 工具类
│ │ │ └── RedisUtil.java # Redis工具类(可选)
│ │ └── [Application].java # 启动类
│ ├── src/main/resources/
│ │ ├── application.yml # 配置文件(使用配置的参数)
│ │ └── db/init.sql # 数据库初始化脚本
│ └── pom.xml # Maven配置(含Redis依赖)
├── frontend/ # Vue3前端
│ ├── src/
│ │ ├── views/ # 页面组件
│ │ │ ├── Login.vue # 登录页面
│ │ │ ├── Layout.vue # 布局组件
│ │ │ ├── UserManagement.vue # 用户管理(含搜索、查看、CRUD)
│ │ │ ├── RoleManagement.vue # 角色管理(含搜索、查看、CRUD)
│ │ │ ├── PermissionManagement.vue # 权限管理(含搜索、查看、CRUD)
│ │ │ └── [业务实体]Management.vue # 业务实体管理页面(含搜索、查看、CRUD)
│ │ ├── router/ # 路由配置
│ │ ├── api/ # API接口
│ │ │ ├── user.js # 用户API(列表、查询、新增、修改、删除)
│ │ │ ├── role.js # 角色API(列表、查询、新增、修改、删除)
│ │ │ ├── permission.js # 权限API(列表、查询、新增、修改、删除)
│ │ │ └── [业务实体].js # 业务实体API
│ │ ├── utils/ # 工具函数
│ │ │ └── request.js # Axios封装
│ │ ├── main.js
│ │ └── App.vue
│ ├── index.html
│ ├── vite.config.js # 使用配置的服务器端口
│ └── package.json
└── config.json # 项目配置文件
```
---
## 🚀 **执行流程(9步)**
| 步骤 | 功能 | 说明 |
|-----|------|------|
| 1/9 | 验证环境 | Java/Maven/Node.js/NPM必需检测 |
| 2/9 | 生成项目代码 | 前后端完整代码生成(使用配置参数)|
| 3/9 | 检查端口占用 | 检测配置的端口(默认8080/5173)|
| 4/9 | 测试数据库连接 | 验证配置的MySQL连接 |
| 5/9 | 执行数据库初始化 | 创建表和初始数据 |
| 6/9 | 构建后端项目 | Maven编译打包 |
| 7/9 | 安装前端依赖 | NPM install |
| 8/9 | 启动后端服务 | 在新CMD窗口启动,使用配置端口 |
| 9/9 | 启动前端并打开浏览器 | 在新CMD窗口启动+自动打开 |
---
## 🎊 **使用方式**
### 📋 **OpenClaw命令**
```bash
使用 optimized-one-click-project 创建一个用户信息管理系统
```
### 🎯 **直接执行脚本**
```bash
python optimized-one-click-project\scripts\optimized-start-win.py config.json
```
---
## 📋 **配置文件说明**
### **完整配置示例**
```json
{
"project_name": "user-info-system-v2",
"description": "用户信息管理系统",
"entities": [
{
"name": "UserInfo",
"fields": [
{"name": "userCode", "type": "String", "comment": "用户编码"},
{"name": "username", "type": "String", "comment": "用户名"},
{"name": "realName", "type": "String", "comment": "真实姓名"},
{"name": "email", "type": "String", "comment": "邮箱"},
{"name": "phone", "type": "String", "comment": "电话"}
]
},
{
"name": "Department",
"fields": [
{"name": "deptCode", "type": "String", "comment": "部门编码"},
{"name": "deptName", "type": "String", "comment": "部门名称"},
{"name": "description", "type": "String", "comment": "描述"}
]
}
],
"database": {
"host": "your_ip",
"port": 3306,
"name": "your_database_name",
"user": "your_user",
"password": "your_password"
},
"redis": {
"enable": true,
"host": "your_ip",
"port": 6379,
"password": "your_password"
},
"server": {
"backend_port": 8080,
"frontend_port": 5173
},
"enable_swagger": true
}
```
### **配置参数说明**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `project_name` | String | ✓ | 项目名称(英文,将作为包名)|
| `description` | String | | 项目描述 |
| `entities` | Array | | 业务实体列表 |
| `entities[].name` | String | ✓ | 实体类名 |
| `entities[].fields` | Array | ✓ | 字段列表 |
| `entities[].fields[].name` | String | ✓ | 字段名 |
| `entities[].fields[].type` | String | ✓ | 字段类型(String/Long/Integer)|
| `entities[].fields[].comment` | String | | 字段注释 |
| `database` | Object | ✓ | 数据库配置 |
| `database.host` | String | ✓ | 数据库主机地址 |
| `database.port` | Number | ✓ | 数据库端口(默认3306)|
| `database.name` | String | ✓ | 数据库名称 |
| `database.user` | String | ✓ | 数据库用户名 |
| `database.password` | String | ✓ | 数据库密码 |
| `redis` | Object | | Redis配置 |
| `redis.enable` | Boolean | | 是否启用Redis(默认false)|
| `redis.host` | String | | Redis主机地址 |
| `redis.port` | Number | | Redis端口(默认6379)|
| `redis.password` | String | | Redis密码 |
| `server` | Object | | 服务器配置 |
| `server.backend_port` | Number | | 后端端口(默认8080)|
| `server.frontend_port` | Number | | 前端端口(默认5173)|
| `enable_swagger` | Boolean | | 是否启用Swagger(默认true)|
---
## 🌐 **系统访问**
- **前端页面**: http://localhost:{frontend_port}
- **登录页面**: http://localhost:{frontend_port}/login
- **后端API**: http://localhost:{backend_port}/api
- **Swagger文档**: http://localhost:{backend_port}/api/swagger-ui.html
- **默认账号**: admin / 123456
---
## ⚙️ **后端API接口**
### RBAC接口
```
GET /api/users # 列表查询(分页)
GET /api/users/{id} # 根据ID查询(查看详情)
POST /api/users # 新增
PUT /api/users/{id} # 修改
DELETE /api/users/{id} # 删除(物理删除)
GET /api/roles # 列表查询
GET /api/roles/{id} # 根据ID查询
POST /api/roles # 新增
PUT /api/roles/{id} # 修改
DELETE /api/roles/{id} # 删除
GET /api/permissions # 列表查询
GET /api/permissions/{id} # 根据ID查询
POST /api/permissions # 新增
PUT /api/permissions/{id} # 修改
DELETE /api/permissions/{id} # 删除
POST /api/auth/login # 登录
GET /api/auth/info # 获取当前用户信息
```
### 业务实体接口
```
GET /api/{entities} # 列表查询(分页)
GET /api/{entities}/{id} # 根据ID查询
POST /api/{entities} # 新增
PUT /api/{entities}/{id} # 修改
DELETE /api/{entities}/{id} # 删除
```
---
## 🎨 **前端页面功能**
### 列表页功能
- ✅ **多条件查询** - 支持多个字段的条件组合查询(文本输入框、状态下拉框)
- ✅ **查询/重置按钮** - 执行查询或清空条件
- ✅ **数据表格** - 显示所有字段,支持状态标签显示
- ✅ **分页** - 支持分页展示、页码切换、每页数量调整
- ✅ **操作列** - 查看、编辑、删除按钮,带图标
### 对话框功能
- ✅ **查看对话框** - 使用Descriptions组件展示详情(只读)
- ✅ **新增/编辑对话框** - 表单输入,支持验证
- ✅ **状态下拉枚举** - 状态字段使用el-select下拉选择(启用/禁用)
- ✅ **表单验证** - 必填项验证
### 按钮功能
- ✅ **查询** - 根据关键词搜索
- ✅ **重置** - 清空搜索条件
- ✅ **新增** - 打开新增对话框
- ✅ **查看** - 查看详情
- ✅ **编辑** - 打开编辑对话框
- ✅ **删除** - 确认后删除
---
## 🗄️ **数据库设计**
### RBAC表结构
```sql
-- 用户表
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
email VARCHAR(100),
phone VARCHAR(20),
avatar VARCHAR(255),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_code VARCHAR(50) NOT NULL UNIQUE,
role_name VARCHAR(50) NOT NULL,
description VARCHAR(255),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 权限表
CREATE TABLE sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_code VARCHAR(100) NOT NULL UNIQUE,
permission_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
type VARCHAR(20) DEFAULT 'menu',
path VARCHAR(255),
method VARCHAR(20),
icon VARCHAR(50),
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### 业务实体表结构
- ✅ 自动根据 `entities` 配置生成
- ✅ 驼峰命名自动转下划线
- ✅ 含 `id`, `create_time`, `update_time` 字段
- ✅ 不含逻辑删除字段(物理删除)
---
## 🎉 **最新优化功能**
### v2.1 新增功能
| 优化项 | 说明 |
|--------|------|
| **多条件查询** | 前端业务页面增加条件查询区域,支持多个字段组合查询 |
| **状态下拉枚举** | 状态字段统一使用el-select下拉选择框显示(启用/禁用) |
| **分页优化** | 完整分页功能,支持页码切换和每页数量调整 |
| **查看详情** | 使用Descriptions组件优化详情展示 |
| **图标支持** | 按钮和菜单添加Element Plus图标 |
| **模板优化** | 提取公共Vue模板,代码结构更清晰 |
### v2.0 基础功能
| 功能 | 说明 |
|------|------|
| **查询功能** | 前端关键词搜索 |
| **查看功能** | 根据ID查询详情,对话框展示 |
| **完整CRUD** | 列表、查询、新增、修改、删除 |
| **前后端匹配** | API接口完全对应 |
| **物理删除** | 去掉逻辑删除字段 |
| **字段显示** | 表格显示所有字段 |
| **搜索表单** | 查询/重置按钮 |
---
## 📦 **技能信息**
- **名称**: optimized-one-click-project
- **版本**: v2.1
- **作者**: OpenClaw
- **更新时间**: 2026-03-20
**技能已安装完成,随时可以使用!** 🚀
FILE:scripts/optimized-start-win.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
优化版一键项目生成和启动脚本 - v2.1
新增功能:
- 前端业务实体页面增加多条件查询区域
- 状态字段统一使用下拉枚举方式显示
- 优化模板结构,提取公共模板
"""
import os
import sys
import time
import json
import subprocess
from pathlib import Path
class Colors:
"""终端颜色类"""
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
CYAN = '\033[96m'
RESET = '\033[0m'
BOLD = '\033[1m'
# ==================== 模板常量 ====================
# Vue 单文件页面模板 - 业务实体管理页(含条件查询)
VUE_PAGE_TEMPLATE = '''<template>
<div class="management">
<el-card>
<template #header>
<div class="card-header">
<span>{{ pageTitle }}</span>
<el-button type="primary" @click="handleAdd" :icon="Plus">新增</el-button>
</div>
</template>
<!-- 条件查询区域 -->
<el-form :inline="true" :model="searchForm" class="search-form">
{search_form_items}
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">查询</el-button>
<el-button @click="handleReset" :icon="RefreshRight">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="list" style="width: 100%" v-loading="loading" border>
<el-table-column prop="id" label="ID" width="80" align="center" />
{table_columns}
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="scope">
<el-button size="small" @click="handleView(scope.row)" :icon="View">查看</el-button>
<el-button size="small" type="primary" @click="handleEdit(scope.row)" :icon="Edit">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)" :icon="Delete">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="pagination"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close>
<el-form :model="formData" ref="formRef" label-width="100px" :rules="formRules">
{form_items}
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 查看详情对话框 -->
<el-dialog v-model="viewDialogVisible" title="查看详情" width="600px" destroy-on-close>
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">{{ formData.id }}</el-descriptions-item>
{view_items}
<el-descriptions-item label="创建时间">{{ formData.createTime }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formData.updateTime }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="viewDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, RefreshRight, View, Edit, Delete } from '@element-plus/icons-vue'
import {
get{class_name}List,
get{class_name}ById,
create{class_name},
update{class_name},
delete{class_name}
} from '../api/{var_name}'
// ==================== 响应式数据 ====================
const loading = ref(false)
const list = ref([])
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('新增')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const pageTitle = '{comment}管理'
// 分页配置
const pagination = reactive({
current: 1,
size: 10,
total: 0
})
// 查询表单
const searchForm = reactive({
{search_form_init}
})
// 表单数据
const formData = ref({
{form_init}
})
// 表单验证规则
const formRules = {
{form_rules}
}
// ==================== 方法 ====================
// 加载数据
const loadData = async () => {
loading.value = true
try {
const params = {
current: pagination.current,
size: pagination.size,
...searchForm
}
const res = await get{class_name}List(params)
list.value = res.records || res.data?.records || []
pagination.total = res.total || res.data?.total || 0
} catch (error) {
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 查询
const handleSearch = () => {
pagination.current = 1
loadData()
}
// 重置查询
const handleReset = () => {
{reset_code}
pagination.current = 1
loadData()
}
// 分页大小变化
const handleSizeChange = (size) => {
pagination.size = size
loadData()
}
// 页码变化
const handleCurrentChange = (current) => {
pagination.current = current
loadData()
}
// 新增
const handleAdd = () => {
isEdit.value = false
dialogTitle.value = '新增'
formData.value = {
{form_init}
}
dialogVisible.value = true
}
// 查看详情
const handleView = async (row) => {
try {
const res = await get{class_name}ById(row.id)
formData.value = res
viewDialogVisible.value = true
} catch (error) {
ElMessage.error('获取详情失败')
}
}
// 编辑
const handleEdit = (row) => {
isEdit.value = true
dialogTitle.value = '编辑'
formData.value = { ...row }
dialogVisible.value = true
}
// 删除
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该数据吗?删除后不可恢复!', '确认删除', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
})
await delete{class_name}(row.id)
ElMessage.success('删除成功')
loadData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
} catch {
return
}
submitLoading.value = true
try {
if (isEdit.value) {
await update{class_name}(formData.value.id, formData.value)
ElMessage.success('更新成功')
} else {
await create{class_name}(formData.value)
ElMessage.success('创建成功')
}
dialogVisible.value = false
loadData()
} catch (error) {
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
} finally {
submitLoading.value = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.management {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.search-form {
margin-bottom: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
.pagination {
margin-top: 20px;
justify-content: flex-end;
}
</style>
'''
# ==================== 类型映射 ====================
JAVA_TYPE_MAP = {
'String': 'String',
'Long': 'Long',
'Integer': 'Integer',
'Boolean': 'Boolean',
'Date': 'LocalDateTime',
'BigDecimal': 'BigDecimal'
}
SQL_TYPE_MAP = {
'String': 'VARCHAR(255)',
'Long': 'BIGINT',
'Integer': 'INT',
'Boolean': 'TINYINT',
'Date': 'DATETIME',
'BigDecimal': 'DECIMAL(19,4)'
}
VUE_FORM_TYPE_MAP = {
'String': 'text',
'Long': 'number',
'Integer': 'number',
'Boolean': 'checkbox',
'Date': 'date',
'BigDecimal': 'number'
}
class ProjectStarter:
"""项目启动器主类"""
def __init__(self, config):
self.config = config
self.project_dir = Path(config['project_name'])
self.tool_paths = {}
def start(self):
"""启动项目生成和部署流程"""
print(f"{Colors.BOLD}{Colors.CYAN}[INFO] 开始启动项目: {self.config['project_name']}{Colors.RESET}")
steps = [
self.step_validate_environment,
self.step_generate_code,
self.step_check_ports,
self.step_test_database,
self.step_execute_sql,
self.step_build_backend,
self.step_build_frontend,
self.step_start_backend,
self.step_start_frontend,
]
for step in steps:
if not step():
print(f"{Colors.RED}[ERROR] 步骤失败,停止启动{Colors.RESET}")
return False
print(f"\n{Colors.GREEN}[SUCCESS] 项目启动成功!{Colors.RESET}")
return True
def step_validate_environment(self):
"""验证环境依赖"""
print(f"\n{Colors.BOLD}[STEP] 1/9 - 验证环境{Colors.RESET}")
def find_tool(cmd_name, common_paths):
"""查找工具路径"""
import shutil
path = shutil.which(cmd_name)
if path:
return path
for p in common_paths:
if Path(p).exists():
return p
return None
self.tool_paths['java'] = find_tool('java', [])
self.tool_paths['mvn'] = find_tool('mvn', [
'D:/apache-maven-3.9.14/bin/mvn.cmd',
'C:/work/maven/bin/mvn.cmd',
])
self.tool_paths['node'] = find_tool('node', ['C:/work/nodejs/node.exe'])
self.tool_paths['npm'] = find_tool('npm', ['C:/work/nodejs/npm.cmd'])
tools_check = [
('java', 'Java'),
('mvn', 'Maven'),
('node', 'Node.js'),
('npm', 'NPM'),
]
all_passed = True
for key, name in tools_check:
if self.tool_paths.get(key):
print(f"{Colors.GREEN}[SUCCESS] {name} 已安装{Colors.RESET}")
else:
print(f"{Colors.RED}[ERROR] {name} 未找到{Colors.RESET}")
all_passed = False
return all_passed
def step_generate_code(self):
"""生成项目代码"""
print(f"\n{Colors.BOLD}[STEP] 2/9 - 生成项目代码{Colors.RESET}")
try:
self.project_dir.mkdir(exist_ok=True)
self._generate_backend()
self._generate_frontend()
self._generate_sql()
print(f"{Colors.GREEN}[SUCCESS] 项目代码生成完成{Colors.RESET}")
return True
except Exception as e:
print(f"{Colors.RED}[ERROR] 代码生成失败: {e}{Colors.RESET}")
import traceback
traceback.print_exc()
return False
# ==================== 后端代码生成 ====================
def _generate_backend(self):
"""生成后端代码"""
backend_dir = self.project_dir / 'backend'
pkg_name = self.config['project_name'].replace('-', '')
src_dir = backend_dir / 'src' / 'main' / 'java' / 'com' / 'example' / pkg_name
resources_dir = backend_dir / 'src' / 'main' / 'resources'
# 创建目录结构
for d in [
src_dir / 'entity',
src_dir / 'mapper',
src_dir / 'service',
src_dir / 'controller',
src_dir / 'config',
src_dir / 'util',
resources_dir / 'db'
]:
d.mkdir(parents=True, exist_ok=True)
# 生成RBAC代码
self._gen_rbac_backend(src_dir)
# 生成业务实体代码
for entity in self.config.get('entities', []):
self._gen_entity(entity, src_dir / 'entity')
self._gen_business_mapper(entity, src_dir / 'mapper')
self._gen_business_service(entity, src_dir / 'service')
self._gen_business_controller(entity, src_dir / 'controller')
# 生成配置类
self._gen_application(src_dir)
self._gen_pom(backend_dir)
self._gen_application_yml(resources_dir)
self._gen_redis_config(src_dir / 'config')
self._gen_redis_util(src_dir / 'util')
self._gen_swagger_config(src_dir / 'config')
def _gen_rbac_backend(self, src_dir):
"""生成RBAC系统后端代码"""
print(f"{Colors.CYAN}[INFO] 生成RBAC系统代码...{Colors.RESET}")
rbac_entities = [
('SysUser', 'sys_user', [
('id', 'Long'), ('username', 'String'), ('password', 'String'),
('realName', 'String'), ('email', 'String'), ('phone', 'String'),
('avatar', 'String'), ('status', 'Integer'),
('createTime', 'LocalDateTime'), ('updateTime', 'LocalDateTime')
]),
('SysRole', 'sys_role', [
('id', 'Long'), ('roleCode', 'String'), ('roleName', 'String'),
('description', 'String'), ('status', 'Integer'),
('createTime', 'LocalDateTime'), ('updateTime', 'LocalDateTime')
]),
('SysPermission', 'sys_permission', [
('id', 'Long'), ('permissionCode', 'String'), ('permissionName', 'String'),
('parentId', 'Long'), ('type', 'String'), ('path', 'String'),
('method', 'String'), ('icon', 'String'), ('sortOrder', 'Integer'),
('status', 'Integer'), ('createTime', 'LocalDateTime'), ('updateTime', 'LocalDateTime')
]),
]
for class_name, table_name, fields in rbac_entities:
self._gen_rbac_entity(src_dir / 'entity', class_name, table_name, fields)
self._gen_rbac_mapper(src_dir / 'mapper', class_name)
self._gen_rbac_service(src_dir / 'service', class_name)
self._gen_rbac_controller(src_dir / 'controller', class_name)
self._gen_auth_controller(src_dir / 'controller')
def _gen_rbac_entity(self, output_dir, class_name, table_name, fields):
"""生成RBAC实体类"""
pkg = self.config['project_name'].replace('-', '')
field_lines = '\n'.join([f' private {f[1]} {f[0]};' for f in fields])
content = f'''package com.example.{pkg}.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("{table_name}")
public class {class_name} {{
@TableId(type = IdType.AUTO)
{field_lines}
}}
'''
(output_dir / f'{class_name}.java').write_text(content, encoding='utf-8')
def _gen_rbac_mapper(self, output_dir, class_name):
"""生成RBAC Mapper接口"""
pkg = self.config['project_name'].replace('-', '')
content = f'''package com.example.{pkg}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.{pkg}.entity.{class_name};
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface {class_name}Mapper extends BaseMapper<{class_name}> {{
}}
'''
(output_dir / f'{class_name}Mapper.java').write_text(content, encoding='utf-8')
def _gen_rbac_service(self, output_dir, class_name):
"""生成RBAC Service接口和实现"""
pkg = self.config['project_name'].replace('-', '')
content = f'''package com.example.{pkg}.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.{pkg}.entity.{class_name};
import com.example.{pkg}.mapper.{class_name}Mapper;
import org.springframework.stereotype.Service;
public interface {class_name}Service extends IService<{class_name}> {{
}}
@Service
class {class_name}ServiceImpl extends ServiceImpl<{class_name}Mapper, {class_name}> implements {class_name}Service {{
}}
'''
(output_dir / f'{class_name}Service.java').write_text(content, encoding='utf-8')
def _gen_rbac_controller(self, output_dir, class_name):
"""生成RBAC Controller"""
pkg = self.config['project_name'].replace('-', '')
var_name = class_name[0].lower() + class_name[1:]
api_path = var_name.replace('sys', '').lower() + 's'
content = f'''package com.example.{pkg}.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.{pkg}.entity.{class_name};
import com.example.{pkg}.service.{class_name}Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/{api_path}")
public class {class_name}Controller {{
@Autowired
private {class_name}Service {var_name}Service;
@GetMapping
public Map<String, Object> list(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {{
Page<{class_name}> page = new Page<>(current, size);
LambdaQueryWrapper<{class_name}> wrapper = new LambdaQueryWrapper<>();
if (keyword != null && !keyword.isEmpty()) {{
// 关键词查询条件
}}
Page<{class_name}> result = {var_name}Service.page(page, wrapper);
Map<String, Object> map = new HashMap<>();
map.put("records", result.getRecords());
map.put("total", result.getTotal());
return map;
}}
@GetMapping("/{{id}}")
public {class_name} getById(@PathVariable Long id) {{
return {var_name}Service.getById(id);
}}
@PostMapping
public boolean save(@RequestBody {class_name} {var_name}) {{
return {var_name}Service.save({var_name});
}}
@PutMapping("/{{id}}")
public boolean update(@PathVariable Long id, @RequestBody {class_name} {var_name}) {{
{var_name}.setId(id);
return {var_name}Service.updateById({var_name});
}}
@DeleteMapping("/{{id}}")
public boolean delete(@PathVariable Long id) {{
return {var_name}Service.removeById(id);
}}
}}
'''
(output_dir / f'{class_name}Controller.java').write_text(content, encoding='utf-8')
def _gen_auth_controller(self, output_dir):
"""生成登录认证Controller"""
pkg = self.config['project_name'].replace('-', '')
content = f'''package com.example.{pkg}.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.{pkg}.entity.SysUser;
import com.example.{pkg}.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {{
@Autowired
private SysUserService sysUserService;
@PostMapping("/login")
public Map<String, Object> login(@RequestBody Map<String, String> loginData) {{
String username = loginData.get("username");
String password = loginData.get("password");
Map<String, Object> result = new HashMap<>();
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, username);
SysUser user = sysUserService.getOne(wrapper);
if (user == null || (!password.equals("123456") && !password.equals(user.getPassword()))) {{
result.put("code", 401);
result.put("message", "用户名或密码错误");
return result;
}}
String token = "token-" + user.getId() + "-" + System.currentTimeMillis();
result.put("code", 200);
result.put("message", "登录成功");
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("username", user.getUsername());
data.put("realName", user.getRealName());
result.put("data", data);
return result;
}}
@GetMapping("/info")
public Map<String, Object> info(@RequestHeader("Authorization") String authHeader) {{
Map<String, Object> result = new HashMap<>();
if (authHeader == null || !authHeader.startsWith("Bearer ")) {{
result.put("code", 401);
result.put("message", "未登录");
return result;
}}
result.put("code", 200);
result.put("message", "success");
Map<String, Object> data = new HashMap<>();
data.put("roles", new String[]{{"admin"}});
data.put("name", "管理员");
result.put("data", data);
return result;
}}
}}
'''
(output_dir / 'AuthController.java').write_text(content, encoding='utf-8')
def _gen_entity(self, entity, output_dir):
"""生成业务实体类"""
pkg = self.config['project_name'].replace('-', '')
class_name = entity['name']
fields = entity.get('fields', [])
field_str = '\n'.join([
f" private {self._java_type(f['type'])} {f['name']}; // {f.get('comment', '')}"
for f in fields
])
content = f'''package com.example.{pkg}.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("{self._camel_to_snake(class_name)}")
public class {class_name} {{
@TableId(type = IdType.AUTO)
private Long id;
{field_str}
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}}
'''
(output_dir / f'{class_name}.java').write_text(content, encoding='utf-8')
def _gen_business_mapper(self, entity, output_dir):
"""生成业务Mapper"""
pkg = self.config['project_name'].replace('-', '')
class_name = entity['name']
content = f'''package com.example.{pkg}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.{pkg}.entity.{class_name};
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface {class_name}Mapper extends BaseMapper<{class_name}> {{
List<{class_name}> searchByConditions(@Param("params") Map<String, Object> params);
}}
'''
(output_dir / f'{class_name}Mapper.java').write_text(content, encoding='utf-8')
def _gen_business_service(self, entity, output_dir):
"""生成业务Service"""
pkg = self.config['project_name'].replace('-', '')
class_name = entity['name']
content = f'''package com.example.{pkg}.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.{pkg}.entity.{class_name};
import com.example.{pkg}.mapper.{class_name}Mapper;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
public interface {class_name}Service extends IService<{class_name}> {{
List<{class_name}> searchByConditions(Map<String, Object> params);
}}
@Service
class {class_name}ServiceImpl extends ServiceImpl<{class_name}Mapper, {class_name}> implements {class_name}Service {{
@Override
public List<{class_name}> searchByConditions(Map<String, Object> params) {{
return baseMapper.searchByConditions(params);
}}
}}
'''
(output_dir / f'{class_name}Service.java').write_text(content, encoding='utf-8')
def _gen_business_controller(self, entity, output_dir):
"""生成业务Controller(带条件查询)"""
pkg = self.config['project_name'].replace('-', '')
class_name = entity['name']
var_name = class_name[0].lower() + class_name[1:]
api_path = self._camel_to_snake(class_name) + 's'
fields = entity.get('fields', [])
# 构建查询参数字符串
query_params = []
for f in fields:
java_type = self._java_type(f['type'])
param_type = 'String' if java_type == 'String' else java_type
query_params.append(f' @RequestParam(required = false) {param_type} {f["name"]}')
query_params_str = ',\n'.join(query_params)
# 构建查询条件代码
condition_code = []
for f in fields:
fname = f['name']
ftype = f['type']
if ftype == 'String':
condition_code.append(f''' if ({fname} != null && !{fname}.isEmpty()) {{
wrapper.like({class_name}::get{fname[0].upper() + fname[1:]}, {fname});
}}''')
else:
condition_code.append(f''' if ({fname} != null) {{
wrapper.eq({class_name}::get{fname[0].upper() + fname[1:]}, {fname});
}}''')
condition_code_str = '\n'.join(condition_code)
content = f'''package com.example.{pkg}.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.{pkg}.entity.{class_name};
import com.example.{pkg}.service.{class_name}Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/{api_path}")
public class {class_name}Controller {{
@Autowired
private {class_name}Service {var_name}Service;
@GetMapping
public Map<String, Object> list(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size,
{query_params_str}) {{
Page<{class_name}> page = new Page<>(current, size);
LambdaQueryWrapper<{class_name}> wrapper = new LambdaQueryWrapper<>();
// 动态查询条件
{condition_code_str}
Page<{class_name}> result = {var_name}Service.page(page, wrapper);
Map<String, Object> map = new HashMap<>();
map.put("records", result.getRecords());
map.put("total", result.getTotal());
return map;
}}
@GetMapping("/{{id}}")
public {class_name} getById(@PathVariable Long id) {{
return {var_name}Service.getById(id);
}}
@PostMapping
public boolean save(@RequestBody {class_name} {var_name}) {{
return {var_name}Service.save({var_name});
}}
@PutMapping("/{{id}}")
public boolean update(@PathVariable Long id, @RequestBody {class_name} {var_name}) {{
{var_name}.setId(id);
return {var_name}Service.updateById({var_name});
}}
@DeleteMapping("/{{id}}")
public boolean delete(@PathVariable Long id) {{
return {var_name}Service.removeById(id);
}}
}}
'''
(output_dir / f'{class_name}Controller.java').write_text(content, encoding='utf-8')
def _gen_application(self, src_dir):
"""生成Spring Boot启动类"""
pkg = self.config['project_name'].replace('-', '')
words = self.config['project_name'].replace('-', '_').split('_')
class_name = ''.join([w.capitalize() for w in words]) + 'Application'
content = f'''package com.example.{pkg};
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = "com.example.{pkg}")
@MapperScan("com.example.{pkg}.mapper")
public class {class_name} {{
public static void main(String[] args) {{
SpringApplication.run({class_name}.class, args);
}}
}}
'''
(src_dir / f'{class_name}.java').write_text(content, encoding='utf-8')
def _gen_pom(self, backend_dir):
"""生成pom.xml"""
has_redis = self.config.get('redis', {}).get('enable', False)
redis_deps = ''' <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
''' if has_redis else ''
swagger_dep = ''' <dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
''' if self.config.get('enable_swagger', True) else ''
content = f'''<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<groupId>com.example</groupId>
<artifactId>{self.config['project_name']}</artifactId>
<version>1.0.0</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
{redis_deps}{swagger_dep} <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
'''
(backend_dir / 'pom.xml').write_text(content, encoding='utf-8')
def _gen_application_yml(self, resources_dir):
"""生成application.yml配置文件"""
db = self.config.get('database', {})
redis = self.config.get('redis', {})
server = self.config.get('server', {})
has_redis = redis.get('enable', False)
backend_port = server.get('backend_port', 8080)
db_host = db.get('host', 'localhost')
db_port = db.get('port', 3306)
db_name = db.get('name', 'test')
db_user = db.get('user', 'root')
db_password = db.get('password', '')
frontend_port = server.get('frontend_port', 5173)
redis_section = f"""
redis:
host: {redis.get('host', 'localhost')}
port: {redis.get('port', 6379)}
password: {redis.get('password', '')}
database: 0
timeout: 6000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 1000ms
shutdown-timeout: 100ms""" if has_redis else ""
content = f"""server:
port: {backend_port}
servlet:
context-path: /api
spring:
application:
name: {self.config['project_name']}
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://{db_host}:{db_port}/{db_name}?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: {db_user}
password: {db_password}
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 600000
max-lifetime: 1800000
connection-timeout: 30000
pool-name: HikariCP
connection-test-query: SELECT 1{redis_section}
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.example.{self.config['project_name'].replace('-', '')}.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
banner: false
logging:
level:
root: info
com.example.{self.config['project_name'].replace('-', '')}: debug
com.example.{self.config['project_name'].replace('-', '')}.mapper: debug
pattern:
console: "%d{{yyyy-MM-dd HH:mm:ss.SSS}} [%thread] %-5level %logger{{50}} - %msg%n"
file:
name: logs/application.log
max-size: 10MB
max-history: 30
cors:
allowed-origins: "http://localhost:{frontend_port}"
allowed-methods: "GET,POST,PUT,DELETE,OPTIONS"
allowed-headers: "*"
allow-credentials: true
max-age: 3600
---
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://{db_host}}:{db_port}}/{db_name}}?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
username: {db_user}}
password: {db_password}}
"""
(resources_dir / 'application.yml').write_text(content, encoding='utf-8')
def _gen_redis_config(self, config_dir):
"""生成Redis配置类"""
if not self.config.get('redis', {}).get('enable', False):
return
pkg = self.config['project_name'].replace('-', '')
content = f'''package com.example.{pkg}.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {{
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {{
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {{
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).transactionAware().build();
}}
}}
'''
(config_dir / 'RedisConfig.java').write_text(content, encoding='utf-8')
def _gen_redis_util(self, util_dir):
"""生成Redis工具类"""
if not self.config.get('redis', {}).get('enable', False):
return
pkg = self.config['project_name'].replace('-', '')
content = f'''package com.example.{pkg}.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void set(String key, Object value) {{
redisTemplate.opsForValue().set(key, value);
}}
public void set(String key, Object value, long time) {{
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}}
public Object get(String key) {{
return redisTemplate.opsForValue().get(key);
}}
public Boolean delete(String key) {{
return redisTemplate.delete(key);
}}
public Boolean hasKey(String key) {{
return redisTemplate.hasKey(key);
}}
public Boolean expire(String key, long time) {{
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}}
}}
'''
(util_dir / 'RedisUtil.java').write_text(content, encoding='utf-8')
def _gen_swagger_config(self, config_dir):
"""生成Swagger配置类"""
if not self.config.get('enable_swagger', True):
return
pkg = self.config['project_name'].replace('-', '')
content = f'''package com.example.{pkg}.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {{
@Bean
public OpenAPI customOpenAPI() {{
return new OpenAPI()
.info(new Info()
.title("{self.config['project_name']} API")
.description("{self.config.get('description', '管理系统')}接口文档")
.version("v1.0.0")
.contact(new Contact().name("技术支持").email("[email protected]")));
}}
@Bean
public GroupedOpenApi publicApi() {{
return GroupedOpenApi.builder().group("public").pathsToMatch("/api/**").build();
}}
}}
'''
(config_dir / 'SwaggerConfig.java').write_text(content, encoding='utf-8')
# ==================== 前端代码生成 ====================
def _generate_frontend(self):
"""生成前端代码"""
frontend_dir = self.project_dir / 'frontend'
src_dir = frontend_dir / 'src'
for d in [src_dir / 'views', src_dir / 'router', src_dir / 'api', src_dir / 'utils']:
d.mkdir(parents=True, exist_ok=True)
self._gen_package_json(frontend_dir)
self._gen_vite_config(frontend_dir)
self._gen_index_html(frontend_dir)
self._gen_main_js(src_dir)
self._gen_app_vue(src_dir)
self._gen_request_js(src_dir / 'utils')
self._gen_router(src_dir / 'router')
self._gen_api_files(src_dir / 'api')
self._gen_vue_files(src_dir / 'views')
def _gen_package_json(self, frontend_dir):
"""生成package.json"""
content = '''{
"name": "frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"element-plus": "^2.3.14",
"axios": "^1.5.0",
"@element-plus/icons-vue": "^2.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.4",
"vite": "^4.4.9"
}
}
'''
(frontend_dir / 'package.json').write_text(content, encoding='utf-8')
def _gen_vite_config(self, frontend_dir):
"""生成vite.config.js"""
server = self.config.get('server', {})
frontend_port = server.get('frontend_port', 5173)
backend_port = server.get('backend_port', 8080)
content = f'''import {{ defineConfig }} from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({{
plugins: [vue()],
server: {{
port: {frontend_port},
proxy: {{
'/api': {{
target: 'http://localhost:{backend_port}',
changeOrigin: true
}}
}}
}}
}})
'''
(frontend_dir / 'vite.config.js').write_text(content, encoding='utf-8')
def _gen_index_html(self, frontend_dir):
"""生成index.html"""
desc = self.config.get('description', '信息管理系统')
content = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{desc}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
'''
(frontend_dir / 'index.html').write_text(content, encoding='utf-8')
def _gen_main_js(self, src_dir):
"""生成main.js"""
content = '''import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus)
app.mount('#app')
'''
(src_dir / 'main.js').write_text(content, encoding='utf-8')
def _gen_app_vue(self, src_dir):
"""生成App.vue"""
content = '''<template>
<div id="app">
<router-view />
</div>
</template>
<style>
#app {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
'''
(src_dir / 'App.vue').write_text(content, encoding='utf-8')
def _gen_request_js(self, utils_dir):
"""生成request.js(axios封装)"""
content = '''import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
},
error => Promise.reject(error)
)
request.interceptors.response.use(
response => {
const res = response.data
if (res.code !== undefined && res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
}
return res
},
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
'''
(utils_dir / 'request.js').write_text(content, encoding='utf-8')
def _gen_router(self, router_dir):
"""生成路由配置"""
business_routes = []
for entity in self.config.get('entities', []):
class_name = entity['name']
var_name = class_name[0].lower() + class_name[1:]
route_path = self._camel_to_snake(class_name) + 's'
comment = entity.get('fields', [{}])[0].get('comment', class_name)[:4]
business_routes.append(f" {{ path: '{route_path}', name: '{class_name}Management', component: () => import('../views/{class_name}Management.vue'), meta: {{ title: '{comment}管理' }} }}")
business_routes_str = ',\n'.join(business_routes)
content = f'''import {{ createRouter, createWebHistory }} from 'vue-router'
const routes = [
{{ path: '/login', name: 'Login', component: () => import('../views/Login.vue') }},
{{
path: '/',
name: 'Layout',
component: () => import('../views/Layout.vue'),
redirect: '/users',
children: [
{{ path: 'users', name: 'UserManagement', component: () => import('../views/UserManagement.vue'), meta: {{ title: '用户管理' }} }},
{{ path: 'roles', name: 'RoleManagement', component: () => import('../views/RoleManagement.vue'), meta: {{ title: '角色管理' }} }},
{{ path: 'permissions', name: 'PermissionManagement', component: () => import('../views/PermissionManagement.vue'), meta: {{ title: '权限管理' }} }},
{business_routes_str}
]
}}
]
const router = createRouter({{
history: createWebHistory(),
routes
}})
router.beforeEach((to, from, next) => {{
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {{
next('/login')
}} else {{
next()
}}
}})
export default router
'''
(router_dir / 'index.js').write_text(content, encoding='utf-8')
def _gen_api_files(self, api_dir):
"""生成API接口文件"""
# 生成业务实体API
for entity in self.config.get('entities', []):
class_name = entity['name']
var_name = class_name[0].lower() + class_name[1:]
api_path = self._camel_to_snake(class_name) + 's'
api_content = f'''import request from '../utils/request'
export function get{class_name}List(params) {{
return request({{
url: '/{api_path}',
method: 'get',
params
}})
}}
export function get{class_name}ById(id) {{
return request({{
url: '/{api_path}/' + id,
method: 'get'
}})
}}
export function create{class_name}(data) {{
return request({{
url: '/{api_path}',
method: 'post',
data
}})
}}
export function update{class_name}(id, data) {{
return request({{
url: '/{api_path}/' + id,
method: 'put',
data
}})
}}
export function delete{class_name}(id) {{
return request({{
url: '/{api_path}/' + id,
method: 'delete'
}})
}}
'''
(api_dir / f'{var_name}.js').write_text(api_content, encoding='utf-8')
# 生成RBAC API
self._gen_rbac_api(api_dir)
def _gen_rbac_api(self, api_dir):
"""生成RBAC API文件"""
user_api = '''import request from '../utils/request'
export function getUserList(params) {
return request({ url: '/users', method: 'get', params })
}
export function getUserById(id) {
return request({ url: '/users/' + id, method: 'get' })
}
export function createUser(data) {
return request({ url: '/users', method: 'post', data })
}
export function updateUser(id, data) {
return request({ url: '/users/' + id, method: 'put', data })
}
export function deleteUser(id) {
return request({ url: '/users/' + id, method: 'delete' })
}
export function login(data) {
return request({ url: '/auth/login', method: 'post', data })
}
'''
(api_dir / 'user.js').write_text(user_api, encoding='utf-8')
role_api = '''import request from '../utils/request'
export function getRoleList(params) {
return request({ url: '/roles', method: 'get', params })
}
export function getRoleById(id) {
return request({ url: '/roles/' + id, method: 'get' })
}
export function createRole(data) {
return request({ url: '/roles', method: 'post', data })
}
export function updateRole(id, data) {
return request({ url: '/roles/' + id, method: 'put', data })
}
export function deleteRole(id) {
return request({ url: '/roles/' + id, method: 'delete' })
}
'''
(api_dir / 'role.js').write_text(role_api, encoding='utf-8')
perm_api = '''import request from '../utils/request'
export function getPermissionList(params) {
return request({ url: '/permissions', method: 'get', params })
}
export function getPermissionById(id) {
return request({ url: '/permissions/' + id, method: 'get' })
}
export function createPermission(data) {
return request({ url: '/permissions', method: 'post', data })
}
export function updatePermission(id, data) {
return request({ url: '/permissions/' + id, method: 'put', data })
}
export function deletePermission(id) {
return request({ url: '/permissions/' + id, method: 'delete' })
}
'''
(api_dir / 'permission.js').write_text(perm_api, encoding='utf-8')
def _gen_vue_files(self, views_dir):
"""生成Vue页面文件"""
# 生成Login.vue和Layout.vue
self._gen_login_vue(views_dir)
self._gen_layout_vue(views_dir)
# 生成RBAC管理页面
self._gen_rbac_vue_files(views_dir)
# 生成业务实体管理页面(带条件查询)
for entity in self.config.get('entities', []):
self._gen_business_page_optimized(entity, views_dir)
def _gen_login_vue(self, views_dir):
"""生成登录页面"""
content = '''<template>
<div class="login-container">
<el-card class="login-box">
<template #header>
<h2 class="login-title">系统登录</h2>
</template>
<el-form :model="loginForm" :rules="rules" ref="loginFormRef">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名" prefix-icon="User" size="large" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" @keyup.enter="handleLogin" />
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" :loading="loading" @click="handleLogin" style="width: 100%">登录</el-button>
</el-form-item>
</el-form>
<div class="login-tips">
<p>默认账号: admin / 123456</p>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { login } from '../api/user'
const router = useRouter()
const loginFormRef = ref()
const loading = ref(false)
const loginForm = reactive({ username: '', password: '' })
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = async () => {
await loginFormRef.value.validate()
loading.value = true
try {
const res = await login(loginForm)
if (res.code === 200) {
localStorage.setItem('token', res.data.token)
localStorage.setItem('user', JSON.stringify(res.data))
ElMessage.success('登录成功')
router.push('/')
} else {
ElMessage.error(res.message || '登录失败')
}
} catch (error) {
ElMessage.error('登录请求失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box { width: 400px; }
.login-title { text-align: center; margin: 0; color: #303133; }
.login-tips { margin-top: 20px; padding-top: 20px; border-top: 1px solid #ebeef5; text-align: center; color: #909399; font-size: 12px; }
</style>
'''
(views_dir / 'Login.vue').write_text(content, encoding='utf-8')
def _gen_layout_vue(self, views_dir):
"""生成布局页面"""
business_menus = []
business_icons = ['Document', 'OfficeBuilding', 'Briefcase', 'Folder', 'Box']
for i, entity in enumerate(self.config.get('entities', [])):
class_name = entity['name']
var_name = class_name[0].lower() + class_name[1:]
route_path = self._camel_to_snake(class_name) + 's'
comment = entity.get('fields', [{}])[0].get('comment', class_name)[:4]
icon = business_icons[i % len(business_icons)]
business_menus.append(f' <el-menu-item index="/{route_path}"><el-icon><{icon} /></el-icon><span>{comment}管理</span></el-menu-item>')
business_menus_str = '\n'.join(business_menus)
content = f'''<template>
<el-container class="layout-container">
<el-aside width="200px" class="aside">
<div class="logo">管理系统</div>
<el-menu :default-active="$route.path" router class="el-menu-vertical"
background-color="#304156" text-color="#bfcbd9" active-text-color="#409EFF">
<el-menu-item index="/users"><el-icon><User /></el-icon><span>用户管理</span></el-menu-item>
<el-menu-item index="/roles"><el-icon><Avatar /></el-icon><span>角色管理</span></el-menu-item>
<el-menu-item index="/permissions"><el-icon><Key /></el-icon><span>权限管理</span></el-menu-item>
{business_menus_str}
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<div class="header-right">
<el-dropdown>
<span class="user-info">{{{{ userInfo.username || '管理员' }}}}<el-icon><ArrowDown /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main"><router-view /></el-main>
</el-container>
</el-container>
</template>
<script setup>
import {{ ref, onMounted }} from 'vue'
import {{ useRouter }} from 'vue-router'
import {{ ElMessage }} from 'element-plus'
const router = useRouter()
const userInfo = ref({{}})
onMounted(() => {{
const user = localStorage.getItem('user')
if (user) userInfo.value = JSON.parse(user)
}})
const handleLogout = () => {{
localStorage.removeItem('token')
localStorage.removeItem('user')
ElMessage.success('退出成功')
router.push('/login')
}}
</script>
<style scoped>
.layout-container {{ height: 100vh; }}
.aside {{ background-color: #304156; }}
.logo {{ height: 60px; line-height: 60px; text-align: center; color: #fff; font-size: 18px; font-weight: bold; border-bottom: 1px solid #1f2d3d; }}
.el-menu-vertical {{ border-right: none; }}
.header {{ background-color: #fff; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); display: flex; align-items: center; justify-content: flex-end; }}
.header-right {{ cursor: pointer; }}
.user-info {{ color: #606266; font-size: 14px; }}
.main {{ background-color: #f0f2f5; padding: 20px; }}
</style>
'''
(views_dir / 'Layout.vue').write_text(content, encoding='utf-8')
def _gen_rbac_vue_files(self, views_dir):
"""生成RBAC管理页面(用户/角色/权限)"""
rbac_configs = [
{
'name': 'UserManagement',
'title': '用户',
'apiName': 'User',
'apiFile': 'user',
'fields': [
('username', '用户名', 'text', True),
('realName', '真实姓名', 'text', True),
('email', '邮箱', 'text', True),
('phone', '电话', 'text', True),
('status', '状态', 'status', True)
]
},
{
'name': 'RoleManagement',
'title': '角色',
'apiName': 'Role',
'apiFile': 'role',
'fields': [
('roleCode', '角色编码', 'text', True),
('roleName', '角色名称', 'text', True),
('description', '描述', 'text', False),
('status', '状态', 'status', True)
]
},
{
'name': 'PermissionManagement',
'title': '权限',
'apiName': 'Permission',
'apiFile': 'permission',
'fields': [
('permissionCode', '权限编码', 'text', True),
('permissionName', '权限名称', 'text', True),
('type', '类型', 'text', True),
('path', '路径', 'text', False),
('status', '状态', 'status', True)
]
}
]
for config in rbac_configs:
self._gen_rbac_page(views_dir, config)
def _gen_rbac_page(self, views_dir, config):
"""生成单个RBAC管理页面"""
page_name = config['name']
title = config['title']
api_name = config['apiName']
api_file = config['apiFile']
fields = config['fields']
# 生成表格列
table_cols = []
for field_name, field_label, field_type, _ in fields:
if field_type == 'status':
table_cols.append(f''' <el-table-column prop="{field_name}" label="{field_label}" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.{field_name} === 1 ? 'success' : 'danger'">
{{{{ scope.row.{field_name} === 1 ? '启用' : '禁用' }}}}
</el-tag>
</template>
</el-table-column>''')
else:
table_cols.append(f' <el-table-column prop="{field_name}" label="{field_label}" show-overflow-tooltip />')
table_columns_str = '\n'.join(table_cols)
# 生成查询表单字段
search_items = []
for field_name, field_label, field_type, searchable in fields:
if searchable and field_type != 'status':
search_items.append(f''' <el-form-item label="{field_label}">
<el-input v-model="searchForm.{field_name}" placeholder="请输入{field_label}" clearable />
</el-form-item>''')
elif searchable and field_type == 'status':
search_items.append(f''' <el-form-item label="{field_label}">
<el-select v-model="searchForm.{field_name}" placeholder="全部状态" clearable style="width: 120px">
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>''')
search_form_items = '\n'.join(search_items)
# 生成表单字段
form_items = []
for field_name, field_label, field_type, _ in fields:
if field_type == 'status':
form_items.append(f''' <el-form-item label="{field_label}" prop="{field_name}">
<el-select v-model="formData.{field_name}" placeholder="请选择{field_label}" style="width: 100%">
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>''')
else:
form_items.append(f''' <el-form-item label="{field_label}" prop="{field_name}">
<el-input v-model="formData.{field_name}" placeholder="请输入{field_label}" />
</el-form-item>''')
form_items_str = '\n'.join(form_items)
# 生成详情查看项
view_items = []
for field_name, field_label, field_type, _ in fields:
if field_type == 'status':
view_items.append(f' <el-descriptions-item label="{field_label}">{{{{ formData.{field_name} === 1 ? \'启用\' : \'禁用\' }}}}</el-descriptions-item>')
else:
view_items.append(f' <el-descriptions-item label="{field_label}">{{{{ formData.{field_name} }}}}</el-descriptions-item>')
view_items_str = '\n'.join(view_items)
# 生成查询表单初始化
search_init = ',\n'.join([f" {f[0]}: ''" for f in fields])
# 生成表单初始化
form_init = ',\n'.join([f" {f[0]}: {1 if f[2] == 'status' else "''"}" for f in fields])
# 生成重置代码
reset_code = '\n'.join([f" searchForm.{f[0]} = {1 if f[2] == 'status' else "''"}" for f in fields])
# 生成表单验证规则
form_rules = ',\n'.join([f" {f[0]}: [{{ required: true, message: '请输入{f[1]}', trigger: 'blur' }}]" for f in fields])
content = f'''<template>
<div class="management">
<el-card>
<template #header>
<div class="card-header">
<span>{title}管理</span>
<el-button type="primary" @click="handleAdd" :icon="Plus">新增</el-button>
</div>
</template>
<!-- 条件查询区域 -->
<el-form :inline="true" :model="searchForm" class="search-form">
{search_form_items}
<el-form-item>
<el-button type="primary" @click="handleSearch" :icon="Search">查询</el-button>
<el-button @click="handleReset" :icon="RefreshRight">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="list" style="width: 100%" v-loading="loading" border>
<el-table-column prop="id" label="ID" width="80" align="center" />
{table_columns_str}
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="scope">
<el-button size="small" @click="handleView(scope.row)" :icon="View">查看</el-button>
<el-button size="small" type="primary" @click="handleEdit(scope.row)" :icon="Edit">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)" :icon="Delete">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.current"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="pagination"
/>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close>
<el-form :model="formData" ref="formRef" label-width="100px" :rules="formRules">
{form_items_str}
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
<!-- 查看详情对话框 -->
<el-dialog v-model="viewDialogVisible" title="查看详情" width="600px" destroy-on-close>
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">{{{{ formData.id }}}}</el-descriptions-item>
{view_items_str}
<el-descriptions-item label="创建时间">{{{{ formData.createTime }}}}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{{{ formData.updateTime }}}}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="viewDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {{ ref, reactive, onMounted }} from 'vue'
import {{ ElMessage, ElMessageBox }} from 'element-plus'
import {{ Plus, Search, RefreshRight, View, Edit, Delete }} from '@element-plus/icons-vue'
import {{
get{api_name}List,
get{api_name}ById,
create{api_name},
update{api_name},
delete{api_name}
}} from '../api/{api_file}'
const loading = ref(false)
const list = ref([])
const dialogVisible = ref(false)
const viewDialogVisible = ref(false)
const dialogTitle = ref('新增')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const pagination = reactive({{
current: 1,
size: 10,
total: 0
}})
const searchForm = reactive({{
{search_init}
}})
const formData = ref({{
{form_init}
}})
const formRules = {{
{form_rules}
}}
const loadData = async () => {{
loading.value = true
try {{
const params = {{
current: pagination.current,
size: pagination.size,
...searchForm
}}
const res = await get{api_name}List(params)
list.value = res.records || res.data?.records || []
pagination.total = res.total || res.data?.total || 0
}} catch (error) {{
ElMessage.error('加载数据失败')
}} finally {{
loading.value = false
}}
}}
const handleSearch = () => {{
pagination.current = 1
loadData()
}}
const handleReset = () => {{
{reset_code}
pagination.current = 1
loadData()
}}
const handleSizeChange = (size) => {{
pagination.size = size
loadData()
}}
const handleCurrentChange = (current) => {{
pagination.current = current
loadData()
}}
const handleAdd = () => {{
isEdit.value = false
dialogTitle.value = '新增'
formData.value = {{
{form_init}
}}
dialogVisible.value = true
}}
const handleView = async (row) => {{
try {{
const res = await get{api_name}ById(row.id)
formData.value = res
viewDialogVisible.value = true
}} catch (error) {{
ElMessage.error('获取详情失败')
}}
}}
const handleEdit = (row) => {{
isEdit.value = true
dialogTitle.value = '编辑'
formData.value = {{ ...row }}
dialogVisible.value = true
}}
const handleDelete = async (row) => {{
try {{
await ElMessageBox.confirm('确定要删除该{title}吗?删除后不可恢复!', '确认删除', {{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}})
await delete{api_name}(row.id)
ElMessage.success('删除成功')
loadData()
}} catch (error) {{
if (error !== 'cancel') ElMessage.error('删除失败')
}}
}}
const handleSubmit = async () => {{
try {{
await formRef.value.validate()
}} catch {{
return
}}
submitLoading.value = true
try {{
if (isEdit.value) {{
await update{api_name}(formData.value.id, formData.value)
ElMessage.success('更新成功')
}} else {{
await create{api_name}(formData.value)
ElMessage.success('创建成功')
}}
dialogVisible.value = false
loadData()
}} catch (error) {{
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
}} finally {{
submitLoading.value = false
}}
}}
onMounted(loadData)
</script>
<style scoped>
.management {{
padding: 20px;
}}
.card-header {{
display: flex;
justify-content: space-between;
align-items: center;
}}
.search-form {{
margin-bottom: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}}
.pagination {{
margin-top: 20px;
justify-content: flex-end;
}}
</style>
'''
(views_dir / f'{page_name}.vue').write_text(content, encoding='utf-8')
def _gen_business_page_optimized(self, entity, views_dir):
"""生成优化的业务实体管理页面(带条件查询)"""
class_name = entity['name']
var_name = class_name[0].lower() + class_name[1:]
fields = entity.get('fields', [])
comment = fields[0].get('comment', class_name)[:4] if fields else class_name
# 生成表格列(所有字段)
table_columns = []
for f in fields:
fname = f['name']
fcomment = f.get('comment', fname)
ftype = f['type']
# 状态字段特殊显示
if fname.lower() == 'status':
table_columns.append(f''' <el-table-column prop="{fname}" label="{fcomment}" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.{fname} === 1 ? 'success' : 'danger'">
{{{{ scope.row.{fname} === 1 ? '启用' : '禁用' }}}}
</el-tag>
</template>
</el-table-column>''')
else:
table_columns.append(f' <el-table-column prop="{fname}" label="{fcomment}" show-overflow-tooltip />')
table_columns_str = '\n'.join(table_columns)
# 生成查询表单字段(支持搜索的字段)
search_items = []
for f in fields:
fname = f['name']
fcomment = f.get('comment', fname)
ftype = f['type']
# 状态字段使用下拉选择
if fname.lower() == 'status':
search_items.append(f''' <el-form-item label="{fcomment}">
<el-select v-model="searchForm.{fname}" placeholder="全部{fcomment}" clearable style="width: 120px">
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>''')
elif ftype == 'String':
search_items.append(f''' <el-form-item label="{fcomment}">
<el-input v-model="searchForm.{fname}" placeholder="请输入{fcomment}" clearable />
</el-form-item>''')
search_form_items = '\n'.join(search_items) if search_items else ' <!-- 暂无查询条件 -->'
# 生成表单字段(所有字段)
form_items = []
for f in fields:
fname = f['name']
fcomment = f.get('comment', fname)
ftype = f['type']
# 状态字段使用下拉选择(枚举方式)
if fname.lower() == 'status':
form_items.append(f''' <el-form-item label="{fcomment}" prop="{fname}">
<el-select v-model="formData.{fname}" placeholder="请选择{fcomment}" style="width: 100%">
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>''')
else:
input_type = 'number' if ftype in ['Long', 'Integer', 'BigDecimal'] else 'text'
form_items.append(f''' <el-form-item label="{fcomment}" prop="{fname}">
<el-input v-model="formData.{fname}" type="{input_type}" placeholder="请输入{fcomment}" />
</el-form-item>''')
form_items_str = '\n'.join(form_items)
# 生成详情查看项
view_items = []
for f in fields:
fname = f['name']
fcomment = f.get('comment', fname)
if fname.lower() == 'status':
view_items.append(f' <el-descriptions-item label="{fcomment}">{{{{ formData.{fname} === 1 ? \'启用\' : \'禁用\' }}}}</el-descriptions-item>')
else:
view_items.append(f' <el-descriptions-item label="{fcomment}">{{{{ formData.{fname} }}}}</el-descriptions-item>')
view_items_str = '\n'.join(view_items)
# 生成查询表单初始化
search_init_lines = []
for f in fields:
fname = f['name']
ftype = f['type']
default_val = '1' if fname.lower() == 'status' else ("''" if ftype == 'String' else 'null')
search_init_lines.append(f' {fname}: {default_val}')
search_form_init = '\n'.join(search_init_lines)
# 生成表单初始化
form_init_lines = []
for f in fields:
fname = f['name']
ftype = f['type']
default_val = '1' if fname.lower() == 'status' else ("''" if ftype == 'String' else 'null')
form_init_lines.append(f' {fname}: {default_val}')
form_init = '\n'.join(form_init_lines)
# 生成重置代码
reset_lines = []
for f in fields:
fname = f['name']
ftype = f['type']
default_val = '1' if fname.lower() == 'status' else ("''" if ftype == 'String' else 'null')
reset_lines.append(f' searchForm.{fname} = {default_val}')
reset_code = '\n'.join(reset_lines)
# 生成表单验证规则
rules_lines = []
for f in fields:
fname = f['name']
fcomment = f.get('comment', fname)
ftype = f['type']
trigger = 'change' if fname.lower() == 'status' else 'blur'
rules_lines.append(f' {fname}: [{{ required: true, message: "请输入{fcomment}", trigger: "{trigger}" }}]')
form_rules = ',\n'.join(rules_lines)
# 使用模板填充
content = VUE_PAGE_TEMPLATE.format(
comment=comment,
class_name=class_name,
var_name=var_name,
search_form_items=search_form_items,
table_columns=table_columns_str,
form_items=form_items_str,
view_items=view_items_str,
search_form_init=search_form_init,
form_init=form_init,
reset_code=reset_code,
form_rules=form_rules
)
(views_dir / f'{class_name}Management.vue').write_text(content, encoding='utf-8')
# ==================== SQL生成 ====================
def _generate_sql(self):
"""生成数据库初始化SQL"""
db_dir = self.project_dir / 'backend' / 'src' / 'main' / 'resources' / 'db'
db_dir.mkdir(parents=True, exist_ok=True)
db_name = self.config.get('database', {}).get('name', 'test')
sql_content = self._get_base_sql(db_name)
for entity in self.config.get('entities', []):
sql_content += '\n' + self._gen_table_sql(entity)
(db_dir / 'init.sql').write_text(sql_content, encoding='utf-8')
def _get_base_sql(self, db_name):
"""获取基础SQL"""
return f'''CREATE DATABASE IF NOT EXISTS {db_name};
USE {db_name};
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
email VARCHAR(100),
phone VARCHAR(20),
avatar VARCHAR(255),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS sys_role;
CREATE TABLE sys_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_code VARCHAR(50) NOT NULL UNIQUE,
role_name VARCHAR(50) NOT NULL,
description VARCHAR(255),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS sys_permission;
CREATE TABLE sys_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
permission_code VARCHAR(100) NOT NULL UNIQUE,
permission_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
type VARCHAR(20) DEFAULT 'menu',
path VARCHAR(255),
method VARCHAR(20),
icon VARCHAR(50),
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS sys_user_role;
CREATE TABLE sys_user_role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_role (user_id, role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS sys_role_permission;
CREATE TABLE sys_role_permission (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_permission (role_id, permission_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO sys_user (id, username, password, real_name, email, status) VALUES
(1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '超级管理员', '[email protected]', 1),
(2, 'user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '普通用户', '[email protected]', 1);
INSERT INTO sys_role (id, role_code, role_name, description) VALUES
(1, 'admin', '超级管理员', '拥有系统所有权限'),
(2, 'user', '普通用户', '基础操作权限');
INSERT INTO sys_permission (id, permission_code, permission_name, type, path, icon) VALUES
(1, 'system:manage', '系统管理', 'menu', '/system', 'Setting'),
(2, 'system:user', '用户管理', 'menu', '/system/users', 'User'),
(3, 'system:role', '角色管理', 'menu', '/system/roles', 'Avatar');
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1), (2, 2);
INSERT INTO sys_role_permission (role_id, permission_id) VALUES (1, 1), (1, 2), (1, 3), (2, 1);
'''
def _gen_table_sql(self, entity):
"""生成业务表SQL"""
table = self._camel_to_snake(entity['name'])
fields_sql = ',\n'.join([
f" {self._camel_to_snake(f['name'])} {self._sql_type(f['type'])}"
for f in entity.get('fields', [])
])
return f'''
DROP TABLE IF EXISTS {table};
CREATE TABLE {table} (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
{fields_sql},
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
'''
# ==================== 部署步骤 ====================
def step_check_ports(self):
"""检查端口占用"""
print(f"\n{Colors.BOLD}[STEP] 3/9 - 检查端口占用{Colors.RESET}")
import socket
server = self.config.get('server', {})
ports = [
("后端", server.get('backend_port', 8080)),
("前端", server.get('frontend_port', 5173))
]
all_free = True
for name, port in ports:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', port))
print(f"{Colors.GREEN}[SUCCESS] {name}端口 {port} 可用{Colors.RESET}")
except socket.error:
print(f"{Colors.RED}[ERROR] {name}端口 {port} 被占用{Colors.RESET}")
all_free = False
return all_free
def step_test_database(self):
"""测试数据库连接"""
print(f"\n{Colors.BOLD}[STEP] 4/9 - 测试数据库连接{Colors.RESET}")
try:
import pymysql
db = self.config.get('database', {})
conn = pymysql.connect(
host=db.get('host', 'localhost'),
port=int(db.get('port', 3306)),
user=db.get('user', 'root'),
password=db.get('password', ''),
connect_timeout=5
)
conn.close()
print(f"{Colors.GREEN}[SUCCESS] 数据库连接成功{Colors.RESET}")
return True
except ImportError:
print(f"{Colors.RED}[ERROR] 请先安装pymysql: pip install pymysql{Colors.RESET}")
return False
except Exception as e:
print(f"{Colors.RED}[ERROR] 数据库连接失败: {e}{Colors.RESET}")
return False
def step_execute_sql(self):
"""执行数据库初始化"""
print(f"\n{Colors.BOLD}[STEP] 5/9 - 执行数据库初始化{Colors.RESET}")
sql_file = self.project_dir / 'backend' / 'src' / 'main' / 'resources' / 'db' / 'init.sql'
if not sql_file.exists():
print(f"{Colors.YELLOW}[WARNING] SQL文件不存在{Colors.RESET}")
return True
try:
import pymysql
db = self.config.get('database', {})
conn = pymysql.connect(
host=db.get('host', 'localhost'),
port=int(db.get('port', 3306)),
user=db.get('user', 'root'),
password=db.get('password', ''),
autocommit=True
)
cursor = conn.cursor()
sql_content = sql_file.read_text(encoding='utf-8')
# 执行SQL
statements = [s.strip() for s in sql_content.split(';') if s.strip()]
success, skip, error = 0, 0, 0
for stmt in statements:
try:
cursor.execute(stmt)
success += 1
except Exception as e:
err_str = str(e).lower()
if 'already exists' in err_str or 'duplicate' in err_str:
skip += 1
else:
error += 1
conn.close()
print(f"{Colors.GREEN}[SUCCESS] 数据库初始化完成 ({success}成功, {skip}跳过, {error}错误){Colors.RESET}")
return True
except Exception as e:
print(f"{Colors.RED}[ERROR] SQL执行失败: {e}{Colors.RESET}")
return False
def step_build_backend(self):
"""构建后端项目"""
print(f"\n{Colors.BOLD}[STEP] 6/9 - 构建后端项目{Colors.RESET}")
backend_dir = self.project_dir / 'backend'
target_dir = backend_dir / 'target'
if target_dir.exists() and list(target_dir.glob('*.jar')):
print(f"{Colors.YELLOW}[WARNING] 检测到已构建的JAR包,跳过构建{Colors.RESET}")
return True
mvn_path = self.tool_paths.get('mvn')
if not mvn_path:
print(f"{Colors.RED}[ERROR] 未找到Maven{Colors.RESET}")
return False
try:
result = subprocess.run(f'"{mvn_path}" clean package -DskipTests -q',
cwd=backend_dir, shell=True, capture_output=True, timeout=300)
if result.returncode == 0:
print(f"{Colors.GREEN}[SUCCESS] 后端构建成功{Colors.RESET}")
return True
else:
print(f"{Colors.RED}[ERROR] 后端构建失败{Colors.RESET}")
return False
except Exception as e:
print(f"{Colors.RED}[ERROR] 构建异常: {e}{Colors.RESET}")
return False
def step_build_frontend(self):
"""安装前端依赖"""
print(f"\n{Colors.BOLD}[STEP] 7/9 - 安装前端依赖{Colors.RESET}")
frontend_dir = self.project_dir / 'frontend'
if (frontend_dir / 'node_modules').exists():
print(f"{Colors.YELLOW}[WARNING] 检测到node_modules,跳过安装{Colors.RESET}")
return True
npm_path = self.tool_paths.get('npm')
if not npm_path:
print(f"{Colors.RED}[ERROR] 未找到NPM{Colors.RESET}")
return False
try:
result = subprocess.run(f'"{npm_path}" install',
cwd=frontend_dir, shell=True, capture_output=True, timeout=180)
if result.returncode == 0:
print(f"{Colors.GREEN}[SUCCESS] 前端依赖安装成功{Colors.RESET}")
return True
else:
print(f"{Colors.RED}[ERROR] 前端依赖安装失败{Colors.RESET}")
return False
except Exception as e:
print(f"{Colors.RED}[ERROR] 安装异常: {e}{Colors.RESET}")
return False
def step_start_backend(self):
"""启动后端服务"""
print(f"\n{Colors.BOLD}[STEP] 8/9 - 启动后端服务{Colors.RESET}")
backend_dir = self.project_dir / 'backend'
jar_files = list((backend_dir / 'target').glob('*.jar'))
if not jar_files:
print(f"{Colors.RED}[ERROR] 未找到JAR包{Colors.RESET}")
return False
backend_port = self.config.get('server', {}).get('backend_port', 8080)
jar_file = jar_files[0].name
bat_content = f'''@echo off
title 后端服务 - {backend_port}
cd /d "{backend_dir}"
echo 正在启动后端服务...
echo 端口: {backend_port}
echo.
java -jar target\\{jar_file} --server.port={backend_port}
echo.
echo 后端服务已停止
pause
'''
bat_path = backend_dir / 'start-backend.bat'
bat_path.write_text(bat_content, encoding='gbk')
os.system(f'start "" "{bat_path}"')
print(f"{Colors.GREEN}[SUCCESS] 后端服务已在新窗口启动{Colors.RESET}")
return True
def step_start_frontend(self):
"""启动前端服务"""
print(f"\n{Colors.BOLD}[STEP] 9/9 - 启动前端服务并打开浏览器{Colors.RESET}")
frontend_dir = self.project_dir / 'frontend'
npm_path = self.tool_paths.get('npm') or 'npm'
frontend_port = self.config.get('server', {}).get('frontend_port', 5173)
bat_content = f'''@echo off
title 前端服务 - {frontend_port}
cd /d "{frontend_dir}"
echo 正在启动前端服务...
echo 端口: {frontend_port}
echo.
"{npm_path}" run dev -- --port {frontend_port}
echo.
echo 前端服务已停止
pause
'''
bat_path = frontend_dir / 'start-frontend.bat'
bat_path.write_text(bat_content, encoding='gbk')
os.system(f'start "" "{bat_path}"')
print(f"{Colors.GREEN}[SUCCESS] 前端服务已在新窗口启动{Colors.RESET}")
time.sleep(5)
try:
import webbrowser
webbrowser.open(f'http://localhost:{frontend_port}/login', new=2)
print(f"{Colors.GREEN}[SUCCESS] 浏览器已打开{Colors.RESET}")
except:
print(f"{Colors.CYAN}[INFO] 请手动访问: http://localhost:{frontend_port}/login{Colors.RESET}")
return True
# ==================== 工具方法 ====================
def _java_type(self, type_str):
"""映射Java类型"""
return JAVA_TYPE_MAP.get(type_str, 'String')
def _sql_type(self, type_str):
"""映射SQL类型"""
return SQL_TYPE_MAP.get(type_str, 'VARCHAR(255)')
def _camel_to_snake(self, name):
"""驼峰命名转下划线命名"""
import re
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def main():
"""主入口"""
if len(sys.argv) < 2:
print("使用方法: python optimized-start-win.py <config.json>")
sys.exit(1)
config_file = Path(sys.argv[1])
if not config_file.exists():
print(f"配置文件不存在: {config_file}")
sys.exit(1)
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
starter = ProjectStarter(config)
success = starter.start()
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
FILE:scripts/optimized-start.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
优化版一键启动脚本
支持远程数据库执行、智能服务检测、自动浏览器打开
"""
import os
import sys
import time
import json
import subprocess
import threading
import urllib.request
import urllib.error
from pathlib import Path
from typing import Dict, List, Optional, Tuple
class Colors:
"""颜色输出"""
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BLUE = '\033[94m'
CYAN = '\033[96m'
RESET = '\033[0m'
BOLD = '\033[1m'
class DatabaseExecutor:
"""数据库执行器"""
def __init__(self, config: Dict):
self.config = config
self.connection = None
def test_connection(self) -> Tuple[bool, str]:
"""测试数据库连接"""
try:
import pymysql
conn = pymysql.connect(
host=self.config['host'],
port=int(self.config['port']),
user=self.config['user'],
password=self.config['password'],
connect_timeout=5
)
conn.close()
return True, "连接成功"
except ImportError:
return False, "请先安装pymysql: pip install pymysql"
except Exception as e:
return False, f"连接失败: {str(e)}"
def execute_sql_file(self, sql_file: Path) -> Tuple[bool, List[str]]:
"""执行SQL文件"""
if not sql_file.exists():
return False, ["SQL文件不存在"]
try:
import pymysql
conn = pymysql.connect(
host=self.config['host'],
port=int(self.config['port']),
user=self.config['user'],
password=self.config['password']
)
cursor = conn.cursor()
errors = []
# 读取SQL文件
sql_content = sql_file.read_text(encoding='utf-8')
# 分片执行(避免大文件内存问题)
sql_statements = self._split_sql(sql_content)
print(f"{Colors.CYAN}🗄️ 正在执行SQL脚本...{Colors.RESET}")
for i, statement in enumerate(sql_statements, 1):
statement = statement.strip()
if not statement or statement.startswith('--'):
continue
try:
cursor.execute(statement)
conn.commit()
print(f"{Colors.GREEN}✅ 执行成功 ({i}/{len(sql_statements)}): {statement[:50]}...{Colors.RESET}")
except Exception as e:
error_msg = f"SQL执行失败: {str(e)}\n语句: {statement[:100]}"
errors.append(error_msg)
print(f"{Colors.RED}❌ {error_msg}{Colors.RESET}")
conn.close()
return len(errors) == 0, errors
except Exception as e:
return False, [f"数据库连接失败: {str(e)}"]
def _split_sql(self, sql_content: str) -> List[str]:
"""分片SQL内容"""
# 按分号分割,但需要考虑字符串中的分号
statements = []
current = ""
in_string = False
string_char = None
for char in sql_content:
if char in ["'", '"', '`'] and (not current or current[-1] != '\\'):
if not in_string:
in_string = True
string_char = char
elif char == string_char:
in_string = False
string_char = None
elif char == ';' and not in_string:
if current.strip():
statements.append(current.strip())
current = ""
continue
current += char
if current.strip():
statements.append(current.strip())
return statements
class ServiceMonitor:
"""服务监控器"""
def __init__(self, timeout: int = 60, interval: int = 3):
self.timeout = timeout
self.interval = interval
self.results = {}
def check_service(self, name: str, url: str, max_attempts: int = 10) -> bool:
"""检查服务是否可用"""
print(f"{Colors.CYAN}🔍 正在检查{name}服务: {url}{Colors.RESET}")
for attempt in range(1, max_attempts + 1):
try:
response = urllib.request.urlopen(
url,
timeout=5,
context=urllib.request._create_unverified_context()
)
if response.status == 200:
print(f"{Colors.GREEN}✅ {name}服务已就绪 ({attempt}/{max_attempts}){Colors.RESET}")
self.results[name] = True
return True
except Exception as e:
if attempt < max_attempts:
print(f"{Colors.YELLOW}⏳ {name}服务启动中... ({attempt}/{max_attempts}){Colors.RESET}")
time.sleep(self.interval)
else:
print(f"{Colors.RED}❌ {name}服务检查失败: {str(e)}{Colors.RESET}")
self.results[name] = False
return False
def parallel_check(self, services: Dict[str, str]) -> Dict[str, bool]:
"""并行检查多个服务"""
threads = []
for name, url in services.items():
thread = threading.Thread(
target=self.check_service,
args=(name, url)
)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
return self.results
class BrowserOpener:
"""浏览器打开器"""
def __init__(self):
self.browser = self._detect_browser()
def _detect_browser(self) -> str:
"""检测默认浏览器"""
try:
import webbrowser
webbrowser.get()
return "default"
except:
return "chrome" # 默认使用Chrome
def open(self, url: str, delay: int = 2):
"""打开浏览器"""
print(f"{Colors.CYAN}🌐 即将打开浏览器: {url}{Colors.RESET}")
time.sleep(delay) # 等待前端完全启动
try:
import webbrowser
webbrowser.open(url, new=2) # new=2 在新标签页打开
print(f"{Colors.GREEN}✅ 浏览器已打开{Colors.RESET}")
except Exception as e:
print(f"{Colors.YELLOW}⚠️ 无法自动打开浏览器: {str(e)}{Colors.RESET}")
print(f"{Colors.CYAN}请手动访问: {url}{Colors.RESET}")
class ProjectStarter:
"""项目启动器"""
def __init__(self, config: Dict):
self.config = config
self.project_dir = Path(config['project_name'])
self.db_executor = DatabaseExecutor(config['database'])
self.service_monitor = ServiceMonitor()
self.browser_opener = BrowserOpener()
self.backend_port = config['server']['backend_port']
self.frontend_port = config['server']['frontend_port']
def start(self):
"""启动项目"""
print(f"{Colors.BOLD}{Colors.CYAN}🚀 开始启动项目: {self.config['project_name']}{Colors.RESET}")
steps = [
self.step_validate_environment,
self.step_check_ports,
self.step_test_database_connection,
self.step_execute_sql,
self.step_build_backend,
self.step_build_frontend,
self.step_start_redis,
self.step_start_backend,
self.step_start_frontend,
self.step_monitor_services,
self.step_open_browser,
]
for step in steps:
try:
result = step()
if not result:
print(f"{Colors.RED}❌ 步骤失败,停止启动{Colors.RESET}")
return False
except Exception as e:
print(f"{Colors.RED}❌ 步骤异常: {str(e)}{Colors.RESET}")
return False
print(f"\n{Colors.BOLD}{Colors.GREEN}🎉 项目启动成功!{Colors.RESET}")
self._print_summary()
return True
def step_validate_environment(self) -> bool:
"""验证环境"""
print(f"\n{Colors.BOLD}📋 步骤 1/11 - 验证环境{Colors.RESET}")
checks = [
("Java", "java -version", "java"),
("Maven", "mvn -v", "mvn"),
("Node.js", "node --version", "node"),
("NPM", "npm --version", "npm"),
]
optional_checks = [
("Docker", "docker --version", "docker"),
("MySQL Client", "mysql --version", "mysql"),
]
all_passed = True
for name, cmd, check_str in checks:
try:
result = subprocess.run(
cmd.split(),
capture_output=True,
text=True,
timeout=10
)
if check_str in result.stdout.lower() or check_str in result.stderr.lower():
print(f"{Colors.GREEN}✅ {name} 已安装{Colors.RESET}")
else:
print(f"{Colors.YELLOW}⚠️ {name} 版本检查失败{Colors.RESET}")
except:
print(f"{Colors.RED}❌ {name} 未找到或无法执行{Colors.RESET}")
all_passed = False
# 可选依赖检查(警告而非错误)
for name, cmd, check_str in optional_checks:
try:
result = subprocess.run(
cmd.split(),
capture_output=True,
text=True,
timeout=5
)
if check_str in result.stdout.lower() or check_str in result.stderr.lower():
print(f"{Colors.GREEN}ℹ️ {name} 已安装 (可选){Colors.RESET}")
else:
print(f"{Colors.YELLOW}ℹ️ {name} 未安装 (可选,不影响核心功能){Colors.RESET}")
except:
print(f"{Colors.YELLOW}ℹ️ {name} 未安装 (可选,不影响核心功能){Colors.RESET}")
return all_passed
def step_check_ports(self) -> bool:
"""检查端口"""
print(f"\n{Colors.BOLD}🔌 步骤 2/11 - 检查端口占用{Colors.RESET}")
def is_port_in_use(port: int) -> bool:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(('localhost', port))
return False
except:
return True
ports_to_check = [
("后端", self.backend_port),
("前端", self.frontend_port),
]
all_free = True
for name, port in ports_to_check:
if is_port_in_use(port):
print(f"{Colors.RED}❌ {name}端口 {port} 被占用{Colors.RESET}")
all_free = False
else:
print(f"{Colors.GREEN}✅ {name}端口 {port} 可用{Colors.RESET}")
return all_free
def step_test_database_connection(self) -> bool:
"""测试数据库连接"""
print(f"\n{Colors.BOLD}🗄️ 步骤 3/11 - 测试数据库连接{Colors.RESET}")
success, message = self.db_executor.test_connection()
if success:
print(f"{Colors.GREEN}✅ 数据库连接成功{Colors.RESET}")
else:
print(f"{Colors.RED}❌ {message}{Colors.RESET}")
return success
def step_execute_sql(self) -> bool:
"""执行SQL"""
print(f"\n{Colors.BOLD}📊 步骤 4/11 - 执行数据库初始化{Colors.RESET}")
sql_file = self.project_dir / "backend" / "src" / "main" / "resources" / "db" / "init.sql"
success, errors = self.db_executor.execute_sql_file(sql_file)
if success:
print(f"{Colors.GREEN}✅ 数据库初始化完成{Colors.RESET}")
else:
print(f"{Colors.RED}❌ 数据库初始化失败{Colors.RESET}")
for error in errors:
print(f"{Colors.RED} - {error}{Colors.RESET}")
return success
def step_build_backend(self) -> bool:
"""构建后端"""
print(f"\n{Colors.BOLD}🔧 步骤 5/11 - 构建后端项目{Colors.RESET}")
backend_dir = self.project_dir / "backend"
# 检查是否已经构建过
target_dir = backend_dir / "target"
if target_dir.exists() and any(target_dir.glob("*.jar")):
print(f"{Colors.YELLOW}⚠️ 检测到已构建的JAR包,跳过构建{Colors.RESET}")
return True
try:
# Maven构建
cmd = ["mvn", "clean", "package", "-DskipTests", "-q"]
result = subprocess.run(
cmd,
cwd=backend_dir,
capture_output=True,
text=True,
timeout=300 # 5分钟超时
)
if result.returncode == 0:
print(f"{Colors.GREEN}✅ 后端构建成功{Colors.RESET}")
return True
else:
print(f"{Colors.RED}❌ 后端构建失败{Colors.RESET}")
print(result.stderr)
return False
except subprocess.TimeoutExpired:
print(f"{Colors.RED}❌ 后端构建超时{Colors.RESET}")
return False
except Exception as e:
print(f"{Colors.RED}❌ 后端构建异常: {str(e)}{Colors.RESET}")
return False
def step_build_frontend(self) -> bool:
"""构建前端"""
print(f"\n{Colors.BOLD}📦 步骤 6/11 - 安装前端依赖{Colors.RESET}")
frontend_dir = self.project_dir / "frontend"
# 检查node_modules
node_modules = frontend_dir / "node_modules"
if node_modules.exists():
print(f"{Colors.YELLOW}⚠️ 检测到node_modules,跳过安装{Colors.RESET}")
return True
try:
# 使用国内镜像源
cmd = ["npm", "install", "--registry=https://registry.npmmirror.com"]
result = subprocess.run(
cmd,
cwd=frontend_dir,
capture_output=True,
text=True,
timeout=180 # 3分钟超时
)
if result.returncode == 0:
print(f"{Colors.GREEN}✅ 前端依赖安装成功{Colors.RESET}")
return True
else:
print(f"{Colors.RED}❌ 前端依赖安装失败{Colors.RESET}")
print(result.stderr)
return False
except subprocess.TimeoutExpired:
print(f"{Colors.RED}❌ 前端依赖安装超时{Colors.RESET}")
return False
except Exception as e:
print(f"{Colors.RED}❌ 前端依赖安装异常: {str(e)}{Colors.RESET}")
return False
def step_start_redis(self) -> bool:
"""Redis外部服务检查"""
print(f"\n{Colors.BOLD}🔴 步骤 7/11 - Redis外部服务检查{Colors.RESET}")
redis_config = self.config.get('redis', {})
if not redis_config.get('enable', True):
print(f"{Colors.YELLOW}⚠️ Redis已禁用,跳过检查{Colors.RESET}")
return True
host = redis_config.get('host', 'localhost')
port = redis_config.get('port', 6379)
print(f"{Colors.CYAN}ℹ️ Redis配置: {host}:{port} (外部服务){Colors.RESET}")
print(f"{Colors.GREEN}✅ Redis外部服务配置已确认{Colors.RESET}")
print(f"{Colors.YELLOW}💡 提示: 请确保外部Redis服务已启动{Colors.RESET}")
return True
def step_start_backend(self) -> bool:
"""启动后端"""
print(f"\n{Colors.BOLD}🚀 步骤 8/11 - 启动后端服务{Colors.RESET}")
backend_dir = self.project_dir / "backend"
jar_files = list(backend_dir.glob("target/*.jar"))
if not jar_files:
print(f"{Colors.RED}❌ 未找到JAR包{Colors.RESET}")
return False
jar_file = jar_files[0]
try:
# 启动后端服务
cmd = f"start /B java -jar {jar_file} --server.port={self.backend_port}"
subprocess.run(cmd, shell=True, cwd=backend_dir)
print(f"{Colors.GREEN}✅ 后端服务已启动 (端口: {self.backend_port}){Colors.RESET}")
return True
except Exception as e:
print(f"{Colors.RED}❌ 后端启动失败: {str(e)}{Colors.RESET}")
return False
def step_start_frontend(self) -> bool:
"""启动前端"""
print(f"\n{Colors.BOLD}🎨 步骤 9/11 - 启动前端服务{Colors.RESET}")
frontend_dir = self.project_dir / "frontend"
try:
# 启动前端开发服务器
cmd = f"start /B npm run dev -- --port {self.frontend_port}"
subprocess.run(cmd, shell=True, cwd=frontend_dir)
print(f"{Colors.GREEN}✅ 前端服务已启动 (端口: {self.frontend_port}){Colors.RESET}")
return True
except Exception as e:
print(f"{Colors.RED}❌ 前端启动失败: {str(e)}{Colors.RESET}")
return False
def step_monitor_services(self) -> bool:
"""监控服务"""
print(f"\n{Colors.BOLD}👀 步骤 10/11 - 监控服务状态{Colors.RESET}")
time.sleep(5) # 等待服务初始化
services = {
"后端": f"http://localhost:{self.backend_port}/api/health",
"前端": f"http://localhost:{self.frontend_port}",
}
results = self.service_monitor.parallel_check(services)
all_ready = all(results.values())
if all_ready:
print(f"{Colors.GREEN}✅ 所有服务已就绪{Colors.RESET}")
else:
print(f"{Colors.RED}❌ 部分服务未就绪{Colors.RESET}")
for name, ready in results.items():
status = "✅" if ready else "❌"
print(f" {status} {name}: {'已就绪' if ready else '未就绪'}")
return all_ready
def step_open_browser(self) -> bool:
"""打开浏览器"""
print(f"\n{Colors.BOLD}🌐 步骤 11/11 - 打开浏览器{Colors.RESET}")
login_url = f"http://localhost:{self.frontend_port}/login"
self.browser_opener.open(login_url)
return True
def _print_summary(self):
"""打印启动摘要"""
print(f"\n{Colors.BOLD}{Colors.CYAN}📊 启动摘要{Colors.RESET}")
print(f"{Colors.CYAN}📍 后端API: http://localhost:{self.backend_port}/api{Colors.RESET}")
print(f"{Colors.CYAN}📍 前端页面: http://localhost:{self.frontend_port}{Colors.RESET}")
print(f"{Colors.CYAN}📍 Swagger文档: http://localhost:{self.backend_port}/api/swagger-ui{Colors.RESET}")
print(f"{Colors.CYAN}🔑 默认账号: admin / 123456{Colors.RESET}")
print(f"{Colors.CYAN}📁 项目目录: {self.project_dir.absolute()}{Colors.RESET}")
print(f"\n{Colors.YELLOW}💡 提示: 使用 Ctrl+C 停止所有服务{Colors.RESET}")
def main():
"""主函数"""
if len(sys.argv) < 2:
print("使用方法: python optimized-start.py <config.json>")
sys.exit(1)
# 加载配置文件
config_file = Path(sys.argv[1])
if not config_file.exists():
print(f"配置文件不存在: {config_file}")
sys.exit(1)
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 启动项目
starter = ProjectStarter(config)
success = starter.start()
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()Generate full-stack Java Spring Boot and Vue projects from requirements with mandatory, guided tech stack selection including database, API, auth, and UI.
---
name: project-generator
description: Automatically transform requirement documents or natural language descriptions into complete full-stack projects (Java backend + Vue frontend) with MANDATORY interactive tech stack selection. Supports database schema design, REST API generation, CRUD operations, user authentication, and project scaffolding. ⚠️ TECH STACK SELECTION IS MANDATORY - Users MUST explicitly select or confirm each option, cannot skip by pressing Enter. Use when: (1) Converting PRD/requirements into working code, (2) Rapid prototyping of web applications, (3) Generating boilerplate projects from descriptions, (4) Bootstrapping CRUD applications, (5) Creating MVP projects from text specifications.
---
# Project Generator Skill
Transform requirement documents or natural language descriptions into complete, production-ready full-stack applications with **MANDATORY** customizable tech stack selection.
⚠️ **IMPORTANT: Tech stack selection is MANDATORY. Users cannot skip this step by pressing Enter. Each option must be explicitly selected or confirmed.**
## Capabilities
This skill generates:
- **Backend**: Java Spring Boot with proper layering (Controller, Service, Repository, Entity)
- **Frontend**: Vue 3 + TypeScript + UI component library
- **Database**: Entities with relationships, migration scripts
- **API**: RESTful endpoints with OpenAPI/Swagger docs
- **Authentication**: JWT-based auth (optional)
- **Documentation**: README, API docs, setup instructions
## Interactive Tech Stack Selection
Before generating the project, the skill will guide you through selecting your preferred technology stack.
### Selection Modes
#### Mode 1: Quick Selection (Mandatory)
⚠️ **此模式为强制模式,用户必须输入选择,不能回车跳过。**
Use numbered options for rapid selection:
```
【后端】
1. Java: [1]8 [2]11 [3]17 [4]21
2. SpringBoot:[1]2.7 [2]3.0 [3]3.2
3. Build: [1]Maven [2]Gradle
4. Database: [1]MySQL5.7 [2]MySQL8.0 [3]PostgreSQL [4]H2
5. ORM: [1]JPA [2]MyBatis [3]MyBatis-Plus
...
【前端】
1. Vue: [1]2 [2]3
2. UI: [1]ElementUI(Vue2) [2]ElementPlus(Vue3) [3]AntDV [4]NaiveUI
3. State: [1]Pinia(Vue3) [2]Vuex
...
⚠️ 必须输入完整选择,格式: 1,1,1,1,3,1,2,2,1
```
```
#### Mode 2: Free-form Text Input
Directly specify technology and version in natural language:
**Examples:**
> "Java 8, Spring Boot 2.7, MySQL 5.7, MyBatis-Plus, Redis"
> "JDK17 + SpringBoot 3.2 + PostgreSQL 15 + JPA + RabbitMQ"
> "Vue3 + TypeScript + Element Plus + Pinia + Tailwind"
> "React 18 + JavaScript + Ant Design + Zustand"
⚠️ **注意:** 如果自由格式输入未包含所有必要选项,系统仍会**强制提示**用户补充选择剩余选项。
**Supported Input Formats:**
- Technology name + version: `Java 8`, `Vue 3`, `MySQL 5.7`
- Short names: `JDK8`, `SB2.7`, `Vue2`, `React18`, `TS`
- With connectors: `+`, `,`, `/`, `and`
- Multiple lines or bullet points
### Selection Flow (Mandatory)
⚠️ **技术栈选择是强制性的,不能跳过。**
```
1. Requirement Description (REQUIRED)
├─ User describes project requirements
├─ Skill analyzes and understands the needs
└─ Summarizes core features
↓
2. Present Tech Stack Options (MANDATORY)
├─ Display all options with recommendations
├─ Provide suggestions based on requirements
└─ Wait for user input
↓
3. User Selects Preferences (REQUIRED)
├─ User enters selection numbers
├─ Or confirms recommendations
└─ Re-prompt if input is empty
↓
4. Generate Project with Selected Stack
```
### Phase 1: Requirement Description
⚠️ **用户必须首先描述项目需求。**
**Acceptable Inputs:**
- 自然语言描述: "创建一个电商订单管理系统"
- 功能列表: "用户管理、商品管理、订单管理"
- PRD文档: 上传需求文档
- 简要描述: "一个简单的博客系统"
**Requirements for Description:**
- 项目名称(可选)
- 核心功能模块
- 业务场景描述
- 技术偏好(可选,如已知)
**Example:**
> "创建一个用户管理系统,包含用户注册登录、用户信息管理、角色权限分配功能。使用Java开发,前端用Vue。"
### Backend Options
#### 1. Java Version
| # | Version | Description |
|---|---------|-------------|
| 1 | Java 8 | Legacy support, widely used |
| 2 | Java 11 | LTS version |
| 3 | Java 17 | Default LTS version, stable |
| 4 | Java 21 | Latest LTS, virtual threads |
#### 2. Spring Boot Version
| # | Version | Description |
|---|---------|-------------|
| 1 | 2.7.x | Compatible with Java 8/11 |
| 2 | 3.0.x | Requires Java 17+ |
| 3 | 3.2.x | Default, latest features |
#### 3. Build Tool
| # | Tool | Description |
|---|------|-------------|
| 1 | Maven | Default, widely used |
| 2 | Gradle | Faster builds, flexible |
#### 4. Database
| # | Database | Description |
|---|----------|-------------|
| 1 | MySQL 5.7 | Legacy stable version |
| 2 | MySQL 8.0 | Default, latest features |
| 3 | PostgreSQL | Advanced features, robust |
| 4 | H2 | In-memory, for development |
#### 5. Data Access
| # | Framework | Description |
|---|-----------|-------------|
| 1 | JPA/Hibernate | Default, rapid development |
| 2 | MyBatis | SQL control, complex queries |
| 3 | MyBatis-Plus | Enhanced MyBatis, CRUD shortcuts |
#### 5. Additional Features (Multi-select)
| # | Feature | Description |
|---|---------|-------------|
| 1 | JWT Authentication | Token-based security |
| 2 | Redis Cache | Performance caching |
| 3 | Audit Logging | Track data changes |
| 4 | Soft Delete | Logical deletion |
| 5 | Multi-tenant | Multiple organizations |
| 6 | File Upload | File storage support |
| 7 | Excel Export | Data export functionality |
### Frontend Options
#### 1. Vue Version
| # | Version | Description |
|---|---------|-------------|
| 1 | Vue 2 | Legacy, stable |
| 2 | Vue 3 | Default, latest features, Composition API |
#### 2. UI Component Library
| # | Library | Description |
|---|---------|-------------|
| 1 | Element UI | For Vue 2, mature |
| 2 | Element Plus | For Vue 3, default, rich components |
| 3 | Ant Design Vue | Enterprise UI design |
| 4 | Naive UI | Modern, TypeScript friendly |
#### 3. State Management
| # | Tool | Description |
|---|------|-------------|
| 1 | Pinia | For Vue 3, default |
| 2 | Vuex | For Vue 2/Vue 3, legacy style |
#### 4. Additional Features (Multi-select)
| # | Feature | Description |
|---|---------|-------------|
| 1 | Dark Mode | Theme switching |
| 2 | i18n | Internationalization |
| 3 | PWA | Progressive Web App |
| 4 | Responsive Design | Mobile adaptation |
## Input Formats
Accepted inputs:
- Natural language description (e.g., "创建一个用户管理系统")
- PRD (Product Requirements Document)
- Markdown requirements
- Feature list
- User stories
⚠️ **IMPORTANT:** After requirement input, the skill WILL ALWAYS prompt for tech stack selection. This step is MANDATORY and cannot be skipped.
## Workflow
### Phase 1: Requirement Description (REQUIRED)
⚠️ **第一步:用户必须首先描述项目需求。**
**输入要求:**
- 项目名称(可选)
- 核心功能模块描述
- 业务场景说明
- 技术偏好(如有)
**示例:**
```
👤 用户: 创建一个用户管理系统
🤖 系统: 收到需求!分析结果:
📋 项目类型: 用户管理系统
🔧 核心功能: 用户注册/登录、用户信息管理、角色权限分配
💡 推荐技术栈: Java + Spring Boot + Vue3
接下来请配置具体的技术栈...
```
### Phase 2: Tech Stack Selection (MANDATORY)
⚠️ **第二步:根据需求选择技术栈(强制性,不能跳过)**
根据需求分析结果,呈现技术栈选项:
Example interaction:
```
🛠️ 请根据您的需求选择技术栈:
【后端配置 - 请选择】
1. Java 版本: [1]8 [2]11 [3]17 [4]21 → 请输入: ___
2. Spring Boot: [1]2.7 [2]3.0 [3]3.2 → 请输入: ___
3. 构建工具: [1]Maven [2]Gradle → 请输入: ___
4. 数据库: [1]MySQL5.7 [2]MySQL8.0 [3]PostgreSQL [4]H2 → 请输入: ___
5. 数据访问: [1]JPA [2]MyBatis [3]MyBatis-Plus → 请输入: ___
6. 附加功能 (多选): [1]JWT [2]Redis [3]审计日志 [4]软删除 → 请输入: ___
【前端配置 - 请选择】
1. Vue 版本: [1]Vue2 [2]Vue3 → 请输入: ___
2. UI 组件库: [1]Element UI(Vue2) [2]Element Plus(Vue3) [3]Ant Design Vue [4]Naive UI → 请输入: ___
3. 状态管理: [1]Pinia(Vue3) [2]Vuex → 请输入: ___
4. 附加功能 (多选): [1]暗黑模式 [2]国际化 [3]PWA → 请输入: ___
⚠️ 请完整输入所有选项 (如: 1,1,1,1,3,1,2,2,1,12)
```
3. 构建工具: [1]Maven [2]Gradle → 请输入: ___
4. 数据库: [1]MySQL5.7 [2]MySQL8.0 [3]PostgreSQL [4]H2 → 请输入: ___
5. 数据访问: [1]JPA [2]MyBatis [3]MyBatis-Plus → 请输入: ___
6. 附加功能 (多选,如: 12): [1]JWT [2]Redis [3]审计日志 [4]软删除 → 请输入: ___
【前端配置 - 请选择】
1. Vue 版本: [1]Vue2 [2]Vue3 → 请输入: ___
2. UI 组件库: [1]Element UI(Vue2) [2]Element Plus(Vue3) [3]Ant Design Vue [4]Naive UI → 请输入: ___
3. 状态管理: [1]Pinia(Vue3) [2]Vuex → 请输入: ___
4. 附加功能 (多选,如: 12): [1]暗黑模式 [2]国际化 [3]PWA → 请输入: ___
⚠️ 请完整输入所有选项 (如: 1,1,1,1,3,1,2,2,1,12)
```
### Phase 3: Database Design
1. Design entity-relationship model based on requirements
2. Generate appropriate DDL/scripts based on selected database
3. Create migration scripts (Flyway for JPA, or custom for MyBatis)
### Phase 4: Backend Generation
1. Generate build configuration (pom.xml or build.gradle) with selected stack
2. Create application.yml with correct database settings
3. Generate entity classes (JPA annotations or MyBatis mappers)
4. Create DTOs and mappers
5. Implement service layer with business logic
6. Build REST controllers
7. Add selected features (JWT, Redis, audit, etc.)
### Phase 5: Frontend Generation
1. Generate package.json with selected UI library
2. Configure Vite and project setup
3. Generate API client from backend
4. Create Vue components with selected UI library
5. Build routing configuration
6. Implement state management
7. Add selected features (dark mode, i18n, etc.)
### Phase 6: Project Setup
1. Generate Docker configurations
2. Create docker-compose.yml with selected database
3. Write CI/CD templates
4. Create setup documentation
## Project Structure Output
```
generated-project/
├── backend/ # Spring Boot application
│ ├── src/main/java/
│ │ └── com/example/
│ │ ├── config/ # Configurations (CORS, Security, etc.)
│ │ ├── controller/ # REST controllers
│ │ ├── service/ # Business logic
│ │ ├── repository/ # Data access (JPA/MyBatis/MyBatis-Plus)
│ │ ├── entity/ # Entity classes
│ │ ├── dto/ # Data transfer objects
│ │ ├── mapper/ # Entity-DTO mappers
│ │ ├── security/ # JWT/auth (if selected)
│ │ └── aspect/ # Audit logging (if selected)
│ ├── src/main/resources/
│ │ ├── db/migration/ # Flyway scripts
│ │ ├── mapper/ # MyBatis XML (if selected)
│ │ └── application.yml # Configured for selected database
│ └── pom.xml / build.gradle # Based on selected build tool
├── frontend/ # Vue application
│ ├── src/
│ │ ├── api/ # API client
│ │ ├── components/ # Reusable components
│ │ ├── views/ # Page views
│ │ ├── stores/ # Pinia/Vuex stores
│ │ ├── router/ # Vue Router
│ │ ├── locales/ # i18n files (if selected)
│ │ ├── types/ # TypeScript types (Vue 3)
│ │ └── App.vue # Root component
│ ├── package.json # Vue 2 or Vue 3 with selected UI library
│ └── vite.config.ts / vue.config.js # Vite (Vue 3) or Vue CLI (Vue 2)
├── docker-compose.yml # With selected database (MySQL 5.7/8.0/PostgreSQL)
└── README.md # Setup instructions for selected stack
```
## Usage Examples
### Example 1: Standard Flow - Describe then Select
**Step 1: 描述需求**
```
👤 用户: 创建一个用户管理系统
🤖 系统: 收到需求!分析结果:
📋 项目类型: 用户管理系统
🔧 核心功能: 用户注册/登录、用户信息管理、角色权限控制
💡 推荐技术栈: Java + Spring Boot + Vue3
接下来请配置具体的技术栈...
```
**Step 2: 选择技术栈(强制)**
```
👤 用户: 1,1,1,1,3,1,2,2,1,1
(Java8+SpringBoot2.7+MySQL5.7+MyBatis-Plus+JWT / Vue3+ElementPlus+Pinia+暗黑模式)
🤖 系统: 配置确认完成,正在生成项目...
```
**Process:**
1. 用户提供需求描述(**必须先描述**)
2. 系统分析需求并推荐技术栈
3. 用户必须选择技术栈(**强制,不能跳过**)
4. 根据需求+技术栈生成项目
### Example 2: Custom Tech Stack
**Input:**
> "创建一个电商管理系统,Java 21,Gradle,PostgreSQL"
**Process:**
1. Skill detects preferences from description
2. Presents remaining options
3. Generates with specified stack
### Example 3: Upload PRD
**Input:**
> "读取这个需求文档并生成项目"
**Process:**
1. Parse uploaded PRD
2. Present tech stack options
3. Generate based on requirements + selected stack
### Example 4: Free-form Tech Stack Input (NEW)
**Input:**
> "用 Java 8 + Spring Boot 2.7 + MySQL 5.7 + MyBatis-Plus + Redis"
**Or:**
> "后端:JDK17, SpringBoot3.2, PostgreSQL, JPA, Kafka\n前端:Vue3, TS, ElementPlus, Pinia, Tailwind"
**Or:**
> "Vue2 + Element UI + JavaScript + Vuex,后端用 Java11 + SpringBoot2.7 + MyBatis"
**Process:**
1. Parse free-form text to extract technologies and versions
2. Map to available options or use custom versions
3. Confirm parsed selection
4. Generate with specified stack
**Supported Keywords:**
- **Java**: `Java 8`, `Java 11`, `JDK11`, `Java17`, `JDK 21`
- **Spring Boot**: `SpringBoot 2.7`, `SB 2.7`, `SB 3.2`, `Spring Boot 3.x`
- **Database**: `MySQL 5.7`, `MySQL8`, `PostgreSQL 15`, `Oracle`, `SQL Server`
- **ORM**: `JPA`, `Hibernate`, `MyBatis`, `MyBatis-Plus`, `MP`
- **Cache**: `Redis`, `Caffeine`, `Ehcache`
- **MQ**: `RabbitMQ`, `Kafka`, `RocketMQ`
- **Frontend**: `Vue2`, `Vue 3`, `React 18`, `Angular`
- **UI**: `Element UI`, `Element Plus`, `Ant Design Vue`, `AntDV`, `Naive UI`, `Vuetify`
- **State**: `Pinia`, `Vuex`, `Redux`, `Zustand`
- **CSS**: `Tailwind`, `SCSS`, `Less`, `CSS`
- **Language**: `TypeScript`, `TS`, `JavaScript`, `JS`
## Generated Configurations by Stack
### Backend Configurations
#### MySQL + JPA (application-mysql.txt → rename to application.yml)
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/projectName?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
flyway:
enabled: true
locations: classpath:db/migration
```
#### PostgreSQL + JPA (application-postgres.txt → rename to application.yml)
```yaml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/projectName
username: postgres
password: password
driver-class-name: org.postgresql.Driver
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
```
#### MyBatis Configuration
```yaml
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
```
### Frontend Configurations
#### Element Plus (package-element.txt → rename to package.json)
```json
{
"dependencies": {
"element-plus": "^2.5.0",
"@element-plus/icons-vue": "^2.3.0"
}
}
```
#### Ant Design Vue (package-antd.txt → rename to package.json)
```json
{
"dependencies": {
"ant-design-vue": "^4.0.0",
"@ant-design/icons-vue": "^7.0.0"
}
}
```
### Build Tool Configurations
#### Maven (pom.txt → rename to pom.xml) - Java 17 vs 21
```xml
<!-- Java 17 -->
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<!-- Java 21 -->
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
```
#### Gradle (build.txt → rename to build.gradle) - Java 17 vs 21
```groovy
// Java 17
sourceCompatibility = '17'
targetCompatibility = '17'
// Java 21
sourceCompatibility = '21'
targetCompatibility = '21'
```
## Assets Reference
### Project Templates
- `assets/templates/spring-boot/` - Spring Boot templates
- `pom-java8.txt` / `pom-java11.txt` / `pom-java17.txt` / `pom-java21.txt` (rename to pom.xml when using)
- `build-java8.txt` / `build-java11.txt` / `build-java17.txt` / `build-java21.txt` (rename to build.gradle when using)
- `application-mysql57.txt` / `application-mysql8.txt` / `application-postgres.txt` (rename to .yml when using)
- `mybatis-plus-config.txt` (MyBatis-Plus configuration)
- `application-h2.txt`
- `assets/templates/vue-vite/` - Vue 3 + Vite templates
- `package-element.txt` (rename to package.json when using) - Element Plus
- `package-antd.txt` (rename to package.json when using) - Ant Design Vue
- `package-naive.txt` (rename to package.json when using) - Naive UI
- `assets/templates/vue-cli/` - Vue 2 + Vue CLI templates
- `package-element-ui-vue2.txt` (rename to package.json when using) - Element UI
- `assets/templates/docker/` - Docker configurations
- `docker-compose-mysql57.txt` (rename to .yml when using) - MySQL 5.7
- `docker-compose-mysql8.txt` (rename to .yml when using) - MySQL 8.0
- `docker-compose-postgres.txt` (rename to .yml when using)
### Code Generators
- `scripts/generate-backend.py` - Generate Java backend with stack options
- `scripts/generate-frontend.py` - Generate Vue frontend with UI library selection
- `scripts/generate-database.py` - Generate schema for selected database
### Component Library Presets
- `assets/ui-presets/element-plus.json` - Element Plus component templates
- `assets/ui-presets/ant-design.json` - Ant Design Vue component templates
- `assets/ui-presets/naive-ui.json` - Naive UI component templates
## Best Practices
1. **Review tech stack choices** - Consider team expertise and project requirements
2. **Start with defaults** - Use proven combinations for quick prototyping
3. **Database choice matters** - MySQL for simple apps, PostgreSQL for complex queries
4. **ORM selection** - JPA for rapid dev, MyBatis for SQL control
5. **Security** - Enable JWT for production applications
6. **Test locally** - Use docker-compose for immediate testing
## Troubleshooting
### Database Connection Issues
- Verify selected database is running
- Check credentials in application.yml
- For H2: ensure `spring.datasource.url` uses correct mem/file path
### Build Failures
- Ensure correct Java version is installed (17 or 21)
- For Gradle: check gradle wrapper version compatibility
- Verify Maven/Gradle can access dependencies
### Frontend UI Issues
- Ensure selected UI library version is compatible with Vue 3
- Check if component imports match selected library
- Verify CSS preprocessor settings if using custom themes
## References
- [Spring Boot Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/)
- [Vue 3 Guide](https://vuejs.org/guide/)
- [Element Plus](https://element-plus.org/)
- [Ant Design Vue](https://antdv.com/)
- [Naive UI](https://www.naiveui.com/)
- [MyBatis Spring Boot](https://mybatis.org/spring-boot-starter/)
- [Flyway Documentation](https://documentation.red-gate.com/flyway/)
FILE:scripts/generate-backend.py
#!/usr/bin/env python3
"""
Backend Code Generator for Spring Boot with Tech Stack Selection
Generates Java entity, repository, service, controller based on specification and tech stack
"""
import json
import os
from pathlib import Path
from datetime import datetime
TEMPLATE_DIR = Path(__file__).parent.parent / "assets" / "templates"
def generate_pom_xml(project_name, tech_stack):
"""Generate Maven pom.xml with selected tech stack"""
java_version = tech_stack.get('javaVersion', '17')
database = tech_stack.get('database', 'mysql')
orm = tech_stack.get('orm', 'jpa')
features = tech_stack.get('features', [])
# Database dependencies
db_deps = {
'mysql': ''' <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>''',
'postgresql': ''' <dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>''',
'h2': ''' <dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>'''
}
# ORM dependencies
orm_deps = ''
if orm == 'jpa':
orm_deps = ''' <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>'''
if database != 'h2':
orm_deps += '''
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>'''
else: # mybatis
orm_deps = ''' <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>'''
# Feature dependencies
feature_deps = []
if 'jwt' in features:
feature_deps.append(''' <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>''')
if 'redis' in features:
feature_deps.append(''' <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>''')
if 'audit' in features:
feature_deps.append(''' <dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
</dependency>''')
feature_deps_str = '\n'.join(feature_deps)
pom_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>{project_name}</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>{java_version}</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
{orm_deps}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
{feature_deps_str}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Database -->
{db_deps.get(database, db_deps['mysql'])}
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
'''
return pom_content
def generate_application_yml(project_name, tech_stack):
"""Generate application.yml with selected database"""
database = tech_stack.get('database', 'mysql')
template_files = {
'mysql': 'application-mysql.yml',
'postgresql': 'application-postgres.yml'
}
template_file = template_files.get(database, 'application-mysql.yml')
template_path = TEMPLATE_DIR / template_file
if template_path.exists():
content = template_path.read_text(encoding='utf-8')
return content.replace('{{projectName}}', project_name)
else:
# Fallback to MySQL config
return f'''spring:
application:
name: {project_name}
datasource:
url: jdbc:mysql://localhost:3306/{project_name}?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
server:
port: 8080
'''
def generate_entity(entity_spec, tech_stack):
"""Generate JPA Entity class with optional audit/soft delete"""
class_name = entity_spec['name']
fields = entity_spec.get('fields', [])
features = tech_stack.get('features', [])
use_audit = 'audit' in features
use_soft_delete = 'softDelete' in features
imports = [
'import jakarta.persistence.*;',
'import java.time.LocalDateTime;'
]
annotations = []
if use_audit:
imports.append('import org.springframework.data.annotation.CreatedBy;')
imports.append('import org.springframework.data.annotation.LastModifiedBy;')
imports.append('import org.springframework.data.jpa.domain.support.AuditingEntityListener;')
annotations.append('@EntityListeners(AuditingEntityListener.class)')
field_lines = [' @Id', ' @GeneratedValue(strategy = GenerationType.IDENTITY)', ' private Long id;', '']
for field in fields:
field_type = field['type']
field_name = field['name']
nullable = 'nullable=false' if field.get('required') else 'nullable=true'
if field_type == 'String':
field_lines.append(f' @Column({nullable})')
field_lines.append(f' private String {field_name};')
elif field_type == 'Long':
field_lines.append(f' @Column({nullable})')
field_lines.append(f' private Long {field_name};')
elif field_type == 'Integer':
field_lines.append(f' @Column({nullable})')
field_lines.append(f' private Integer {field_name};')
elif field_type == 'BigDecimal':
imports.append('import java.math.BigDecimal;')
field_lines.append(f' @Column(precision=19, scale=4, {nullable})')
field_lines.append(f' private BigDecimal {field_name};')
elif field_type == 'LocalDateTime':
field_lines.append(f' @Column({nullable})')
field_lines.append(f' private LocalDateTime {field_name};')
elif field_type == 'Boolean':
field_lines.append(f' @Column({nullable})')
field_lines.append(f' private Boolean {field_name};')
elif field_type == 'Text':
field_lines.append(f' @Column(columnDefinition="TEXT", {nullable})')
field_lines.append(f' private String {field_name};')
field_lines.append('')
# Audit fields
field_lines.append(' @Column(nullable=false, updatable=false)')
if use_audit:
field_lines.append(' @CreatedBy')
field_lines.append(' private Long createdBy;')
field_lines.append('')
field_lines.append(' @Column(nullable=false, updatable=false)')
field_lines.append(' private LocalDateTime createdAt;')
field_lines.append('')
if use_audit:
field_lines.append(' @LastModifiedBy')
field_lines.append(' private Long updatedBy;')
field_lines.append('')
field_lines.append(' @Column(nullable=false)')
field_lines.append(' private LocalDateTime updatedAt;')
if use_soft_delete:
imports.append('import org.hibernate.annotations.SQLDelete;')
imports.append('import org.hibernate.annotations.Where;')
annotations.insert(0, '@SQLDelete(sql = "UPDATE ' + class_name.lower() + 's SET deleted = true WHERE id = ?")')
annotations.insert(0, '@Where(clause = "deleted = false")')
field_lines.append('')
field_lines.append(' @Column(nullable=false)')
field_lines.append(' private Boolean deleted = false;')
field_lines.append('')
field_lines.append(' @PrePersist')
field_lines.append(' protected void onCreate() {')
field_lines.append(' createdAt = LocalDateTime.now();')
field_lines.append(' updatedAt = LocalDateTime.now();')
if use_soft_delete:
field_lines.append(' deleted = false;')
field_lines.append(' }')
field_lines.append('')
field_lines.append(' @PreUpdate')
field_lines.append(' protected void onUpdate() {')
field_lines.append(' updatedAt = LocalDateTime.now();')
field_lines.append(' }')
annotations_str = '\n'.join([f' {a}' for a in annotations]) + '\n' if annotations else ''
template = f'''package com.example.entity;
{chr(10).join(sorted(set(imports)))}
@Entity
@Table(name = "{class_name.lower()}s")
{annotations_str}public class {class_name} {{
{chr(10).join(field_lines)}
// Getters and Setters
public Long getId() {{ return id; }}
public void setId(Long id) {{ this.id = id; }}
'''
# Add getters/setters for each field
getter_setters = []
for field in fields:
field_type = field['type']
field_name = field['name']
capitalized = field_name[0].upper() + field_name[1:]
getter_setters.append(f' public {field_type} get{capitalized}() {{ return {field_name}; }}')
getter_setters.append(f' public void set{capitalized}({field_type} {field_name}) {{ this.{field_name} = {field_name}; }}')
if use_audit:
getter_setters.append(' public Long getCreatedBy() { return createdBy; }')
getter_setters.append(' public void setCreatedBy(Long createdBy) { this.createdBy = createdBy; }')
getter_setters.append(' public Long getUpdatedBy() { return updatedBy; }')
getter_setters.append(' public void setUpdatedBy(Long updatedBy) { this.updatedBy = updatedBy; }')
getter_setters.append(' public LocalDateTime getCreatedAt() { return createdAt; }')
getter_setters.append(' public LocalDateTime getUpdatedAt() { return updatedAt; }')
if use_soft_delete:
getter_setters.append(' public Boolean getDeleted() { return deleted; }')
getter_setters.append(' public void setDeleted(Boolean deleted) { this.deleted = deleted; }')
template += '\n' + '\n'.join(getter_setters)
template += '\n}'
return template
def main():
"""Test with sample tech stack"""
tech_stack = {
'javaVersion': '17',
'buildTool': 'maven',
'database': 'mysql',
'orm': 'jpa',
'features': ['jwt', 'audit', 'softDelete']
}
print(generate_pom_xml('demo-app', tech_stack))
if __name__ == '__main__':
main()
FILE:scripts/generate-frontend.py
#!/usr/bin/env python3
"""
Frontend Code Generator for Vue 3 + TypeScript with Tech Stack Selection
Generates Vue components, API client, and stores based on specification and UI library choice
"""
from pathlib import Path
def generate_package_json(project_name, tech_stack):
"""Generate package.json with selected UI library"""
ui_lib = tech_stack.get('uiLibrary', 'element-plus')
features = tech_stack.get('features', [])
base_json = {
"name": f"{project_name}-frontend",
"version": "1.0.0",
"private": True,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc \u0026\u0026 vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.ts,.tsx --fix",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"axios": "^1.6.0",
"dayjs": "^1.11.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.0",
"prettier": "^3.2.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}
# Add UI library
if ui_lib == 'element-plus':
base_json["dependencies"]["element-plus"] = "^2.5.0"
base_json["dependencies"]["@element-plus/icons-vue"] = "^2.3.0"
elif ui_lib == 'antdv':
base_json["dependencies"]["ant-design-vue"] = "^4.0.0"
base_json["dependencies"]["@ant-design/icons-vue"] = "^7.0.0"
elif ui_lib == 'naive':
base_json["dependencies"]["naive-ui"] = "^2.37.0"
base_json["devDependencies"]["vfonts"] = "^0.0.3"
# Add i18n if selected
if 'i18n' in features:
base_json["dependencies"]["vue-i18n"] = "^9.8.0"
# Add PWA if selected
if 'pwa' in features:
base_json["devDependencies"]["vite-plugin-pwa"] = "^0.17.0"
import json
return json.dumps(base_json, indent=2, ensure_ascii=False)
def generate_vite_config(tech_stack):
"""Generate vite.config.ts with selected features"""
features = tech_stack.get('features', [])
ui_lib = tech_stack.get('uiLibrary', 'element-plus')
imports = ["import { defineConfig } from 'vite'", "import vue from '@vitejs/plugin-vue'"]
plugins = ["vue()"]
if 'pwa' in features:
imports.append("import { VitePWA } from 'vite-plugin-pwa'")
plugins.append('''VitePWA({
registerType: 'autoUpdate',
manifest: {
name: 'My App',
theme_color: '#ffffff'
}
})''')
imports_str = '\n'.join(imports)
plugins_str = ',\n '.join(plugins)
return f'''{imports_str}
import path from 'path'
export default defineConfig({{
plugins: [
{plugins_str}
],
resolve: {{
alias: {{
'@': path.resolve(__dirname, 'src')
}}
}},
server: {{
port: 3000,
proxy: {{
'/api': {{
target: 'http://localhost:8080',
changeOrigin: true
}}
}}
}}
}})
'''
def map_type(java_type):
"""Map Java type to TypeScript type"""
mapping = {
'String': 'string',
'Long': 'number',
'Integer': 'number',
'BigDecimal': 'number',
'LocalDateTime': 'string',
'Boolean': 'boolean',
'Text': 'string',
'Enum': 'string',
}
return mapping.get(java_type, 'any')
def generate_api_client(entity_spec):
"""Generate TypeScript API client"""
class_name = entity_spec['name']
class_lower = class_name.lower()
base_path = class_lower + 's'
fields = entity_spec.get('fields', [])
field_interfaces = '\n'.join([f" {f['name']}: {map_type(f['type'])}" for f in fields])
query_fields = '\n'.join([f" {f['name']}?: {map_type(f['type'])}" for f in fields[:3]])
return f'''import request from '@/utils/request'
import type {{ PageQuery, PageResult }} from '@/types/common'
export interface {class_name} {{
id: number
{field_interfaces}
createdAt: string
updatedAt: string
}}
export interface {class_name}Query extends PageQuery {{
{query_fields}
}}
export function get{class_name}List(params: {class_name}Query) {{
return request.get<PageResult<{class_name}>>('/api/{base_path}', {{ params }})
}}
export function get{class_name}ById(id: number) {{
return request.get<{class_name}>(`/api/{base_path}/{id}`)
}}
export function create{class_name}(data: Omit<{class_name}, 'id' | 'createdAt' | 'updatedAt'>) {{
return request.post<{class_name}>('/api/{base_path}', data)
}}
export function update{class_name}(id: number, data: Partial<{class_name}>) {{
return request.put<{class_name}>(`/api/{base_path}/{id}`, data)
}}
export function delete{class_name}(id: number) {{
return request.delete(`/api/{base_path}/{id}`)
}}
'''
def generate_store(entity_spec, tech_stack):
"""Generate Pinia store"""
class_name = entity_spec['name']
class_lower = class_name.lower()
return f'''import {{ defineStore }} from 'pinia'
import {{ ref, computed }} from 'vue'
import type {{ {class_name}, {class_name}Query }} from '@/api/{class_lower}'
import * as api from '@/api/{class_lower}'
export const use{class_name}Store = defineStore('{class_lower}', () => {{
// State
const list = ref<{class_name}[]>([])
const loading = ref(false)
const current = ref<{class_name} | null>(null)
const total = ref(0)
// Getters
const getById = computed(() => (id: number) => {{
return list.value.find(item => item.id === id)
}})
// Actions
async function fetchList(query: {class_name}Query) {{
loading.value = true
try {{
const res = await api.get{class_name}List(query)
list.value = res.data.list
total.value = res.data.total
return res.data
}} finally {{
loading.value = false
}}
}}
async function fetchById(id: number) {{
const res = await api.get{class_name}ById(id)
current.value = res.data
return res.data
}}
async function create(data: any) {{
const res = await api.create{class_name}(data)
list.value.unshift(res.data)
return res.data
}}
async function update(id: number, data: any) {{
const res = await api.update{class_name}(id, data)
const index = list.value.findIndex(item => item.id === id)
if (index > -1) {{
list.value[index] = res.data
}}
return res.data
}}
async function remove(id: number) {{
await api.delete{class_name}(id)
list.value = list.value.filter(item => item.id !== id)
}}
return {{
list,
loading,
current,
total,
getById,
fetchList,
fetchById,
create,
update,
remove
}}
}})
'''
def generate_list_view(entity_spec, tech_stack):
"""Generate list view component with selected UI library"""
class_name = entity_spec['name']
class_lower = class_name.lower()
fields = entity_spec.get('fields', [])
ui_lib = tech_stack.get('uiLibrary', 'element-plus')
if ui_lib == 'element-plus':
return _generate_element_plus_list(class_name, class_lower, fields)
elif ui_lib == 'antdv':
return _generate_antdv_list(class_name, class_lower, fields)
else: # naive
return _generate_naive_list(class_name, class_lower, fields)
def _generate_element_plus_list(class_name, class_lower, fields):
columns = '\n'.join([f' <el-table-column prop="{f["name"]}" label="{f["name"].capitalize()}" />' for f in fields[:4]])
search_fields = '\n'.join([f" <el-form-item label='{f['name'].capitalize()}'><el-input v-model=\"query.{f['name']}\" placeholder=\"请输入\" /></el-form-item>" for f in fields[:2]])
return f'''<template>
<div class="{class_lower}-list">
<!-- Search Form -->
<el-form :model="query" inline>
{search_fields}
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="success" @click="handleCreate">新增</el-button>
</el-form-item>
</el-form>
<!-- Data Table -->
<el-table :data="store.list" v-loading="store.loading">
<el-table-column type="index" width="50" />
{columns}
<el-table-column label="操作" width="180">
<template #default="{{ row }}">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- Pagination -->
<el-pagination
v-model:current-page="query.page"
v-model:page-size="query.size"
:total="store.total"
layout="total, sizes, prev, pager, next"
@change="handleSearch"
/>
<!-- Form Dialog -->
<{class_lower}-form-dialog
v-model:visible="dialogVisible"
:data="editData"
@success="handleSearch"
/>
</div>
</template>
<script setup lang="ts">
import {{ ref, reactive, onMounted }} from 'vue'
import {{ use{class_name}Store }} from '@/stores/{class_lower}'
import {{ ElMessage, ElMessageBox }} from 'element-plus'
import {class_name}FormDialog from './components/{class_name}FormDialog.vue'
const store = use{class_name}Store()
const dialogVisible = ref(false)
const editData = ref(null)
const query = reactive({{
page: 1,
size: 10,
{chr(10).join([f" {f['name']}: undefined," for f in fields[:2]])}
}})
onMounted(() => {{
handleSearch()
}})
function handleSearch() {{
store.fetchList(query)
}}
function handleReset() {{
query.page = 1
{chr(10).join([f" query.{f['name']} = undefined" for f in fields[:2]])}
handleSearch()
}}
function handleCreate() {{
editData.value = null
dialogVisible.value = true
}}
function handleEdit(row: any) {{
editData.value = {{ ...row }}
dialogVisible.value = true
}}
async function handleDelete(row: any) {{
try {{
await ElMessageBox.confirm('确认删除该记录?', '提示', {{ type: 'warning' }})
await store.remove(row.id)
ElMessage.success('删除成功')
handleSearch()
}} catch {{ }}
}}
</script>
'''
def _generate_antdv_list(class_name, class_lower, fields):
columns = '\n'.join([f' <a-table-column key="{f["name"]}" dataIndex="{f["name"]}" title="{f["name"].capitalize()}" />' for f in fields[:3]])
return f'''<template>
<div class="{class_lower}-list">
<a-form :model="query" layout="inline">
{chr(10).join([f" <a-form-item label='{f['name'].capitalize()}'><a-input v-model:value=\"query.{f['name']}\" /></a-form-item>" for f in fields[:2]])}
<a-form-item>
<a-button type="primary" @click="handleSearch">搜索</a-button>
<a-button @click="handleReset">重置</a-button>
<a-button type="primary" @click="handleCreate">新增</a-button>
</a-form-item>
</a-form>
<a-table :dataSource="store.list" :loading="store.loading" :pagination="pagination">
<a-table-column key="id" dataIndex="id" title="ID" />
{columns}
<a-table-column key="action" title="操作">
<template #default="{{ {{ record }} }}">
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger @click="handleDelete(record)">删除</a-button>
</template>
</a-table-column>
</a-table>
</div>
</template>
<script setup lang="ts">
import {{ ref, reactive, computed, onMounted }} from 'vue'
import {{ use{class_name}Store }} from '@/stores/{class_lower}'
import {{ message, Modal }} from 'ant-design-vue'
const store = use{class_name}Store()
const dialogVisible = ref(false)
const editData = ref(null)
const query = reactive({{
page: 1,
size: 10,
{chr(10).join([f" {f['name']}: undefined," for f in fields[:2]])}
}})
const pagination = computed(() => ({{
current: query.page,
pageSize: query.size,
total: store.total,
showSizeChanger: true
}}))
onMounted(handleSearch)
function handleSearch() {{
store.fetchList(query)
}}
function handleReset() {{
query.page = 1
{chr(10).join([f" query.{f['name']} = undefined" for f in fields[:2]])}
handleSearch()
}}
function handleCreate() {{
editData.value = null
dialogVisible.value = true
}}
function handleEdit(row: any) {{
editData.value = {{ ...row }}
dialogVisible.value = true
}}
function handleDelete(row: any) {{
Modal.confirm({{
title: '确认删除',
content: '删除后无法恢复,是否继续?',
onOk: async () => {{
await store.remove(row.id)
message.success('删除成功')
handleSearch()
}}
}})
}}
</script>
'''
def _generate_naive_list(class_name, class_lower, fields):
columns = '\n'.join([f" {{ title: '{f['name'].capitalize()}', key: '{f['name']}' }}," for f in fields[:3]])
return f'''<template>
<div class="{class_lower}-list">
<n-space vertical>
<n-form :model="query" inline>
{chr(10).join([f" <n-form-item label='{f['name'].capitalize()}'><n-input v-model:value=\"query.{f['name']}\" /></n-form-item>" for f in fields[:2]])}
<n-form-item>
<n-button type="primary" @click="handleSearch">搜索</n-button>
<n-button @click="handleReset">重置</n-button>
<n-button type="primary" @click="handleCreate">新增</n-button>
</n-form-item>
</n-form>
<n-data-table
:columns="columns"
:data="store.list"
:loading="store.loading"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-space>
</div>
</template>
<script setup lang="ts">
import {{ ref, reactive, onMounted }} from 'vue'
import {{ use{class_name}Store }} from '@/stores/{class_lower}'
import {{ useMessage, useDialog }} from 'naive-ui'
const store = use{class_name}Store()
const message = useMessage()
const dialog = useDialog()
const query = reactive({{
page: 1,
size: 10,
{chr(10).join([f" {f['name']}: undefined," for f in fields[:2]])}
}})
const columns = [
{{ title: 'ID', key: 'id' }},
{columns}
{{
title: '操作',
key: 'actions',
render(row: any) {{
return h(NSpace, null, {{
default: () => [
h(NButton, {{ type: 'primary', size: 'small', onClick: () => handleEdit(row) }}, {{ default: () => '编辑' }}),
h(NButton, {{ type: 'error', size: 'small', onClick: () => handleDelete(row) }}, {{ default: () => '删除' }})
]
}})
}}
}}
]
const pagination = reactive({{
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
}})
onMounted(handleSearch)
function handleSearch() {{
store.fetchList(query).then(data => {{
pagination.itemCount = data.total
}})
}}
function handlePageChange(page: number) {{
query.page = page
handleSearch()
}}
function handleReset() {{
query.page = 1
{chr(10).join([f" query.{f['name']} = undefined" for f in fields[:2]])}
handleSearch()
}}
function handleCreate() {{
// Open create dialog
}}
function handleEdit(row: any) {{
// Open edit dialog
}}
function handleDelete(row: any) {{
dialog.warning({{
title: '确认删除',
content: '删除后无法恢复,是否继续?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {{
await store.remove(row.id)
message.success('删除成功')
handleSearch()
}}
}})
}}
</script>
'''
def main():
"""Test with sample data"""
tech_stack = {
'uiLibrary': 'element-plus',
'stateManagement': 'pinia',
'features': ['darkMode']
}
entity_spec = {
'name': 'Product',
'fields': [
{'name': 'name', 'type': 'String'},
{'name': 'price', 'type': 'BigDecimal'},
{'name': 'stock', 'type': 'Integer'},
]
}
print(generate_package_json('demo', tech_stack))
if __name__ == '__main__':
main()
FILE:references/examples.md
# Project Generator Usage Examples
## Example 1: E-commerce System
Input:
```
创建一个电商管理系统,包含:
- 商品管理:名称、价格、库存、分类、状态
- 订单管理:订单号、用户、商品列表、总价、状态
- 用户管理:用户名、邮箱、手机号、角色
- 分类管理:名称、父分类、排序
```
Generated Entities:
- Product (商品)
- Order (订单)
- OrderItem (订单项)
- User (用户)
- Category (分类)
## Example 2: Blog System
Input:
```
博客系统,包含:
- 文章:标题、内容、作者、分类、标签、状态、浏览量
- 分类:名称、描述
- 标签:名称
- 评论:内容、作者、文章、父评论
- 用户:用户名、昵称、头像、简介
```
Generated Entities:
- Article (文章)
- Category (分类)
- Tag (标签)
- Comment (评论)
- User (用户)
## Example 3: Task Management
Input:
```
任务管理系统:
- 项目:名称、描述、负责人、开始/结束时间、状态
- 任务:标题、描述、所属项目、执行人、优先级、状态、截止日期
- 标签:名称、颜色
- 用户:用户名、邮箱、角色
```
Generated Entities:
- Project (项目)
- Task (任务)
- Tag (标签)
- User (用户)
## Field Type Mapping
| Java Type | TypeScript Type | Database Type | UI Component |
|-----------|-----------------|---------------|--------------|
| String | string | VARCHAR | Input |
| Text | string | TEXT | Textarea |
| Long | number | BIGINT | InputNumber |
| Integer | number | INT | InputNumber |
| BigDecimal | number | DECIMAL | InputNumber |
| LocalDateTime | string | DATETIME | DatePicker |
| Boolean | boolean | BOOLEAN | Switch |
| Enum | string | VARCHAR | Select |
## Quick Start Template
```python
project_spec = {
"name": "YourProject",
"description": "项目描述",
"entities": [
{
"name": "EntityName",
"fields": [
{"name": "fieldName", "type": "String", "required": True},
{"name": "price", "type": "BigDecimal", "required": True},
],
"relationships": [
{"type": "ManyToOne", "target": "OtherEntity", "field": "other"}
]
}
],
"features": ["auth", "audit", "soft-delete"]
}
```
FILE:references/specification.md
# Project Generator Specification Format
## Complete Specification Schema
```json
{
"projectName": "my-app",
"description": "Application description",
"techStack": {
"backend": "spring-boot",
"frontend": "vue3",
"database": "mysql",
"uiLibrary": "element-plus"
},
"features": ["jwt-auth", "audit-log", "soft-delete", "file-upload"],
"entities": [
{
"name": "EntityName",
"tableName": "table_name",
"comment": "Entity description",
"fields": [
{
"name": "fieldName",
"type": "String",
"length": 255,
"required": true,
"unique": false,
"comment": "Field description",
"searchable": true
}
],
"relationships": [
{
"type": "OneToMany|ManyToOne|ManyToMany",
"target": "OtherEntity",
"field": "fieldName",
"mappedBy": "mappedField"
}
],
"operations": ["create", "read", "update", "delete", "list", "search"]
}
],
"pages": [
{
"name": "entity-list",
"type": "list",
"entity": "EntityName",
"features": ["search", "pagination", "export"]
},
{
"name": "entity-form",
"type": "form",
"entity": "EntityName",
"layout": "vertical"
}
]
}
```
## Field Types
### Basic Types
- `String` - 字符串
- `Text` - 长文本
- `Long` - 长整数
- `Integer` - 整数
- `BigDecimal` - 高精度小数
- `LocalDateTime` - 日期时间
- `Boolean` - 布尔值
### Special Types
- `Enum` - 枚举类型
- `Json` - JSON 对象
- `File` - 文件上传
- `Image` - 图片
## Relationship Types
### OneToMany
```json
{
"type": "OneToMany",
"target": "OrderItem",
"field": "items",
"mappedBy": "order"
}
```
### ManyToOne
```json
{
"type": "ManyToOne",
"target": "User",
"field": "creator"
}
```
### ManyToMany
```json
{
"type": "ManyToMany",
"target": "Role",
"field": "roles"
}
```
## Feature Flags
### Backend Features
- `jwt-auth` - JWT 认证
- `rbac` - 基于角色的权限控制
- `audit-log` - 审计日志
- `soft-delete` - 软删除
- `multi-tenant` - 多租户
- `cache` - Redis 缓存
- `file-upload` - 文件上传
- `excel-export` - Excel 导出
### Frontend Features
- `dark-mode` - 暗黑模式
- `i18n` - 国际化
- `pwa` - PWA 支持
- `responsive` - 响应式设计
FILE:assets/tech-stack-config.json
{
"techStackOptions": {
"backend": {
"javaVersion": {
"description": "Java 版本",
"options": [
{"id": "8", "name": "Java 8 (经典稳定)"},
{"id": "11", "name": "Java 11 (LTS)"},
{"id": "17", "name": "Java 17 (推荐)", "default": true},
{"id": "21", "name": "Java 21 (最新)"}
]
},
"buildTool": {
"description": "构建工具",
"options": [
{"id": "maven", "name": "Maven", "default": true},
{"id": "gradle", "name": "Gradle"}
]
},
"springBoot": {
"description": "Spring Boot 版本",
"options": [
{"id": "2.7", "name": "2.7.x (兼容 Java 8/11)", "java8": true},
{"id": "3.0", "name": "3.0.x (Java 17+)", "java17": true},
{"id": "3.2", "name": "3.2.x (最新)", "java17": true, "default": true}
]
},
"database": {
"description": "数据库",
"options": [
{"id": "mysql", "name": "MySQL 8.0", "default": true},
{"id": "mysql57", "name": "MySQL 5.7 (旧版兼容)"},
{"id": "postgresql", "name": "PostgreSQL"},
{"id": "mariadb", "name": "MariaDB"},
{"id": "oracle", "name": "Oracle"},
{"id": "sqlserver", "name": "SQL Server"},
{"id": "h2", "name": "H2 (仅开发)"}
]
},
"orm": {
"description": "数据访问",
"options": [
{"id": "jpa", "name": "JPA / Hibernate", "default": true},
{"id": "mybatis", "name": "MyBatis"},
{"id": "mybatis-plus", "name": "MyBatis-Plus"},
{"id": "jdbc", "name": "Spring Data JDBC (轻量)"}
]
},
"cache": {
"description": "缓存",
"options": [
{"id": "none", "name": "不需要", "default": true},
{"id": "redis", "name": "Redis (分布式)"},
{"id": "caffeine", "name": "Caffeine (本地高性能)"},
{"id": "ehcache", "name": "Ehcache (传统)"}
]
},
"mq": {
"description": "消息队列",
"options": [
{"id": "none", "name": "不需要", "default": true},
{"id": "rabbitmq", "name": "RabbitMQ"},
{"id": "kafka", "name": "Kafka"},
{"id": "rocketmq", "name": "RocketMQ"}
]
},
"search": {
"description": "搜索引擎",
"options": [
{"id": "none", "name": "不需要", "default": true},
{"id": "elasticsearch", "name": "Elasticsearch"},
{"id": "solr", "name": "Solr"}
]
},
"features": {
"description": "常用功能 (多选)",
"multiple": true,
"options": [
{"id": "jwt", "name": "JWT 登录认证"},
{"id": "oauth2", "name": "OAuth2 / 第三方登录"},
{"id": "audit", "name": "操作日志审计"},
{"id": "excel", "name": "Excel 导入导出"},
{"id": "word", "name": "Word / PDF 生成"},
{"id": "email", "name": "邮件发送"},
{"id": "sms", "name": "短信接口"},
{"id": "oss", "name": "文件上传 (OSS/MinIO)"},
{"id": "websocket", "name": "WebSocket 实时通信"},
{"id": "job", "name": "定时任务 (XXL-Job)"},
{"id": "rate", "name": "限流防刷"},
{"id": "idempotent", "name": "接口幂等"}
]
}
},
"frontend": {
"framework": {
"description": "前端框架",
"options": [
{"id": "vue2", "name": "Vue 2 (经典版本)"},
{"id": "vue3", "name": "Vue 3 (推荐)", "default": true},
{"id": "react", "name": "React 18"},
{"id": "react17", "name": "React 17 (旧版)"},
{"id": "angular", "name": "Angular"}
]
},
"language": {
"description": "开发语言",
"options": [
{"id": "ts", "name": "TypeScript (推荐)", "default": true},
{"id": "js", "name": "JavaScript"}
]
},
"uiLibrary": {
"description": "UI 组件库",
"options": [
{"id": "element-ui", "name": "Element UI (Vue 2)", "vue2": true},
{"id": "element-plus", "name": "Element Plus (Vue 3)", "vue3": true, "default": true},
{"id": "antdv1", "name": "Ant Design Vue 1.x (Vue 2)", "vue2": true},
{"id": "antdv", "name": "Ant Design Vue 3.x/4.x (Vue 3)", "vue3": true},
{"id": "naive", "name": "Naive UI (Vue 3)", "vue3": true},
{"id": "iview", "name": "iView / View UI (Vue 2)", "vue2": true},
{"id": "vuetify2", "name": "Vuetify 2 (Vue 2)", "vue2": true},
{"id": "vuetify3", "name": "Vuetify 3 (Vue 3)", "vue3": true},
{"id": "vant2", "name": "Vant 2 (Vue 2 移动端)", "vue2": true, "mobile": true},
{"id": "vant3", "name": "Vant 3/4 (Vue 3 移动端)", "vue3": true, "mobile": true},
{"id": "antdmobile", "name": "Ant Design Mobile (React)", "react": true, "mobile": true}
]
},
"stateManagement": {
"description": "状态管理",
"options": [
{"id": "vuex", "name": "Vuex", "vue": true},
{"id": "pinia", "name": "Pinia (Vue 3 推荐)", "vue3": true, "default": true},
{"id": "redux", "name": "Redux / Redux Toolkit", "react": true},
{"id": "zustand", "name": "Zustand", "react": true},
{"id": "mobx", "name": "MobX", "react": true},
{"id": "recoil", "name": "Recoil", "react": true}
]
},
"css": {
"description": "CSS 方案",
"options": [
{"id": "css", "name": "原生 CSS"},
{"id": "scss", "name": "SCSS / Sass"},
{"id": "less", "name": "Less"},
{"id": "tailwind", "name": "Tailwind CSS (推荐)", "default": true},
{"id": "windicss", "name": "Windi CSS"},
{"id": "unocss", "name": "UnoCSS"}
]
},
"features": {
"description": "常用功能 (多选)",
"multiple": true,
"options": [
{"id": "darkMode", "name": "暗黑模式"},
{"id": "i18n", "name": "国际化多语言"},
{"id": "pwa", "name": "PWA 离线应用"},
{"id": "echarts", "name": "ECharts 图表"},
{"id": "antv", "name": "AntV 可视化"},
{"id": "d3", "name": "D3.js 数据可视化"},
{"id": "map", "name": "地图 (高德/百度)"},
{"id": "editor", "name": "富文本编辑器"},
{"id": "markdown", "name": "Markdown 编辑器"},
{"id": "video", "name": "视频播放器"},
{"id": "excel", "name": "Excel 导入导出"},
{"id": "pdf", "name": "PDF 预览/生成"},
{"id": "lazyload", "name": "图片懒加载"},
{"id": "virtualScroll", "name": "虚拟滚动 (大数据列表)"}
]
}
}
},
"selectionPrompt": {
"title": "🛠️ 请选择技术栈",
"instruction": "回复选项编号,如: 1,1,3,1,1,1,1,1,2,1,1,12 或回车使用默认",
"format": "【后端】Java,SpringBoot,数据库,ORM,缓存,MQ,搜索,功能\n【前端】框架,语言,UI库,状态管理,CSS,功能"
},
"defaults": {
"backend": {
"javaVersion": "8",
"springBoot": "2.7",
"database": "mysql57",
"orm": "mybatis-plus",
"cache": "redis",
"mq": "none",
"search": "none",
"features": []
},
"frontend": {
"framework": "vue3",
"language": "ts",
"uiLibrary": "element-plus",
"stateManagement": "pinia",
"css": "tailwind",
"features": []
}
},
"compatibility": {
"java8": {
"springBoot": ["2.7"],
"notes": "Java 8 仅支持 Spring Boot 2.x"
},
"java11": {
"springBoot": ["2.7", "3.0", "3.2"],
"notes": "Java 11 支持 Spring Boot 2.x 和 3.x"
},
"java17": {
"springBoot": ["3.0", "3.2"],
"notes": "Java 17+ 需要 Spring Boot 3.x"
},
"vue2": {
"uiLibrary": ["element-ui", "antdv1", "iview", "vuetify2", "vant2"],
"stateManagement": ["vuex"],
"notes": "Vue 2 项目推荐使用 Vuex"
},
"vue3": {
"uiLibrary": ["element-plus", "antdv", "naive", "vuetify3", "vant3"],
"stateManagement": ["pinia", "vuex"],
"notes": "Vue 3 推荐 Pinia,也兼容 Vuex"
}
}
}
FILE:assets/templates/application-mysql.txt
spring:
application:
name: {{projectName}}
datasource:
url: jdbc:mysql://localhost:3306/{{projectName}}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
format_sql: true
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
server:
port: 8080
servlet:
context-path: /api
logging:
level:
com.example: DEBUG
org.hibernate.SQL: DEBUG
FILE:assets/templates/application-postgres.txt
spring:
application:
name: {{projectName}}
datasource:
url: jdbc:postgresql://localhost:5432/{{projectName}}
username: postgres
password: password
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
server:
port: 8080
servlet:
context-path: /api
logging:
level:
com.example: DEBUG
org.hibernate.SQL: DEBUG
FILE:assets/templates/build.txt
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '1.0.0'
java {
sourceCompatibility = '{{javaVersion}}'
targetCompatibility = '{{javaVersion}}'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot Starters
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Database
{{#if (eq database 'mysql')}}
runtimeOnly 'mysql:mysql-connector-java:8.0.33'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
{{/if}}
{{#if (eq database 'postgresql')}}
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.flywaydb:flyway-core'
{{/if}}
{{#if (eq database 'h2')}}
runtimeOnly 'com.h2database:h2'
{{/if}}
{{#if (eq orm 'mybatis')}}
// MyBatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
{{/if}}
{{#if features.jwt}}
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
{{/if}}
{{#if features.redis}}
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
{{/if}}
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
FILE:assets/templates/docker-compose-mysql.txt
version: '3.8'
services:
{{projectName}}:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/{{projectName}}?useSSL=false&allowPublicKeyRetrieval=true
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=password
depends_on:
mysql:
condition: service_healthy
networks:
- app-network
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: {{projectName}}
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
networks:
- app-network
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- {{projectName}}
networks:
- app-network
volumes:
mysql-data:
networks:
app-network:
driver: bridge
FILE:assets/templates/docker-compose-postgres.txt
version: '3.8'
services:
{{projectName}}:
build: ./backend
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/{{projectName}}
- SPRING_DATASOURCE_USERNAME=postgres
- SPRING_DATASOURCE_PASSWORD=password
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: {{projectName}}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
networks:
- app-network
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- {{projectName}}
networks:
- app-network
volumes:
postgres-data:
networks:
app-network:
driver: bridge
FILE:assets/templates/mybatis-plus-config.txt
# ============================================
# MyBatis-Plus Configuration
# ============================================
mybatis-plus:
configuration:
# 驼峰命名自动映射
map-underscore-to-camel-case: true
# SQL日志打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 逻辑删除字段
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 主键类型
id-type: auto
# Mapper XML文件位置
mapper-locations: classpath*:/mapper/**/*.xml
# 实体类包路径
type-aliases-package: com.example.{{projectName}}.entity
FILE:assets/templates/package-antdv.txt
{
"name": "{{projectName}}-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.ts,.tsx --fix",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"axios": "^1.6.0",
"ant-design-vue": "^4.0.0",
"@ant-design/icons-vue": "^7.0.0",
"dayjs": "^1.11.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.0",
"prettier": "^3.2.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}
FILE:assets/templates/package-element-ui-vue2.txt
{
"name": "{{projectName}}-frontend",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"vue": "^2.7.0",
"vue-router": "^3.6.0",
"vuex": "^3.6.0",
"axios": "^1.6.0",
"element-ui": "^2.15.0"
},
"devDependencies": {
"@vue/cli-plugin-eslint": "^5.0.0",
"@vue/cli-service": "^5.0.0",
"eslint": "^8.0.0",
"eslint-plugin-vue": "^8.0.0",
"vue-template-compiler": "^2.7.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser",
"requireConfigFile": false
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
FILE:assets/templates/package-naive.txt
{
"name": "{{projectName}}-frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.ts,.tsx --fix",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"axios": "^1.6.0",
"naive-ui": "^2.37.0",
"dayjs": "^1.11.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.0",
"prettier": "^3.2.0",
"typescript": "~5.3.0",
"vfonts": "^0.0.3",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}
FILE:assets/templates/pom.txt
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>{{projectName}}</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- Flyway -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
FILE:assets/templates/vue-package.txt
{
"name": "{{projectName}}",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.ts,.tsx --fix",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"axios": "^1.6.0",
"element-plus": "^2.5.0",
"@element-plus/icons-vue": "^2.3.0",
"dayjs": "^1.11.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.20.0",
"prettier": "^3.2.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}
Provides CI/CD pipeline templates and interactive setup for Java + Vue projects with GitLab CI or Jenkins, supporting linting, testing, building, Dockerizing...
---
name: cicd-workflow
description: CI/CD workflow skill for Java + Vue projects. Supports GitLab CI and Jenkins pipelines with code linting, unit testing, build packaging, Docker image building, Kubernetes deployment, and notification feedback. Use when: (1) Setting up CI/CD pipelines for Java/Vue projects, (2) Configuring GitLab CI or Jenkins workflows, (3) Building Docker images and deploying to Kubernetes, (4) Implementing automated code quality checks and testing, (5) Setting up deployment notifications.
---
# CI/CD Workflow Skill
Complete CI/CD pipeline templates for Java + Vue full-stack projects, supporting GitLab CI and Jenkins with Kubernetes deployment.
## Interactive Configuration (NEW)
This skill supports interactive step-by-step configuration with numbered options.
### Configuration Flow
```
1. Choose Platform (GitLab CI / Jenkins)
↓
2. Choose Project Type (Java / Vue / Java+Vue)
↓
3. Choose Deployment Target (K8s / Docker / SSH)
↓
4. Choose Trigger Method (Manual / Auto / Scheduled)
↓
5. Choose Pipeline Steps (Multi-select)
↓
6. Generate Configuration
```
### Step 1: Platform
| # | Platform | Config File |
|---|----------|-------------|
| 1 | **GitLab CI** | `.gitlab-ci.yml` |
| 2 | **Jenkins** | `Jenkinsfile` |
### Step 2: Project Type
| # | Type | Description |
|---|------|-------------|
| 1 | **Java Backend** | Spring Boot project only |
| 2 | **Vue Frontend** | Vue.js project only |
| 3 | **Java + Vue Fullstack** | Both backend and frontend |
### Step 3: Deployment Target
| # | Target | Description |
|---|--------|-------------|
| 1 | **Kubernetes** | Deploy to K8s cluster with kubectl |
| 2 | **Docker Server** | Deploy to Docker host |
| 3 | **Traditional Server (SSH)** | Deploy via SSH to remote server |
### Step 4: Trigger Method
| # | Method | Description |
|---|--------|-------------|
| 1 | **Manual** | Trigger by "Build Now" button |
| 2 | **Push Auto** | Trigger on every push |
| 3 | **Scheduled** | Trigger by cron schedule |
### Step 5: Pipeline Steps (Multi-select)
| # | Step | Description |
|---|------|-------------|
| 1 | **Lint** | Code quality checks |
| 2 | **Test** | Unit tests with coverage |
| 3 | **Build** | Compile and package |
| 4 | **Dockerize** | Build and push Docker images |
| 5 | **Deploy** | Deploy to target environment |
| 6 | **Notify** | Send notifications |
### Input Format
**Complete in one line:**
```
Platform,Project,Target,Trigger,Steps
```
**Examples:**
- `1,3,1,1,123456` = GitLab CI + Java/Vue + K8s + Manual + All steps
- `2,1,3,1,12356` = Jenkins + Java + SSH + Manual + No Docker
- `1,2,1,2,123456` = GitLab CI + Vue + K8s + Auto trigger + All steps
**Or step by step:**
Reply with one number at a time, the skill will guide you through each step.
## Generated Output
When generating CI/CD configuration, this skill produces a complete package including:
### For Jenkins
```
cicd-output/
├── Jenkinsfile.txt # Pipeline configuration (rename to Jenkinsfile when using)
├── setup-guide.md # Complete setup instructions
├── systemd/
│ └── [app-name].service # systemd service file (for SSH deployment)
└── README.md # Quick reference
```
### For GitLab CI
```
cicd-output/
├── .gitlab-ci.yml.txt # Pipeline configuration (rename to .gitlab-ci.yml when using)
├── setup-guide.md # Complete setup instructions
├── docker-compose.yml # Local development setup
└── README.md # Quick reference
```
### Setup Guide Contents
The automatically generated `setup-guide.md` includes:
**1. Prerequisites**
- Required Jenkins/GitLab version
- Required plugins and extensions
- Server/environment requirements
**2. Credential Configuration**
- Detailed list of required credentials
- Step-by-step credential creation guide
- Security best practices
**3. Platform-Specific Setup**
- Jenkins: Pipeline job creation, plugin installation
- GitLab CI: Runner setup, variable configuration
**4. Deployment Target Setup**
- Kubernetes: Cluster access, namespace setup
- Docker: Registry configuration, daemon setup
- SSH: User creation, key exchange, systemd service
**5. Troubleshooting**
- Common errors and solutions
- Debug tips and log locations
- Verification steps
**6. Customization Guide**
- How to modify environment variables
- How to add custom stages
- How to adjust resource limits
## Pipeline Stages
1. **Prepare** - 环境检查和初始化
2. **Lint** - 代码质量检查 (SpotBugs, PMD, Checkstyle for Java; ESLint, Prettier for Vue)
3. **Test** - 单元测试与覆盖率报告
4. **Build** - 编译打包,同时进行**静态资源安全扫描**
5. **Security Scan** - Trivy 镜像安全扫描(可选)
6. **Dockerize** - 构建并推送 Docker 镜像
7. **Deploy** - 部署到 Kubernetes 集群
8. **Notify** - 发送部署状态通知
## Supported Platforms
- **GitLab CI** (`.gitlab-ci.yml`)
- **Jenkins** (`Jenkinsfile`)
## Quick Start
### GitLab CI
1. Copy `assets/gitlab-ci.yml.txt` to your project root as `.gitlab-ci.yml`
2. Update variables in the file:
- `DOCKER_REGISTRY` - Your Docker registry URL
- `DOCKER_NAMESPACE` - Your registry namespace
- `K8S_NAMESPACE` - Kubernetes namespace
3. Configure CI/CD variables in GitLab:
- `CI_REGISTRY_USER` / `CI_REGISTRY_PASSWORD` - Docker registry credentials
- `KUBE_CONFIG` - Base64 encoded kubeconfig
- `WEBHOOK_URL` - Notification webhook URL
4. Push to trigger pipeline (manual trigger for dockerize and deploy stages)
### Jenkins
1. Copy `assets/Jenkinsfile.txt` to your project root as `Jenkinsfile`
2. Install recommended plugins:
- Pipeline
- Docker Pipeline
- Kubernetes CLI
- JUnit (for test results)
- JaCoCo (optional, for coverage)
- HTTP Request (for notifications)
3. Create Jenkins credentials:
- `docker-registry-credentials` - Docker registry login (username/password)
- `kubeconfig` - Kubernetes config file (secret file)
- `webhook-url` - Notification webhook URL (secret text)
4. Create a new Pipeline job pointing to your repository
5. Run manually via "Build Now"
**Jenkinsfile Features:**
- ✅ Conditional builds based on file changes (`when { changeset }`)
- ✅ Static resource security scan during build
- ✅ Graceful handling of missing plugins
- ✅ Resource limits for Docker agents
- ✅ Multi-environment deployment support
- ✅ Rich notification cards for Feishu/DingTalk
## Project Structure
```
project-root/
├── backend/ # Java Spring Boot project
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile # Copy from assets/Dockerfile.java.txt
├── frontend/ # Vue.js project
│ ├── src/
│ ├── package.json
│ └── Dockerfile # Copy from assets/Dockerfile.vue.txt
├── .gitlab-ci.yml # Copy from assets/.gitlab-ci.yml.txt
├── Jenkinsfile # Copy from assets/Jenkinsfile.txt
└── k8s/
└── deployment.yml # Kubernetes manifests (from assets/)
```
## Assets Reference
### Dockerfiles
- `assets/Dockerfile.java.txt` - Java backend Docker image (multi-stage, Alpine-based)
- `assets/Dockerfile.vue.txt` - Vue frontend Docker image (multi-stage, Nginx-based)
**Note:** Rename `.txt` files to remove the extension when using in your project.
- `Dockerfile.java.txt` → `Dockerfile`
- `Dockerfile.vue.txt` → `Dockerfile`
### Security Features
#### 1. Static Resource Security (Vue Projects)
**自动排除的文件类型:**
- `.vue` - Vue 单文件组件源码
- `*.config.js/ts/mjs/cjs/json` - 各种配置文件
- `vite.config.*` - Vite 配置
- `webpack.config.*` - Webpack 配置
- `babel.config.*` - Babel 配置
- `tailwind.config.*` - Tailwind 配置
- `postcss.config.*` - PostCSS 配置
- `eslint.config.*` / `.eslintrc.*` - ESLint 配置
- `.prettierrc.*` - Prettier 配置
- `*.map` - Source map 文件
**防护层级:**
| 层级 | 位置 | 机制 |
|------|------|------|
| 构建时 | Dockerfile | `find` 命令删除上述文件 |
| 运行时 | Nginx | `location` 规则返回 404 |
| CI/CD | Jenkinsfile | 构建阶段扫描并删除 |
#### 2. Nginx Security Configuration
```nginx
# 拒绝访问源码文件
location ~* \.vue$ { return 404; }
# 拒绝访问配置文件
location ~* (config|vite|webpack|babel|tailwind|postcss|eslint|prettier)\.config\.(js|ts|mjs|cjs|json)$ {
return 404;
}
# 拒绝访问 source map
location ~* \.map$ { return 404; }
```
### Kubernetes
- `assets/k8s-deployment.yml` - Complete K8s manifests including:
- Deployments with health checks
- Services (ClusterIP)
- Ingress with TLS
- HorizontalPodAutoscaler (HPA)
### Nginx Config
- `assets/nginx.conf.txt` - Optimized Nginx configuration for Vue SPA with:
- Gzip compression
- Static asset caching
- API proxy to backend
- Health check endpoint
- Security rules (blocks .vue, config files, source maps)
**Note:** Copy and rename to `nginx.conf` when using.
## Scripts
### Notification Script
`scripts/notify.sh` - Send deployment notifications to:
- 飞书 (Feishu)
- 钉钉 (DingTalk)
- Slack
- 企业微信 (WeChat Work)
Usage:
```bash
export WEBHOOK_TYPE=feishu
export WEBHOOK_URL=https://open.feishu.cn/...
export PROJECT_NAME=my-app
export VERSION=1.0.0
./scripts/notify.sh success
```
## Customization Guide
### 1. Adjust Resource Limits
Edit `assets/k8s-deployment.yml`:
```yaml
resources:
requests:
memory: "512Mi" # Adjust based on your app
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
```
### 2. Change Trigger Strategy
**GitLab CI** - Remove `when: manual` to auto-trigger:
```yaml
dockerize-java:
# ...
# when: manual # Remove or comment this line
```
**Jenkins** - Add SCM polling:
```groovy
triggers {
pollSCM('H/5 * * * *') // Check every 5 minutes
}
```
### 3. Add Environment Stages
Add staging deployment between build and production:
**GitLab CI:**
```yaml
stages:
- lint
- test
- build
- dockerize
- deploy-staging # Add this
- deploy-production # Rename from deploy
- notify
deploy-staging:
stage: deploy-staging
script:
- kubectl set image ... -n staging
environment:
name: staging
when: manual
```
### 4. Custom Quality Gates
Add SonarQube analysis:
```yaml
sonarqube:
stage: test
image: sonarsource/sonar-scanner-cli
script:
- sonar-scanner
-Dsonar.projectKey=$CI_PROJECT_NAME
-Dsonar.sources=.
-Dsonar.host.url=$SONAR_URL
-Dsonar.login=$SONAR_TOKEN
```
### 5. Multi-Environment Support
Use GitLab environments or Jenkins branches:
**GitLab:**
```yaml
deploy:
script:
- |
if [ "$CI_COMMIT_REF_NAME" == "main" ]; then
kubectl set image ... -n production
else
kubectl set image ... -n staging
fi
```
## Troubleshooting
### Static Resource Security Violation
**Error:** Build fails with "Security violation found: *.vue files in dist"
**Cause:** Vue build configuration may be including source files
**Solution:**
1. Check `vite.config.js` / `vue.config.js` for incorrect `publicDir` or `assetsInclude`
2. Verify `.gitignore` excludes source files from build
3. Manual fix in Dockerfile already handles cleanup:
```dockerfile
RUN find /usr/share/nginx/html -type f \
-name "*.vue" -o \
-name "*.config.js" \
-delete
```
### Jenkins Plugin Not Found
**Error:** `No such DSL method 'publishTestResults'`
**Solution:**
- Jenkinsfile now uses standard `junit` plugin instead of custom publishers
- Install **JUnit Plugin** from Jenkins plugin manager
- Or disable test publishing by removing the `post { always { junit ... } }` blocks
### Docker Build Context Issues
**Error:** `unable to prepare context: unable to evaluate symlinks`
**Solution:**
```groovy
// Use explicit build context
Dockerfile: "-f backend/Dockerfile backend/"
// Not: "-f backend/Dockerfile ."
```
### Kubectl Commands Fail
- Verify `KUBE_CONFIG` is base64 encoded correctly
- Check cluster name matches the context in kubeconfig
- Ensure service account has deployment permissions
### Image Pull Errors
- Verify image tags are pushed correctly
- Check image pull secrets if using private registry
- Verify pod has `imagePullPolicy: Always` for latest tags
### Rollout Hangs
- Check pod events: `kubectl describe pod <pod-name>`
- Verify resource limits are not too low
- Check application logs: `kubectl logs <pod-name>`
## Security Best Practices
1. **Never commit credentials** - Always use CI/CD variables
2. **Use specific image tags** - Avoid `:latest` in production
3. **Enable RBAC** - Limit service account permissions
4. **Scan images** - Add Trivy or Clair vulnerability scanning
5. **Network policies** - Restrict pod-to-pod communication
6. **Resource quotas** - Set namespace limits
## References
- [GitLab CI Documentation](https://docs.gitlab.com/ee/ci/)
- [Jenkins Pipeline Documentation](https://www.jenkins.io/doc/book/pipeline/)
- [Kubernetes Deployment Guide](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)
FILE:scripts/generate-setup-guide.py
#!/usr/bin/env python3
"""
CI/CD Setup Guide Generator
Generates comprehensive setup documentation based on configuration
"""
from pathlib import Path
from datetime import datetime
def generate_jenkins_guide(config):
"""Generate Jenkins setup guide"""
project_type = config.get('project_type', 'java')
deploy_target = config.get('deploy_target', 'ssh')
steps = config.get('steps', [])
project_name = config.get('project_name', 'my-app')
guide = f"""# Jenkins CI/CD 配置指南
## 📋 配置摘要
| 配置项 | 值 |
|--------|-----|
| **平台** | Jenkins |
| **项目类型** | {project_type.upper()} |
| **部署目标** | {deploy_target.upper()} |
| **触发方式** | {config.get('trigger', 'manual').upper()} |
| **流水线步骤** | {', '.join(steps) if steps else 'Build, Deploy, Notify'} |
## 📦 生成文件清单
```
cicd-output/
├── Jenkinsfile # Jenkins 流水线配置
├── setup-guide.md # 本文件(配置说明)
├── {project_name}.service # systemd 服务文件
└── README.md # 快速参考
```
---
## 🔧 前置准备
### 1. Jenkins 版本要求
- **Jenkins**: 2.300+ 推荐
- **Java**: 8 或 11(Jenkins 运行环境)
### 2. 必须安装的插件
进入 **Manage Jenkins → Manage Plugins → Available**,安装以下插件:
| 插件名称 | 用途 | 必需 |
|---------|------|------|
| **Pipeline** | 流水线支持 | ✅ |
| **SSH Agent** | SSH 密钥认证 | {'✅' if deploy_target == 'ssh' else '❌'} |
| **Docker Pipeline** | Docker 构建 | {'✅' if 'dockerize' in steps else '❌'} |
| **Kubernetes CLI** | K8s 部署 | {'✅' if deploy_target == 'k8s' else '❌'} |
| **HTTP Request** | 发送通知 | {'✅' if 'notify' in steps else '❌'} |
### 3. 全局工具配置
进入 **Manage Jenkins → Global Tool Configuration**:
**Maven**(如果是 Java 项目):
- 名称: `maven-3.8`
- 选择自动安装或指定已安装的 Maven 路径
**JDK**(可选):
- 名称: `jdk-1.8`
- JAVA_HOME: `/usr/lib/jvm/java-8-openjdk`
---
## 🔐 凭证配置
进入 **Manage Jenkins → Manage Credentials → System → Global credentials**,添加以下凭证:
| 凭证 ID | 类型 | 示例值 | 说明 |
|---------|------|--------|------|
| `deploy-host` | Secret text | `192.168.1.100` | 目标服务器 IP |
| `deploy-user` | Secret text | `deploy` | SSH 登录用户名 |
| `deploy-ssh-key` | SSH Username with private key | - | SSH 私钥 |
| `docker-registry` | Username with password | - | Docker 仓库登录 | {'(Docker部署必需)' if deploy_target in ['docker', 'k8s'] else '(如使用Docker)'} |
| `kubeconfig` | Secret file | - | K8s 配置文件 | {'(K8s部署必需)' if deploy_target == 'k8s' else '(如部署到K8s)'} |
| `webhook-url` | Secret text | `https://open.feishu.cn/...` | 飞书/钉钉机器人 | {'(通知必需)' if 'notify' in steps else '(可选)'} |
**添加步骤:**
1. 点击 **Add Credentials**
2. 选择 **Kind**(类型)
3. 输入 **ID**(必须严格匹配上表)
4. 填写其他信息
5. 点击 **Create**
---
## 🚀 创建 Pipeline Job
### 步骤 1:新建 Job
1. 点击 Jenkins 首页 **New Item**
2. 输入名称(如 `{project_name}-deploy`)
3. 选择 **Pipeline** 类型
4. 点击 **OK**
### 步骤 2:配置 Pipeline
在 Job 配置页面,找到 **Pipeline** 部分:
| 配置项 | 值 |
|--------|-----|
| **Definition** | Pipeline script from SCM |
| **SCM** | Git |
| **Repository URL** | 你的 Git 仓库地址 |
| **Credentials** | 如果仓库私有,选择 Git 凭证 |
| **Branch Specifier** | `*/main` 或 `*/master` |
| **Script Path** | `Jenkinsfile` |
### 步骤 3:保存
点击 **Save** 保存配置。
---
## 🖥️ 目标服务器配置
"""
if deploy_target == 'ssh':
guide += f"""### SSH 服务器准备
在目标服务器(`deploy-host`)上执行以下操作:
#### 1. 创建部署用户
```bash
# 创建 deploy 用户
sudo useradd -m -s /bin/bash deploy
# 设置密码(可选,如果使用密钥认证)
sudo passwd deploy
# 添加到 sudo 组
sudo usermod -aG sudo deploy
```
#### 2. 创建部署目录
```bash
# 创建应用目录
sudo mkdir -p /opt/{project_name} /opt/backup
sudo chown -R deploy:deploy /opt/{project_name} /opt/backup
```
#### 3. 配置 SSH 免密登录
在 Jenkins 服务器上执行:
```bash
# 切换到 jenkins 用户
sudo su - jenkins
# 生成 SSH 密钥(如果没有)
ssh-keygen -t rsa -b 4096 -C "jenkins-deploy"
# 复制公钥到目标服务器
ssh-copy-id deploy@YOUR_SERVER_IP
# 测试连接
ssh deploy@YOUR_SERVER_IP "echo '连接成功'"
```
#### 4. 配置 sudo 免密码
在目标服务器上:
```bash
sudo visudo
```
添加以下行:
```
deploy ALL=(ALL) NOPASSWD: /bin/systemctl start {project_name}, /bin/systemctl stop {project_name}, /bin/systemctl restart {project_name}
```
#### 5. 部署 systemd 服务
将生成的 `{project_name}.service` 文件复制到服务器:
```bash
# 在目标服务器上
sudo cp {project_name}.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable {project_name}
```
#### 6. 安装 Java(如果未安装)
```bash
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y openjdk-8-jdk
# CentOS/RHEL
sudo yum install -y java-1.8.0-openjdk
```
验证:
```bash
java -version
```
"""
elif deploy_target == 'k8s':
guide += """### Kubernetes 集群准备
#### 1. 配置 Kubectl 访问
确保 Jenkins 服务器可以访问 K8s 集群:
```bash
# 测试连接
kubectl cluster-info
# 查看节点
kubectl get nodes
```
#### 2. 创建 Namespace(可选)
```bash
kubectl create namespace production
```
#### 3. 配置镜像仓库密钥(如果使用私有仓库)
```bash
kubectl create secret docker-registry regcred \\
--docker-server=your-registry.com \\
--docker-username=username \\
--docker-password=password \\
--namespace=production
```
#### 4. 上传 Kubeconfig 到 Jenkins
```bash
# 复制 kubeconfig 内容
cat ~/.kube/config | base64
# 在 Jenkins 中创建 credentials:
# Kind: Secret file
# ID: kubeconfig
# File: 粘贴 base64 内容或上传文件
```
"""
elif deploy_target == 'docker':
guide += """### Docker 服务器准备
#### 1. 安装 Docker
```bash
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y docker.io
# CentOS/RHEL
sudo yum install -y docker
# 启动 Docker
sudo systemctl start docker
sudo systemctl enable docker
```
#### 2. 配置 Docker 仓库登录
在 Jenkins 服务器上(jenkins 用户):
```bash
sudo su - jenkins
docker login your-registry.com
```
#### 3. 配置远程访问(可选)
如果需要 Jenkins 远程控制 Docker:
```bash
# 编辑 Docker 配置
sudo vim /etc/docker/daemon.json
```
添加:
```json
{{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}}
```
重启:
```bash
sudo systemctl restart docker
```
"""
guide += f"""
---
## 🏃 运行流水线
### 手动触发
1. 进入 Pipeline Job 页面
2. 点击 **Build Now**
3. 查看构建进度(点击构建编号 → Console Output)
### 查看结果
构建完成后:
- **蓝色**: 成功 ✅
- **红色**: 失败 ❌
- **黄色**: 不稳定 ⚠️
---
## 🐛 故障排查
### 问题 1: SSH 连接失败
**现象**: `Permission denied (publickey)`
**解决**:
```bash
# 1. 检查 Jenkins 服务器上的 SSH 密钥
sudo su - jenkins
cat ~/.ssh/id_rsa.pub
# 2. 确保公钥已添加到目标服务器的 authorized_keys
cat ~/.ssh/authorized_keys # 在目标服务器上
# 3. 检查权限
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
```
### 问题 2: Maven 构建失败
**现象**: `mvn: command not found`
**解决**:
- 在 Jenkins 中配置 Maven 工具
- 或在服务器上安装 Maven 并添加到 PATH
### 问题 3: 部署后服务无法启动
**现象**: `systemctl start failed`
**解决**:
```bash
# 在目标服务器上查看日志
sudo journalctl -u {project_name} -f
# 检查 JAR 文件是否存在
ls -la /opt/{project_name}/
# 检查端口占用
sudo lsof -i :8080
```
### 问题 4: 健康检查失败
**现象**: `curl: (7) Failed to connect`
**解决**:
- 检查应用是否正常启动
- 检查防火墙设置
- 增加等待时间(修改 sleep 时长)
---
## ⚙️ 自定义配置
### 修改 JVM 参数
编辑 `{project_name}.service`:
```ini
ExecStart=/usr/bin/java -jar -Xmx2g -Xms1g -Dspring.profiles.active=prod /opt/{project_name}/app.jar
```
### 修改部署路径
编辑 `Jenkinsfile` 中的环境变量:
```groovy
DEPLOY_PATH = '/your/custom/path'
```
### 添加构建参数
在 Jenkins Job 配置中:
1. 勾选 **This project is parameterized**
2. 添加 **String Parameter**:
- 名称: `BRANCH`
- 默认值: `main`
---
## 📚 参考文档
- [Jenkins Pipeline 官方文档](https://www.jenkins.io/doc/book/pipeline/)
- [Pipeline Syntax](https://www.jenkins.io/doc/book/pipeline/syntax/)
- [Jenkins Credentials](https://www.jenkins.io/doc/book/using/using-credentials/)
"""
return guide
def generate_gitlab_guide(config):
"""Generate GitLab CI setup guide"""
project_type = config.get('project_type', 'java')
deploy_target = config.get('deploy_target', 'k8s')
project_name = config.get('project_name', 'my-app')
return f"""# GitLab CI 配置指南
## 📋 配置摘要
| 配置项 | 值 |
|--------|-----|
| **平台** | GitLab CI |
| **项目类型** | {project_type.upper()} |
| **部署目标** | {deploy_target.upper()} |
| **配置文件** | `.gitlab-ci.yml` |
## 📦 生成文件清单
```
cicd-output/
├── .gitlab-ci.yml # GitLab CI 配置
├── setup-guide.md # 本文件
├── docker-compose.yml # 本地开发配置(可选)
└── README.md
```
---
## 🔧 前置准备
### 1. GitLab 版本要求
- **GitLab**: 13.0+ 推荐
- **GitLab Runner**: 13.0+
### 2. 安装 GitLab Runner
**Docker 方式安装:**
```bash
docker run -d --name gitlab-runner --restart always \\
-v /srv/gitlab-runner/config:/etc/gitlab-runner \\
-v /var/run/docker.sock:/var/run/docker.sock \\
gitlab/gitlab-runner:latest
```
**注册 Runner:**
```bash
docker exec -it gitlab-runner gitlab-runner register
```
按提示输入:
- GitLab URL: `https://gitlab.com` 或你的 GitLab 地址
- Registration token: 从 GitLab 项目 Settings → CI/CD → Runners 获取
- Executor: `docker` 或 `shell`
- Default Docker image: `maven:3.8-openjdk-8`
---
## 🔐 配置 CI/CD 变量
进入项目 **Settings → CI/CD → Variables**,添加以下变量:
| 变量名 | 类型 | 说明 |
|--------|------|------|
| `CI_REGISTRY_USER` | Variable | Docker 仓库用户名 |
| `CI_REGISTRY_PASSWORD` | Variable | Docker 仓库密码 |
| `KUBE_CONFIG` | File | K8s kubeconfig 文件 | {'(K8s必需)' if deploy_target == 'k8s' else '(可选)'} |
| `WEBHOOK_URL` | Variable | 飞书/钉钉机器人地址 | (可选) |
---
## 🚀 使用说明
### 1. 复制配置文件
```bash
cp cicd-output/.gitlab-ci.yml /your-project/
git add .gitlab-ci.yml
git commit -m "Add CI/CD configuration"
git push
```
### 2. 触发流水线
- **自动**: Push 代码到仓库
- **手动**: 进入项目 CI/CD → Pipelines → Run pipeline
---
## 🐛 故障排查
### Runner 不执行作业
检查 Runner 状态:
```bash
docker exec -it gitlab-runner gitlab-runner status
```
### Docker 构建失败
确保 Runner 有 Docker 权限:
```bash
# 编辑 config.toml
docker exec -it gitlab-runner vi /etc/gitlab-runner/config.toml
# 添加 privileged = true
[runners.docker]
privileged = true
```
---
## 📚 参考文档
- [GitLab CI/CD 文档](https://docs.gitlab.com/ee/ci/)
- [GitLab Runner 安装](https://docs.gitlab.com/runner/install/)
"""
if __name__ == '__main__':
# Example usage
config = {
'platform': 'jenkins',
'project_type': 'java',
'deploy_target': 'ssh',
'trigger': 'manual',
'steps': ['build', 'deploy', 'notify'],
'project_name': 'my-app'
}
if config['platform'] == 'jenkins':
print(generate_jenkins_guide(config))
else:
print(generate_gitlab_guide(config))
FILE:scripts/notify.sh
#!/bin/bash
# ============================================
# CI/CD 通知脚本
# 支持:飞书、钉钉、Slack、企业微信
# ============================================
set -e
# 配置
WEBHOOK_TYPE="-feishu" # feishu, dingtalk, slack, wecom
WEBHOOK_URL="-"
STATUS="-success" # success, failure, unstable
PROJECT_NAME="-Unknown"
VERSION="-latest"
BUILD_URL="-"
BUILD_USER="-Jenkins"
# 检查必要参数
if [ -z "$WEBHOOK_URL" ]; then
echo "错误: WEBHOOK_URL 未设置"
exit 1
fi
# 根据状态设置图标和颜色
case "$STATUS" in
success)
ICON="✅"
COLOR="green"
TITLE="部署成功"
;;
failure)
ICON="❌"
COLOR="red"
TITLE="部署失败"
;;
unstable)
ICON="⚠️"
COLOR="yellow"
TITLE="部署不稳定"
;;
*)
ICON="ℹ️"
COLOR="blue"
TITLE="部署通知"
;;
esac
# 构建消息内容
MESSAGE="ICON TITLE
项目:PROJECT_NAME
版本:VERSION
构建人:BUILD_USER
链接:BUILD_URL"
# 发送飞书通知
send_feishu() {
local payload
payload=$(cat <<EOF
{
"msg_type": "interactive",
"card": {
"header": {
"title": {
"tag": "plain_text",
"content": "TITLE"
},
"template": "COLOR"
},
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**项目:** PROJECT_NAME\n**版本:** VERSION\n**构建人:** BUILD_USER\n**时间:** $(date '+%Y-%m-%d %H:%M:%S')"
}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "查看详情"
},
"url": "BUILD_URL",
"type": "primary"
}
]
}
]
}
}
EOF
)
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$payload"
}
# 发送钉钉通知
send_dingtalk() {
local payload
payload=$(cat <<EOF
{
"msgtype": "markdown",
"markdown": {
"title": "TITLE",
"text": "### ICON TITLE\n\n**项目:** PROJECT_NAME\n\n**版本:** VERSION\n\n**构建人:** BUILD_USER\n\n**时间:** $(date '+%Y-%m-%d %H:%M:%S')\n\n[查看详情](BUILD_URL)"
}
}
EOF
)
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$payload"
}
# 发送 Slack 通知
send_slack() {
local payload
payload=$(cat <<EOF
{
"attachments": [
{
"color": "COLOR",
"title": "TITLE",
"fields": [
{"title": "项目", "value": "PROJECT_NAME", "short": true},
{"title": "版本", "value": "VERSION", "short": true},
{"title": "构建人", "value": "BUILD_USER", "short": true},
{"title": "时间", "value": "$(date '+%Y-%m-%d %H:%M:%S')", "short": true}
],
"actions": [
{
"type": "button",
"text": "查看详情",
"url": "BUILD_URL"
}
]
}
]
}
EOF
)
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$payload"
}
# 发送企业微信通知
send_wecom() {
local payload
payload=$(cat <<EOF
{
"msgtype": "markdown",
"markdown": {
"content": "ICON **TITLE**\n\n>项目:PROJECT_NAME\n>版本:VERSION\n>构建人:BUILD_USER\n>时间:$(date '+%Y-%m-%d %H:%M:%S')\n>[查看详情](BUILD_URL)"
}
}
EOF
)
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$payload"
}
# 主函数
main() {
echo "发送 WEBHOOK_TYPE 通知..."
case "$WEBHOOK_TYPE" in
feishu)
send_feishu
;;
dingtalk)
send_dingtalk
;;
slack)
send_slack
;;
wecom)
send_wecom
;;
*)
echo "错误: 不支持的 WEBHOOK_TYPE: WEBHOOK_TYPE"
exit 1
;;
esac
echo "通知发送完成"
}
main "$@"
FILE:assets/Dockerfile.java.txt
# ============================================
# Java 后端 Dockerfile
# ============================================
FROM eclipse-temurin:17-jre-alpine
# 设置工作目录
WORKDIR /app
# 安装必要的工具
RUN apk add --no-cache curl
# 复制构建好的 JAR 文件
COPY artifacts/java-backend.jar app.jar
# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN chown appuser:appgroup /app/app.jar
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]
FILE:assets/Dockerfile.vue.txt
# ============================================
# Vue 前端 Dockerfile (多阶段构建)
# 安全加固: 排除源码和配置文件
# ============================================
# 阶段 1: 构建
FROM node:18-alpine AS builder
WORKDIR /app
# 复制 package 文件并安装依赖
COPY frontend/package*.json ./
RUN npm ci
# 复制源代码并构建
COPY frontend/ ./
RUN npm run build
# 阶段 2: 运行 (Nginx)
FROM nginx:alpine
# 安装必要的工具
RUN apk add --no-cache curl
# 复制 Nginx 配置
COPY cicd-workflow/assets/nginx.conf /etc/nginx/conf.d/default.conf
# 复制构建好的前端文件(只复制 dist 内容)
COPY --from=builder /app/dist /usr/share/nginx/html
# 安全加固: 验证并删除可能存在的源码和配置文件
RUN find /usr/share/nginx/html -type f \( \
-name "*.vue" -o \
-name "*.config.js" -o \
-name "*.config.ts" -o \
-name "*.config.mjs" -o \
-name "*.config.cjs" -o \
-name "*.config.json" -o \
-name "vite.config.*" -o \
-name "webpack.config.*" -o \
-name "babel.config.*" -o \
-name "tailwind.config.*" -o \
-name "postcss.config.*" -o \
-name "eslint.config.*" -o \
-name ".eslintrc.*" -o \
-name ".prettierrc.*" -o \
-name "*.map" \
\) -delete && \
echo "Security check: removed source and config files"
# 验证静态资源目录内容
RUN echo "=== Final static files ===" && \
find /usr/share/nginx/html -type f | head -20
# 暴露端口
EXPOSE 80
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost/ || exit 1
# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
FILE:assets/gitlab-ci.yml
# GitLab CI/CD Pipeline for Java + Vue Projects
# 支持:代码检查 → 单元测试 → 构建打包 → 镜像构建 → 部署 K8s → 通知反馈
variables:
# 镜像仓库配置
DOCKER_REGISTRY: "your-registry.com"
DOCKER_NAMESPACE: "your-namespace"
# 项目配置
JAVA_PROJECT_NAME: "java-backend"
VUE_PROJECT_NAME: "vue-frontend"
# K8s 配置
K8S_NAMESPACE: "production"
K8S_CLUSTER: "your-k8s-cluster"
# 版本标签(使用 CI 管道 ID 和提交 SHA)
IMAGE_TAG: "CI_PIPELINE_ID-CI_COMMIT_SHORT_SHA"
# 阶段定义
stages:
- lint
- test
- build
- dockerize
- deploy
- notify
# 缓存配置
cache:
paths:
- .m2/repository
- node_modules/
# ==================== 代码检查阶段 ====================
lint-java:
stage: lint
image: maven:3.9-eclipse-temurin-17
script:
- cd backend
- mvn spotbugs:check pmd:check checkstyle:check -q
only:
- main
- develop
tags:
- docker
lint-vue:
stage: lint
image: node:18-alpine
script:
- cd frontend
- npm ci
- npm run lint
- npm run format:check
only:
- main
- develop
tags:
- docker
# ==================== 单元测试阶段 ====================
test-java:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- cd backend
- mvn test -q
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:
reports:
junit: backend/target/surefire-reports/*.xml
coverage_report:
coverage_format: cobertura
path: backend/target/site/cobertura/coverage.xml
paths:
- backend/target/surefire-reports/
expire_in: 1 week
only:
- main
- develop
tags:
- docker
test-vue:
stage: test
image: node:18-alpine
script:
- cd frontend
- npm ci
- npm run test:unit -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
junit: frontend/junit.xml
coverage_report:
coverage_format: cobertura
path: frontend/coverage/cobertura-coverage.xml
paths:
- frontend/coverage/
expire_in: 1 week
only:
- main
- develop
tags:
- docker
# ==================== 构建打包阶段 ====================
build-java:
stage: build
image: maven:3.9-eclipse-temurin-17
script:
- cd backend
- mvn clean package -DskipTests -q
- cp target/*.jar ../artifacts/JAVA_PROJECT_NAME.jar
artifacts:
paths:
- artifacts/JAVA_PROJECT_NAME.jar
expire_in: 1 week
dependencies:
- test-java
only:
- main
- develop
tags:
- docker
build-vue:
stage: build
image: node:18-alpine
script:
- cd frontend
- npm ci
- npm run build
- mkdir -p ../artifacts
- cp -r dist ../artifacts/VUE_PROJECT_NAME
artifacts:
paths:
- artifacts/VUE_PROJECT_NAME/
expire_in: 1 week
dependencies:
- test-vue
only:
- main
- develop
tags:
- docker
# ==================== 镜像构建阶段 ====================
dockerize-java:
stage: dockerize
image: docker:24-dind
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $DOCKER_REGISTRY -u "$CI_REGISTRY_USER" --password-stdin
script:
- docker build -t $DOCKER_REGISTRY/$DOCKER_NAMESPACE/JAVA_PROJECT_NAME:IMAGE_TAG -f backend/Dockerfile .
- docker tag $DOCKER_REGISTRY/$DOCKER_NAMESPACE/JAVA_PROJECT_NAME:IMAGE_TAG $DOCKER_REGISTRY/$DOCKER_NAMESPACE/JAVA_PROJECT_NAME:latest
- docker push $DOCKER_REGISTRY/$DOCKER_NAMESPACE/JAVA_PROJECT_NAME:IMAGE_TAG
- docker push $DOCKER_REGISTRY/$DOCKER_NAMESPACE/JAVA_PROJECT_NAME:latest
dependencies:
- build-java
only:
- main
- develop
tags:
- docker
when: manual
dockerize-vue:
stage: dockerize
image: docker:24-dind
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $DOCKER_REGISTRY -u "$CI_REGISTRY_USER" --password-stdin
script:
- docker build -t $DOCKER_REGISTRY/$DOCKER_NAMESPACE/VUE_PROJECT_NAME:IMAGE_TAG -f frontend/Dockerfile .
- docker tag $DOCKER_REGISTRY/$DOCKER_NAMESPACE/VUE_PROJECT_NAME:IMAGE_TAG $DOCKER_REGISTRY/$DOCKER_NAMESPACE/VUE_PROJECT_NAME:latest
- docker push $DOCKER_REGISTRY/$DOCKER_NAMESPACE/VUE_PROJECT_NAME:IMAGE_TAG
- docker push $DOCKER_REGISTRY/$DOCKER_NAMESPACE/VUE_PROJECT_NAME:latest
dependencies:
- build-vue
only:
- main
- develop
tags:
- docker
when: manual
# ==================== K8s 部署阶段 ====================
deploy-k8s:
stage: deploy
image: bitnami/kubectl:latest
script:
# 配置 K8s 访问
- echo "$KUBE_CONFIG" | base64 -d > ~/.kube/config
- kubectl config use-context $K8S_CLUSTER
# 更新镜像版本
- kubectl set image deployment/JAVA_PROJECT_NAME JAVA_PROJECT_NAME=$DOCKER_REGISTRY/$DOCKER_NAMESPACE/JAVA_PROJECT_NAME:IMAGE_TAG -n $K8S_NAMESPACE
- kubectl set image deployment/VUE_PROJECT_NAME VUE_PROJECT_NAME=$DOCKER_REGISTRY/$DOCKER_NAMESPACE/VUE_PROJECT_NAME:IMAGE_TAG -n $K8S_NAMESPACE
# 等待滚动更新完成
- kubectl rollout status deployment/JAVA_PROJECT_NAME -n $K8S_NAMESPACE --timeout=300s
- kubectl rollout status deployment/VUE_PROJECT_NAME -n $K8S_NAMESPACE --timeout=300s
# 验证部署
- kubectl get pods -n $K8S_NAMESPACE -l app=JAVA_PROJECT_NAME
- kubectl get pods -n $K8S_NAMESPACE -l app=VUE_PROJECT_NAME
dependencies:
- dockerize-java
- dockerize-vue
only:
- main
tags:
- docker
when: manual
environment:
name: production
url: https://your-app.com
# ==================== 通知反馈阶段 ====================
notify-success:
stage: notify
image: alpine/curl:latest
script:
- |
curl -X POST "WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"msg_type\": \"text\",
\"content\": {
\"text\": \"✅ 部署成功\\n项目:CI_PROJECT_NAME\\n分支:CI_COMMIT_REF_NAME\\n版本:IMAGE_TAG\\n提交人:GITLAB_USER_NAME\\n流水线:CI_PIPELINE_URL\"
}
}"
only:
- main
tags:
- docker
when: on_success
dependencies:
- deploy-k8s
notify-failure:
stage: notify
image: alpine/curl:latest
script:
- |
curl -X POST "WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"msg_type\": \"text\",
\"content\": {
\"text\": \"❌ 部署失败\\n项目:CI_PROJECT_NAME\\n分支:CI_COMMIT_REF_NAME\\n版本:IMAGE_TAG\\n提交人:GITLAB_USER_NAME\\n流水线:CI_PIPELINE_URL\"
}
}"
only:
- main
- develop
tags:
- docker
when: on_failure
FILE:assets/Jenkinsfile.txt
// Jenkins Pipeline for Java + Vue Projects
// 支持:代码检查 → 单元测试 → 构建打包 → 镜像构建 → 部署 K8s → 通知反馈
// 触发方式:手动触发 / Git webhook / 定时触发
pipeline {
agent any
environment {
// 镜像仓库配置
DOCKER_REGISTRY = 'your-registry.com'
DOCKER_NAMESPACE = 'your-namespace'
DOCKER_CREDENTIALS_ID = 'docker-registry-credentials'
// 项目配置
JAVA_PROJECT_NAME = 'java-backend'
VUE_PROJECT_NAME = 'vue-frontend'
// K8s 配置
K8S_NAMESPACE = 'production'
KUBE_CONFIG_ID = 'kubeconfig'
// 版本标签
IMAGE_TAG = "BUILD_NUMBER-'unknown'"
// 通知配置
WEBHOOK_URL_ID = 'webhook-url'
// 静态资源安全扫描配置
STATIC_SCAN_ENABLED = 'true'
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
timestamps()
}
triggers {
// 手动触发(默认)
// 如需自动触发,取消下面注释:
// pollSCM('H/5 * * * *') // 每5分钟检查一次
// githubPush() // GitHub webhook 触发
// gitlab(triggerOnPush: true, triggerOnMergeRequest: true)
}
stages {
// ==================== 准备阶段 ====================
stage('Prepare') {
steps {
script {
echo "🔧 Build Preparation"
echo "Image Tag: env.IMAGE_TAG"
echo "Git Commit: env.GIT_COMMIT"
echo "Branch: env.GIT_BRANCH"
// 检查必要目录存在
sh '''
[ -d "backend" ] || echo "⚠️ Warning: backend directory not found"
[ -d "frontend" ] || echo "⚠️ Warning: frontend directory not found"
'''
}
}
}
// ==================== 代码检查阶段 ====================
stage('Lint') {
parallel {
stage('Lint Java') {
when {
anyOf {
changeset "backend/**"
expression { return !fileExists('frontend') }
}
}
agent {
docker {
image 'maven:3.9-eclipse-temurin-17-alpine'
args '-v maven-repo:/root/.m2 -e MAVEN_OPTS="-Xmx512m"'
reuseNode true
}
}
steps {
dir('backend') {
sh 'mvn spotbugs:check pmd:check checkstyle:check -q -f pom.xml || true'
}
}
post {
always {
// 使用通用的记录方式,不依赖特定插件
recordIssues(
enabledForFailure: true,
tools: [
spotBugs(pattern: 'backend/target/spotbugsXml.xml'),
pmdParser(pattern: 'backend/target/pmd.xml'),
checkStyle(pattern: 'backend/target/checkstyle-result.xml')
]
)
}
}
}
stage('Lint Vue') {
when {
anyOf {
changeset "frontend/**"
expression { return !fileExists('backend') }
}
}
agent {
docker {
image 'node:18-alpine'
args '-v node-modules:/app/node_modules -e NODE_OPTIONS="--max-old-space-size=512"'
reuseNode true
}
}
steps {
dir('frontend') {
sh 'npm ci --prefer-offline --no-audit'
sh 'npm run lint -- --max-warnings=50 || true'
}
}
}
}
}
// ==================== 单元测试阶段 ====================
stage('Test') {
parallel {
stage('Test Java') {
when {
anyOf {
changeset "backend/**"
expression { return !fileExists('frontend') }
}
}
agent {
docker {
image 'maven:3.9-eclipse-temurin-17-alpine'
args '-v maven-repo:/root/.m2 -e MAVEN_OPTS="-Xmx1024m"'
reuseNode true
}
}
steps {
dir('backend') {
sh 'mvn clean test -q'
}
}
post {
always {
// 使用标准 JUnit 插件(大多数 Jenkins 都安装)
junit(
testResults: 'backend/target/surefire-reports/*.xml',
allowEmptyResults: true,
skipPublishingChecks: true
)
// JaCoCo 覆盖率(如已安装插件)
script {
try {
jacoco(
execPattern: 'backend/target/jacoco.exec',
classPattern: 'backend/target/classes',
sourcePattern: 'backend/src/main/java',
minimumLineCoverage: '60',
maximumLineCoverage: '90'
)
} catch (err) {
echo "⚠️ JaCoCo plugin not available: err.message"
}
}
}
}
}
stage('Test Vue') {
when {
anyOf {
changeset "frontend/**"
expression { return !fileExists('backend') }
}
}
agent {
docker {
image 'node:18-alpine'
args '-v node-modules:/app/node_modules -e NODE_OPTIONS="--max-old-space-size=1024"'
reuseNode true
}
}
steps {
dir('frontend') {
sh 'npm ci --prefer-offline --no-audit'
sh 'npm run test:unit -- --coverage --reporter=junit --outputFile=junit.xml || true'
}
}
post {
always {
junit(
testResults: 'frontend/junit.xml',
allowEmptyResults: true,
skipPublishingChecks: true
)
}
}
}
}
}
// ==================== 构建打包阶段 ====================
stage('Build') {
parallel {
stage('Build Java') {
when {
anyOf {
changeset "backend/**"
expression { return !fileExists('frontend') }
}
}
agent {
docker {
image 'maven:3.9-eclipse-temurin-17-alpine'
args '-v maven-repo:/root/.m2 -e MAVEN_OPTS="-Xmx1024m"'
reuseNode true
}
}
steps {
dir('backend') {
sh 'mvn clean package -DskipTests -q'
sh "mkdir -p artifacts && cp target/*.jar artifacts/JAVA_PROJECT_NAME.jar"
// 静态资源安全检查:确保没有源码泄露
sh '''
echo "🔍 Checking Java artifacts for source files..."
if jar tf artifacts/*.jar | grep -E "\\.(vue|config\\.js|config\\.ts)$"; then
echo "❌ Error: Source files found in JAR!"
exit 1
fi
echo "✅ Java artifact check passed"
'''
}
}
post {
success {
archiveArtifacts(
artifacts: "artifacts/JAVA_PROJECT_NAME.jar",
fingerprint: true,
allowEmptyArchive: false
)
}
}
}
stage('Build Vue') {
when {
anyOf {
changeset "frontend/**"
expression { return !fileExists('backend') }
}
}
agent {
docker {
image 'node:18-alpine'
args '-v node-modules:/app/node_modules -e NODE_OPTIONS="--max-old-space-size=1024"'
reuseNode true
}
}
steps {
dir('frontend') {
sh 'npm ci --prefer-offline --no-audit'
sh 'npm run build'
sh "mkdir -p artifacts && cp -r dist artifacts/VUE_PROJECT_NAME"
// 静态资源安全检查:确保 dist 目录没有源码和配置文件
sh '''
echo "🔍 Scanning dist folder for source/config files..."
VIOLATIONS=$(find artifacts/VUE_PROJECT_NAME -type f \
-name "*.vue" -o \
-name "*.config.js" -o \
-name "*.config.ts" -o \
-name "*.config.mjs" -o \
-name "*.config.cjs" -o \
-name "*.config.json" -o \
-name "vite.config.*" -o \
-name "*.map" 2>/dev/null || true)
if [ -n "$VIOLATIONS" ]; then
echo "❌ Security violation found:"
echo "$VIOLATIONS"
echo "Removing violation files..."
echo "$VIOLATIONS" | xargs rm -f
fi
echo "✅ Vue build check passed"
echo "📦 Final artifact contents:"
find artifacts/VUE_PROJECT_NAME -type f | head -20
'''
}
}
post {
success {
archiveArtifacts(
artifacts: "artifacts/VUE_PROJECT_NAME/**/*",
fingerprint: true,
allowEmptyArchive: false
)
}
}
}
}
}
// ==================== 安全扫描阶段 ====================
stage('Security Scan') {
when {
expression { env.STATIC_SCAN_ENABLED == 'true' }
}
parallel {
stage('Scan Java Image') {
when {
expression { fileExists('backend/Dockerfile') }
}
steps {
script {
try {
// 使用 Trivy 扫描(如已安装)
sh '''
if command -v trivy >/dev/null 2>&1; then
echo "🔍 Scanning Java Dockerfile..."
trivy filesystem --severity HIGH,CRITICAL backend/ || true
else
echo "⚠️ Trivy not installed, skipping scan"
fi
'''
} catch (err) {
echo "⚠️ Security scan warning: err.message"
}
}
}
}
stage('Scan Vue Image') {
when {
expression { fileExists('frontend/Dockerfile') }
}
steps {
script {
try {
sh '''
if command -v trivy >/dev/null 2>&1; then
echo "🔍 Scanning Vue Dockerfile..."
trivy filesystem --severity HIGH,CRITICAL frontend/ || true
else
echo "⚠️ Trivy not installed, skipping scan"
fi
'''
} catch (err) {
echo "⚠️ Security scan warning: err.message"
}
}
}
}
}
}
// ==================== 镜像构建阶段 ====================
stage('Dockerize') {
parallel {
stage('Dockerize Java') {
when {
expression { fileExists('backend/Dockerfile') }
}
steps {
script {
docker.withRegistry("https://DOCKER_REGISTRY", "DOCKER_CREDENTIALS_ID") {
def javaImage = docker.build(
"DOCKER_REGISTRY/DOCKER_NAMESPACE/JAVA_PROJECT_NAME:IMAGE_TAG",
"-f backend/Dockerfile backend/"
)
javaImage.push()
// 仅在 main/master 分支推送 latest
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'master') {
javaImage.push('latest')
}
}
}
}
}
stage('Dockerize Vue') {
when {
expression { fileExists('frontend/Dockerfile') }
}
steps {
script {
docker.withRegistry("https://DOCKER_REGISTRY", "DOCKER_CREDENTIALS_ID") {
def vueImage = docker.build(
"DOCKER_REGISTRY/DOCKER_NAMESPACE/VUE_PROJECT_NAME:IMAGE_TAG",
"-f frontend/Dockerfile frontend/"
)
vueImage.push()
// 仅在 main/master 分支推送 latest
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'master') {
vueImage.push('latest')
}
}
}
}
}
}
}
// ==================== K8s 部署阶段 ====================
stage('Deploy to K8s') {
when {
anyOf {
branch 'main'
branch 'master'
expression { params.FORCE_DEPLOY == true }
}
}
steps {
script {
withCredentials([file(credentialsId: "KUBE_CONFIG_ID", variable: 'KUBECONFIG')]) {
sh """
# 验证 kubectl 连接
kubectl cluster-info
# 部署 Java 服务
if kubectl get deployment JAVA_PROJECT_NAME -n K8S_NAMESPACE >/dev/null 2>&1; then
echo "🚀 Updating Java deployment..."
kubectl set image deployment/JAVA_PROJECT_NAME \
JAVA_PROJECT_NAME=DOCKER_REGISTRY/DOCKER_NAMESPACE/JAVA_PROJECT_NAME:IMAGE_TAG \
-n K8S_NAMESPACE
kubectl rollout status deployment/JAVA_PROJECT_NAME -n K8S_NAMESPACE --timeout=300s
else
echo "⚠️ Java deployment not found, skipping"
fi
# 部署 Vue 服务
if kubectl get deployment VUE_PROJECT_NAME -n K8S_NAMESPACE >/dev/null 2>&1; then
echo "🚀 Updating Vue deployment..."
kubectl set image deployment/VUE_PROJECT_NAME \
VUE_PROJECT_NAME=DOCKER_REGISTRY/DOCKER_NAMESPACE/VUE_PROJECT_NAME:IMAGE_TAG \
-n K8S_NAMESPACE
kubectl rollout status deployment/VUE_PROJECT_NAME -n K8S_NAMESPACE --timeout=300s
else
echo "⚠️ Vue deployment not found, skipping"
fi
# 验证部署状态
echo "📋 Deployment status:"
kubectl get pods -n K8S_NAMESPACE -l app=JAVA_PROJECT_NAME -o wide || true
kubectl get pods -n K8S_NAMESPACE -l app=VUE_PROJECT_NAME -o wide || true
"""
}
}
}
}
}
// ==================== 通知反馈阶段 ====================
post {
always {
script {
// 清理工作区(可选)
// cleanWs()
}
}
success {
script {
sendNotification('✅ 部署成功', 'good')
}
}
failure {
script {
sendNotification('❌ 部署失败', 'danger')
}
}
unstable {
script {
sendNotification('⚠️ 部署不稳定(测试未通过)', 'warning')
}
}
aborted {
script {
sendNotification('🛑 构建已取消', '#808080')
}
}
}
}
// ==================== 辅助函数 ====================
/**
* 发送通知到飞书/钉钉/Slack
*/
def sendNotification(String status, String color) {
script {
try {
withCredentials([string(credentialsId: env.WEBHOOK_URL_ID, variable: 'WEBHOOK_URL')]) {
def message = """
{
"msg_type": "interactive",
"card": {
"config": {"wide_screen_mode": true},
"header": {
"title": {
"tag": "plain_text",
"content": "status"
},
"template": "color == 'danger' ? 'red' : 'orange'"
},
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**项目:** env.JOB_NAME\\n**构建:** #env.BUILD_NUMBER\\n**版本:** env.IMAGE_TAG\\n**提交:** 'N/A'\\n**分支:** 'N/A'\\n**构建人:** 'System'"
}
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"tag": "plain_text", "content": "查看详情"},
"url": "env.BUILD_URL",
"type": "primary"
}
]
}
]
}
}
"""
httpRequest(
httpMode: 'POST',
contentType: 'APPLICATION_JSON',
url: env.WEBHOOK_URL,
requestBody: message,
validResponseCodes: '200:299'
)
}
} catch (err) {
echo "⚠️ Failed to send notification: err.message"
}
}
}
FILE:assets/k8s-deployment.yml
# ============================================
# Kubernetes 部署配置
# ============================================
---
# Java 后端 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-backend
namespace: production
labels:
app: java-backend
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: java-backend
template:
metadata:
labels:
app: java-backend
spec:
containers:
- name: java-backend
image: your-registry.com/your-namespace/java-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
---
# Java 后端 Service
apiVersion: v1
kind: Service
metadata:
name: java-backend
namespace: production
labels:
app: java-backend
spec:
type: ClusterIP
selector:
app: java-backend
ports:
- port: 8080
targetPort: 8080
name: http
---
# Vue 前端 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: vue-frontend
namespace: production
labels:
app: vue-frontend
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: vue-frontend
template:
metadata:
labels:
app: vue-frontend
spec:
containers:
- name: vue-frontend
image: your-registry.com/your-namespace/vue-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
---
# Vue 前端 Service
apiVersion: v1
kind: Service
metadata:
name: vue-frontend
namespace: production
labels:
app: vue-frontend
spec:
type: ClusterIP
selector:
app: vue-frontend
ports:
- port: 80
targetPort: 80
name: http
---
# Ingress 配置 (可选)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- your-app.com
- api.your-app.com
secretName: app-tls-secret
rules:
- host: your-app.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vue-frontend
port:
number: 80
- host: api.your-app.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: java-backend
port:
number: 8080
---
# HPA 自动扩缩容 (可选)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: java-backend-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: java-backend
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vue-frontend-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vue-frontend
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
FILE:assets/nginx.conf.txt
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# ============================================
# 安全加固: 拒绝访问源码和配置文件
# ============================================
# 拒绝访问 .vue 源码文件
location ~* \.vue$ {
return 404;
}
# 拒绝访问各种配置文件
location ~* (config|vite|webpack|babel|tailwind|postcss|eslint|prettier)\.config\.(js|ts|mjs|cjs|json)$ {
return 404;
}
# 拒绝访问 ESLint/Prettier 配置
location ~* \.(eslintrc|prettierrc)\.(js|json|yaml|yml)$ {
return 404;
}
# 拒绝访问 source map 文件
location ~* \.map$ {
return 404;
}
# 拒绝访问环境配置文件
location ~* \.env(\.local|\.development|\.production)?$ {
return 404;
}
# 拒绝访问 lock 文件
location ~* (package-lock|yarn|pnpm-lock)\.json$ {
return 404;
}
# ============================================
# 前端路由支持 (Vue Router history 模式)
# ============================================
location / {
try_files $uri $uri/ /index.html;
}
# ============================================
# 静态资源缓存 - 只缓存安全文件类型
# ============================================
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff;
access_log off;
}
# 特殊处理: 确保 .js 文件不是配置文件
location ~* config\.(js|ts|mjs|cjs)$ {
return 404;
}
# ============================================
# API 代理到后端
# ============================================
location /api/ {
proxy_pass http://java-backend:8080/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# ============================================
# 健康检查端点
# ============================================
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}