Add executable business blueprint workflow
This commit is contained in:
@@ -124,7 +124,7 @@ CREATE DATABASE ry DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
mysql -uroot -p ry < sql/db.sql
|
||||
```
|
||||
|
||||
`sql/db.sql` 已整合若依基础表、Quartz 调度表、代码生成平台扩展表、前台工作台表、初始化数据、代码生成平台菜单和按钮权限。分散脚本仍可作为增量或排查参考:
|
||||
`sql/db.sql` 已整合若依基础表、Quartz 调度表、代码生成平台扩展表、前台工作台表、源码商城表、业务蓝图模板、初始化数据、代码生成平台菜单和按钮权限。新库只需要导入这一份脚本;分散脚本保留作旧库增量、模板重放或排查参考:
|
||||
|
||||
1. `sql/ry_20240629.sql`:若依基础表与基础数据。
|
||||
2. `sql/quartz.sql`:Quartz 调度表。
|
||||
@@ -134,7 +134,11 @@ mysql -uroot -p ry < sql/db.sql
|
||||
6. `sql/sys_project_structure.sql`:项目结构节点表与初始化结构。
|
||||
7. `sql/add_create_table_sql_field.sql`:给 `gen_table` 增加建表 SQL 字段。
|
||||
8. `ruoyi-generator/sql/template_file_update.sql`:模板文件表增量字段。
|
||||
9. `sql/front_workbench.sql`:前台用户、项目草稿、表结构、生成记录和预览文件表。
|
||||
9. `sql/source_store.sql`:源码商城表、源码分类字典与示例数据。
|
||||
10. `sql/front_workbench.sql`:前台用户、项目草稿、表结构、生成记录和预览文件表。
|
||||
11. `sql/front_requirement_keyword.sql`、`sql/front_app_blueprint.sql`、`sql/front_er_diagram.sql`、`sql/front_project_upgrade.sql`:旧版前台项目草稿表升级到当前字段结构。
|
||||
12. `sql/business_blueprint_templates.sql`:业务蓝图后端、前端和结构模板种子。
|
||||
13. `sql/generator_test_data.sql`:生成器演示/测试数据,默认不并入生产初始化脚本。
|
||||
|
||||
### 3. 配置后端
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# Executable Business Blueprint Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Generate runnable workflow, status, insert, update, and numeric inventory/amount business code from business blueprints.
|
||||
|
||||
**Architecture:** Extend the current safe DSL instead of letting AI write Java. The backend validates `ruleChecks` and expanded `effects` against saved schema, then Velocity templates render deterministic Java/Vue code.
|
||||
|
||||
**Tech Stack:** Java 8, Spring Boot, MyBatis, Fastjson2/Jackson, Velocity, JUnit 4, Mockito, Vue.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Extend Blueprint DTO And AI Normalization
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionRuleDesign.java`
|
||||
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java`
|
||||
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java`
|
||||
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java`
|
||||
- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java`
|
||||
|
||||
- [ ] Add a failing test proving `ruleChecks`, `INSERT_ROW`, and `DECREASE_NUMBER` parse and persist.
|
||||
- [ ] Run `mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest test` and verify RED.
|
||||
- [ ] Add DTO fields and normalization helpers.
|
||||
- [ ] Re-run the targeted test and verify GREEN.
|
||||
|
||||
### Task 2: Validate Executable DSL Safely
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java`
|
||||
- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java`
|
||||
|
||||
- [ ] Add tests for unknown rule/effect fields and unsafe values.
|
||||
- [ ] Run the targeted test and verify RED.
|
||||
- [ ] Validate action tables, rule tables/fields, effect tables/fields, map keys, placeholders, and numeric amounts.
|
||||
- [ ] Re-run tests and verify GREEN.
|
||||
|
||||
### Task 3: Render Full Runnable Workflow Helpers
|
||||
|
||||
**Files:**
|
||||
- Modify: `sql/business_blueprint_templates.sql`
|
||||
- Modify: `sql/db.sql`
|
||||
- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java`
|
||||
|
||||
- [ ] Add a failing template test for `assertExists`, `insertRow`, `updateFields`, `setStatus`, `increaseNumberField`, and `decreaseNumberField`.
|
||||
- [ ] Run the targeted test and verify RED.
|
||||
- [ ] Extend the backend Velocity template to render rule checks and executable effects.
|
||||
- [ ] Sync the template block into `sql/db.sql`.
|
||||
- [ ] Re-run template tests and verify GREEN.
|
||||
|
||||
### Task 4: Frontend Display Compatibility
|
||||
|
||||
**Files:**
|
||||
- Modify: `easycode-web/src/views/GenerateView.vue`
|
||||
|
||||
- [ ] Render structured `ruleChecks` and expanded effect details without breaking old string `rules`.
|
||||
- [ ] Run existing frontend tests if available.
|
||||
|
||||
### Task 5: Full Verification
|
||||
|
||||
- [ ] Run `mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest,GenProjectServiceImplTest test`.
|
||||
- [ ] Run any impacted frontend tests.
|
||||
- [ ] Inspect generated template snippets for one sample action.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Executable Business Blueprint Design
|
||||
|
||||
## Goal
|
||||
|
||||
Generate runnable business workflow code from EasyCode business blueprints without allowing AI to emit arbitrary Java source.
|
||||
|
||||
## Scope
|
||||
|
||||
This phase covers general workflow actions plus inventory and amount style numeric changes. Generated projects should get Controller endpoints, transactional Service methods, rule checks, row inserts, field updates, status updates, numeric increase/decrease helpers, and frontend buttons/API calls.
|
||||
|
||||
This phase does not cover arbitrary Java snippets, complex accounting ledgers, permissions beyond the existing generated shell, distributed locks, or idempotency keys.
|
||||
|
||||
## Architecture
|
||||
|
||||
AI returns a structured DSL in `businessActions`. The backend normalizes and validates that DSL against the saved database schema, stores it in `front_project.business_blueprint`, and passes it into Velocity templates. Templates render deterministic Java and Vue code from the DSL.
|
||||
|
||||
Existing `rules: List<String>` remains as human-readable notes. New executable checks live in `ruleChecks`.
|
||||
|
||||
## DSL
|
||||
|
||||
Each action may include:
|
||||
|
||||
- `code`, `name`, `ownerTable`, `method`, `path`, `transaction`, `requestFields`
|
||||
- `rules`: human-readable notes
|
||||
- `ruleChecks`: executable checks
|
||||
- `effects`: executable data mutations
|
||||
|
||||
Supported `ruleChecks`:
|
||||
|
||||
- `EXISTS`
|
||||
- `NOT_EXISTS`
|
||||
- `FIELD_EQUALS`
|
||||
- `FIELD_NOT_EQUALS`
|
||||
- `FIELD_IN`
|
||||
- `NUMBER_GTE`
|
||||
|
||||
Supported `effects`:
|
||||
|
||||
- `INSERT_ROW`
|
||||
- `UPDATE_FIELDS`
|
||||
- `SET_STATUS`
|
||||
- `INCREASE_NUMBER`
|
||||
- `DECREASE_NUMBER`
|
||||
- legacy `UPDATE_FIELD`
|
||||
|
||||
## Safety
|
||||
|
||||
All table and column identifiers must exist in the saved schema. Effect value placeholders are limited to safe forms: literals, `${param.field}`, and `${now}`. Numeric amounts may be literals or `${param.field}`. SQL fragments from AI are not accepted except the existing legacy `UPDATE_FIELD.value`, which remains bounded by the template helper.
|
||||
|
||||
## Testing
|
||||
|
||||
Backend tests cover parsing and persistence of structured checks/effects, validation of unknown tables or fields, and template rendering markers for the generated helper methods. Existing tests must remain green.
|
||||
@@ -27,6 +27,14 @@ export function getProject(projectId) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function updateProject(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}`,
|
||||
method: 'put',
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function deleteProject(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}`,
|
||||
@@ -34,6 +42,13 @@ export function deleteProject(projectId) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function listAvailableModules() {
|
||||
return request({
|
||||
url: '/front/project/options/modules',
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateDatabase(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generate-database`,
|
||||
@@ -52,6 +67,15 @@ export function generateAppBlueprint(projectId, data) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateBusinessBlueprint(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generate-business-blueprint`,
|
||||
method: 'post',
|
||||
timeout: GENERATE_REQUEST_TIMEOUT,
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getDatabase(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/database`,
|
||||
|
||||
327
RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue
Normal file
327
RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<div class="app-module-diagram">
|
||||
<el-empty v-if="!graph.nodes.length" description="生成应用蓝图后可查看系统功能模块图" />
|
||||
<template v-else>
|
||||
<div class="module-toolbar">
|
||||
<div class="module-summary">
|
||||
<span>节点:{{ graph.nodes.length }}</span>
|
||||
<span>连线:{{ graph.edges.length }}</span>
|
||||
</div>
|
||||
<el-button :icon="Picture" @click="exportPng">导出图片</el-button>
|
||||
</div>
|
||||
|
||||
<div class="module-scroll">
|
||||
<svg ref="svgRef" class="module-canvas" :viewBox="`0 0 ${canvas.width} ${canvas.height}`" :style="canvasStyle">
|
||||
<defs>
|
||||
<marker
|
||||
id="app-module-arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="8"
|
||||
refY="3"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M0,0 L0,6 L8,3 z" fill="#8aa0bc" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<g class="lane-layer">
|
||||
<g v-for="lane in graph.lanes" :key="lane.key">
|
||||
<rect class="lane-bg" :x="lane.x - 16" y="22" :width="lane.width + 32" :height="canvas.height - 44" rx="10" />
|
||||
<text class="lane-title" :x="lane.x" y="46">{{ lane.title }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g class="edge-layer">
|
||||
<g v-for="edge in edgeViews" :key="edge.id">
|
||||
<path class="module-edge" :d="edge.path" marker-end="url(#app-module-arrow)" />
|
||||
<text class="edge-label" :x="edge.labelX" :y="edge.labelY">{{ edge.label }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g class="node-layer">
|
||||
<g
|
||||
v-for="node in graph.nodes"
|
||||
:key="node.id"
|
||||
class="module-node"
|
||||
:class="[node.kind, node.lane]"
|
||||
:transform="`translate(${node.x}, ${node.y})`"
|
||||
>
|
||||
<rect class="module-node-box" :width="node.width" :height="node.height" rx="8" />
|
||||
<rect class="module-node-head" :width="node.width" height="52" rx="8" />
|
||||
<rect class="module-node-head-fill" y="44" :width="node.width" height="8" />
|
||||
<text class="node-title" x="14" y="27">{{ shortText(node.title, 18) }}</text>
|
||||
<text class="node-subtitle" x="14" y="46">{{ shortText(node.subtitle, 28) }}</text>
|
||||
<g v-for="(badge, badgeIndex) in node.badges" :key="badge" :transform="`translate(${badgeX(node, badgeIndex)}, 72)`">
|
||||
<rect class="node-badge-bg" :width="badgeWidth(badge)" height="22" rx="5" />
|
||||
<text class="node-badge-text" :x="badgeWidth(badge) / 2" y="15">{{ shortText(badge, 14) }}</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { buildAppModuleGraph } from '@/utils/appModuleGraph'
|
||||
import { saveBlob } from '@/utils/download'
|
||||
|
||||
const props = defineProps({
|
||||
blueprint: {
|
||||
type: Object,
|
||||
default: () => ({ roles: [], frontendMenus: [], adminMenus: [] })
|
||||
}
|
||||
})
|
||||
|
||||
const svgRef = ref(null)
|
||||
const graph = computed(() => buildAppModuleGraph(props.blueprint))
|
||||
|
||||
const nodeMap = computed(() => {
|
||||
const map = new Map()
|
||||
graph.value.nodes.forEach((node) => map.set(node.id, node))
|
||||
return map
|
||||
})
|
||||
|
||||
const canvas = computed(() => {
|
||||
const nodes = graph.value.nodes
|
||||
const lanes = graph.value.lanes || []
|
||||
const width = Math.max(980, ...lanes.map((lane) => lane.x + lane.width + 48), ...nodes.map((node) => node.x + node.width + 48))
|
||||
const height = Math.max(430, ...nodes.map((node) => node.y + node.height + 48))
|
||||
return { width, height }
|
||||
})
|
||||
|
||||
const canvasStyle = computed(() => ({
|
||||
width: `${canvas.value.width}px`,
|
||||
height: `${canvas.value.height}px`
|
||||
}))
|
||||
|
||||
const edgeViews = computed(() => graph.value.edges
|
||||
.map((edge) => {
|
||||
const source = nodeMap.value.get(edge.source)
|
||||
const target = nodeMap.value.get(edge.target)
|
||||
if (!source || !target) return null
|
||||
|
||||
const sourceX = source.x + source.width
|
||||
const sourceY = source.y + source.height / 2
|
||||
const targetX = target.x
|
||||
const targetY = target.y + target.height / 2
|
||||
const curve = Math.max(64, Math.abs(targetX - sourceX) / 2)
|
||||
|
||||
return {
|
||||
...edge,
|
||||
path: `M ${sourceX} ${sourceY} C ${sourceX + curve} ${sourceY}, ${targetX - curve} ${targetY}, ${targetX} ${targetY}`,
|
||||
labelX: (sourceX + targetX) / 2,
|
||||
labelY: (sourceY + targetY) / 2 - 9
|
||||
}
|
||||
})
|
||||
.filter(Boolean))
|
||||
|
||||
function shortText(value, maxLength) {
|
||||
const text = String(value ?? '')
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text
|
||||
}
|
||||
|
||||
function badgeWidth(value) {
|
||||
const text = String(value ?? '')
|
||||
return Math.min(112, Math.max(48, text.length * 8 + 20))
|
||||
}
|
||||
|
||||
function badgeX(node, index) {
|
||||
return node.badges.slice(0, index).reduce((x, badge) => x + badgeWidth(badge) + 6, 14)
|
||||
}
|
||||
|
||||
function exportStyle() {
|
||||
return `
|
||||
.lane-bg{fill:#f8fafc;stroke:#e2e8f0;stroke-width:1}
|
||||
.lane-title{fill:#475569;font-family:Arial,sans-serif;font-size:13px;font-weight:700}
|
||||
.module-edge{fill:none;stroke:#8aa0bc;stroke-width:1.7}
|
||||
.edge-label{fill:#64748b;font-family:Arial,sans-serif;font-size:12px;paint-order:stroke;stroke:#f8fafc;stroke-width:4px;text-anchor:middle}
|
||||
.module-node-box{fill:#fff;stroke:#d6e0ee;stroke-width:1}
|
||||
.module-node-head{fill:#eef5ff}
|
||||
.module-node-head-fill{fill:#eef5ff}
|
||||
.module-node.access .module-node-head,.module-node.access .module-node-head-fill{fill:#ecfdf5}
|
||||
.module-node.role .module-node-head,.module-node.role .module-node-head-fill{fill:#eff6ff}
|
||||
.module-node.frontend .module-node-head,.module-node.frontend .module-node-head-fill{fill:#f0fdf4}
|
||||
.module-node.admin .module-node-head,.module-node.admin .module-node-head-fill{fill:#fff7ed}
|
||||
.node-title{fill:#0f172a;font-family:Arial,sans-serif;font-size:15px;font-weight:700}
|
||||
.node-subtitle{fill:#64748b;font-family:Arial,sans-serif;font-size:12px}
|
||||
.node-badge-bg{fill:#f8fafc;stroke:#e2e8f0;stroke-width:1}
|
||||
.node-badge-text{fill:#475569;font-family:Arial,sans-serif;font-size:11px;font-weight:700;text-anchor:middle}`
|
||||
}
|
||||
|
||||
async function exportPng() {
|
||||
if (!svgRef.value) return
|
||||
|
||||
const svg = svgRef.value.cloneNode(true)
|
||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
||||
style.textContent = exportStyle()
|
||||
svg.insertBefore(style, svg.firstChild)
|
||||
const svgText = `<?xml version="1.0" encoding="UTF-8"?>${svg.outerHTML}`
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' })
|
||||
const url = URL.createObjectURL(svgBlob)
|
||||
const image = new Image()
|
||||
image.decoding = 'async'
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = resolve
|
||||
image.onerror = reject
|
||||
image.src = url
|
||||
})
|
||||
const canvasElement = document.createElement('canvas')
|
||||
canvasElement.width = canvas.value.width
|
||||
canvasElement.height = canvas.value.height
|
||||
const context = canvasElement.getContext('2d')
|
||||
context.fillStyle = '#f8fafc'
|
||||
context.fillRect(0, 0, canvasElement.width, canvasElement.height)
|
||||
context.drawImage(image, 0, 0)
|
||||
const blob = await new Promise((resolve) => canvasElement.toBlob(resolve, 'image/png'))
|
||||
if (blob) {
|
||||
saveBlob(blob, 'app-module-diagram.png')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.warning('导出图片失败,请稍后重试')
|
||||
} finally {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-module-diagram {
|
||||
background: #f8fafc;
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.module-toolbar {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.module-summary {
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.module-scroll {
|
||||
min-height: 430px;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.module-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lane-bg {
|
||||
fill: #f8fafc;
|
||||
stroke: #e2e8f0;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.lane-title {
|
||||
fill: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.module-edge {
|
||||
fill: none;
|
||||
stroke: #8aa0bc;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.edge-label {
|
||||
fill: #64748b;
|
||||
font-size: 12px;
|
||||
paint-order: stroke;
|
||||
stroke: #f8fafc;
|
||||
stroke-width: 4px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.module-node-box {
|
||||
fill: #ffffff;
|
||||
filter: drop-shadow(0 8px 18px rgba(15, 23, 42, 0.08));
|
||||
stroke: #d6e0ee;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #eef5ff;
|
||||
}
|
||||
|
||||
.module-node.access {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.module-node.role {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #eff6ff;
|
||||
}
|
||||
}
|
||||
|
||||
.module-node.frontend {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.module-node.admin {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #fff7ed;
|
||||
}
|
||||
}
|
||||
|
||||
.node-title {
|
||||
fill: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
fill: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-badge-bg {
|
||||
fill: #f8fafc;
|
||||
stroke: #e2e8f0;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.node-badge-text {
|
||||
fill: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.module-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js
Normal file
200
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const SOURCE_X = 32
|
||||
const FRONTEND_X = 360
|
||||
const ADMIN_X = 688
|
||||
const START_Y = 72
|
||||
const NODE_WIDTH = 252
|
||||
const NODE_HEIGHT = 108
|
||||
const NODE_GAP_Y = 24
|
||||
|
||||
function cleanText(value, fallback = '') {
|
||||
const text = String(value ?? '').trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function flagOn(value) {
|
||||
return value === true || value === 1 || value === '1' || value === 'true'
|
||||
}
|
||||
|
||||
function normalizeKey(value, fallback) {
|
||||
const text = cleanText(value, fallback)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]+/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function uniqueKey(value, fallbackPrefix, index, used) {
|
||||
const base = normalizeKey(value, `${fallbackPrefix}_${index + 1}`)
|
||||
let candidate = base
|
||||
let suffix = 2
|
||||
while (used.has(candidate)) {
|
||||
candidate = `${base}_${suffix}`
|
||||
suffix += 1
|
||||
}
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
}
|
||||
|
||||
function normalizeRoles(roles) {
|
||||
const used = new Set()
|
||||
return (Array.isArray(roles) ? roles : []).filter(Boolean).map((role, index) => {
|
||||
const code = uniqueKey(role.code, 'role', index, used)
|
||||
return {
|
||||
code,
|
||||
id: `role:${code}`,
|
||||
title: cleanText(role.name, code),
|
||||
subtitle: cleanText(role.description, '角色'),
|
||||
kind: 'role',
|
||||
lane: 'role',
|
||||
badges: ['角色']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeMenus(menus, lane, fallbackPrefix) {
|
||||
const used = new Set()
|
||||
return (Array.isArray(menus) ? menus : []).filter(Boolean).map((menu, index) => {
|
||||
const code = uniqueKey(menu.code, fallbackPrefix, index, used)
|
||||
const requiresLogin = flagOn(menu.requiresLogin)
|
||||
const dataScope = cleanText(menu.dataScope, requiresLogin ? 'CURRENT_USER' : 'PUBLIC').toUpperCase()
|
||||
const visibleRoles = Array.isArray(menu.visibleRoles)
|
||||
? menu.visibleRoles.map((role) => normalizeKey(role, '')).filter(Boolean)
|
||||
: []
|
||||
|
||||
return {
|
||||
code,
|
||||
id: `${lane}:${code}`,
|
||||
title: cleanText(menu.name, code),
|
||||
subtitle: cleanText(menu.path, '/'),
|
||||
kind: 'menu',
|
||||
lane,
|
||||
requiresLogin,
|
||||
visibleRoles,
|
||||
dataScope,
|
||||
badges: [
|
||||
lane === 'frontend' ? '前台' : '后台',
|
||||
requiresLogin ? '需登录' : '公开',
|
||||
dataScope
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function shouldUsePublic(menu) {
|
||||
return !menu.requiresLogin || menu.dataScope === 'PUBLIC'
|
||||
}
|
||||
|
||||
function needsPublicAccess(menus) {
|
||||
return menus.some(shouldUsePublic)
|
||||
}
|
||||
|
||||
function needsLoggedInAccess(menus, roleIds) {
|
||||
return menus.some((menu) => {
|
||||
if (!menu.requiresLogin) return false
|
||||
return !menu.visibleRoles.some((roleCode) => roleIds.has(`role:${roleCode}`))
|
||||
})
|
||||
}
|
||||
|
||||
function buildAccessNodes(menus, roleIds) {
|
||||
const nodes = []
|
||||
if (needsPublicAccess(menus)) {
|
||||
nodes.push({
|
||||
code: 'public',
|
||||
id: 'access:public',
|
||||
title: '公开访问',
|
||||
subtitle: '无需登录',
|
||||
kind: 'access',
|
||||
lane: 'access',
|
||||
badges: ['PUBLIC']
|
||||
})
|
||||
}
|
||||
if (needsLoggedInAccess(menus, roleIds)) {
|
||||
nodes.push({
|
||||
code: 'logged_in',
|
||||
id: 'access:logged_in',
|
||||
title: '登录用户',
|
||||
subtitle: '未限定角色',
|
||||
kind: 'access',
|
||||
lane: 'access',
|
||||
badges: ['LOGIN']
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
function applyLayout(nodes) {
|
||||
const laneIndexes = new Map()
|
||||
return nodes.map((node) => {
|
||||
const layoutLane = node.lane === 'admin' ? 'admin' : node.lane === 'frontend' ? 'frontend' : 'source'
|
||||
const index = laneIndexes.get(layoutLane) || 0
|
||||
laneIndexes.set(layoutLane, index + 1)
|
||||
const x = layoutLane === 'admin' ? ADMIN_X : layoutLane === 'frontend' ? FRONTEND_X : SOURCE_X
|
||||
|
||||
return {
|
||||
...node,
|
||||
x,
|
||||
y: START_Y + index * (NODE_HEIGHT + NODE_GAP_Y),
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function edgeLabel(menu) {
|
||||
return menu.dataScope || (menu.requiresLogin ? 'LOGIN' : 'PUBLIC')
|
||||
}
|
||||
|
||||
function edgeId(source, target) {
|
||||
return `${source}->${target}`
|
||||
}
|
||||
|
||||
function buildEdges(menus, roleIds) {
|
||||
const edges = []
|
||||
menus.forEach((menu) => {
|
||||
const validRoleIds = menu.visibleRoles
|
||||
.map((roleCode) => `role:${roleCode}`)
|
||||
.filter((roleId) => roleIds.has(roleId))
|
||||
|
||||
if (validRoleIds.length) {
|
||||
validRoleIds.forEach((roleId) => {
|
||||
edges.push({
|
||||
id: edgeId(roleId, menu.id),
|
||||
source: roleId,
|
||||
target: menu.id,
|
||||
label: edgeLabel(menu)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const source = shouldUsePublic(menu) ? 'access:public' : 'access:logged_in'
|
||||
edges.push({
|
||||
id: edgeId(source, menu.id),
|
||||
source,
|
||||
target: menu.id,
|
||||
label: edgeLabel(menu)
|
||||
})
|
||||
})
|
||||
return edges
|
||||
}
|
||||
|
||||
export function buildAppModuleGraph(blueprint = {}) {
|
||||
const roles = normalizeRoles(blueprint.roles)
|
||||
const frontendMenus = normalizeMenus(blueprint.frontendMenus, 'frontend', 'frontend_menu')
|
||||
const adminMenus = normalizeMenus(blueprint.adminMenus, 'admin', 'admin_menu')
|
||||
const menus = [...frontendMenus, ...adminMenus]
|
||||
const roleIds = new Set(roles.map((role) => role.id))
|
||||
const accessNodes = buildAccessNodes(menus, roleIds)
|
||||
const nodes = applyLayout([...accessNodes, ...roles, ...frontendMenus, ...adminMenus])
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges: buildEdges(menus, roleIds),
|
||||
lanes: [
|
||||
{ key: 'source', title: '访问角色', x: SOURCE_X, width: NODE_WIDTH },
|
||||
{ key: 'frontend', title: '前台功能模块', x: FRONTEND_X, width: NODE_WIDTH },
|
||||
{ key: 'admin', title: '后台功能模块', x: ADMIN_X, width: NODE_WIDTH }
|
||||
]
|
||||
}
|
||||
}
|
||||
82
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs
Normal file
82
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildAppModuleGraph } from './appModuleGraph.js'
|
||||
|
||||
test('buildAppModuleGraph creates role and menu module nodes with access edges', () => {
|
||||
const graph = buildAppModuleGraph({
|
||||
roles: [
|
||||
{ code: 'reader', name: '读者', description: '前台用户' },
|
||||
{ code: 'admin', name: '管理员', description: '后台运营' }
|
||||
],
|
||||
frontendMenus: [
|
||||
{
|
||||
code: 'catalog',
|
||||
name: '图书目录',
|
||||
path: '/catalog',
|
||||
requiresLogin: false,
|
||||
visibleRoles: [],
|
||||
dataScope: 'PUBLIC'
|
||||
},
|
||||
{
|
||||
code: 'my_borrows',
|
||||
name: '我的借阅',
|
||||
path: '/my-borrows',
|
||||
requiresLogin: true,
|
||||
visibleRoles: ['reader'],
|
||||
dataScope: 'CURRENT_USER'
|
||||
}
|
||||
],
|
||||
adminMenus: [
|
||||
{
|
||||
code: 'book_admin',
|
||||
name: '图书管理',
|
||||
path: '/admin/books',
|
||||
requiresLogin: true,
|
||||
visibleRoles: ['admin'],
|
||||
dataScope: 'ROLE'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
graph.nodes.map((node) => ({ id: node.id, title: node.title, kind: node.kind, lane: node.lane })),
|
||||
[
|
||||
{ id: 'access:public', title: '公开访问', kind: 'access', lane: 'access' },
|
||||
{ id: 'role:reader', title: '读者', kind: 'role', lane: 'role' },
|
||||
{ id: 'role:admin', title: '管理员', kind: 'role', lane: 'role' },
|
||||
{ id: 'frontend:catalog', title: '图书目录', kind: 'menu', lane: 'frontend' },
|
||||
{ id: 'frontend:my_borrows', title: '我的借阅', kind: 'menu', lane: 'frontend' },
|
||||
{ id: 'admin:book_admin', title: '图书管理', kind: 'menu', lane: 'admin' }
|
||||
]
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
graph.edges.map((edge) => ({ source: edge.source, target: edge.target, label: edge.label })),
|
||||
[
|
||||
{ source: 'access:public', target: 'frontend:catalog', label: 'PUBLIC' },
|
||||
{ source: 'role:reader', target: 'frontend:my_borrows', label: 'CURRENT_USER' },
|
||||
{ source: 'role:admin', target: 'admin:book_admin', label: 'ROLE' }
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('buildAppModuleGraph creates a logged-in access node for menus without explicit visible roles', () => {
|
||||
const graph = buildAppModuleGraph({
|
||||
roles: [],
|
||||
frontendMenus: [
|
||||
{
|
||||
code: 'profile',
|
||||
name: '个人中心',
|
||||
path: '/profile',
|
||||
requiresLogin: true,
|
||||
visibleRoles: [],
|
||||
dataScope: 'CURRENT_USER'
|
||||
}
|
||||
],
|
||||
adminMenus: []
|
||||
})
|
||||
|
||||
assert.equal(graph.nodes[0].id, 'access:logged_in')
|
||||
assert.equal(graph.edges[0].source, 'access:logged_in')
|
||||
assert.equal(graph.edges[0].target, 'frontend:profile')
|
||||
})
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h1 class="panel-title">项目生成工作台</h1>
|
||||
<p class="muted">先生成数据库结构,确认后再进入项目预览。</p>
|
||||
<p class="muted">先生成应用蓝图,再生成数据库结构,保存修改后生成业务蓝图。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,39 @@
|
||||
placeholder="例如字段偏好、模块边界、业务规则"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="8">
|
||||
<el-form-item label="项目形态">
|
||||
<el-switch
|
||||
v-model="projectForm.frontendEnabled"
|
||||
active-text="生成用户前台"
|
||||
inactive-text="仅后台管理"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="16">
|
||||
<el-form-item label="后台功能模块">
|
||||
<el-select
|
||||
v-model="projectForm.moduleIds"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择后台维护的功能模块"
|
||||
class="module-select"
|
||||
:loading="loadingModules"
|
||||
>
|
||||
<el-option
|
||||
v-for="module in availableModules"
|
||||
:key="module.moduleId"
|
||||
:label="moduleLabel(module)"
|
||||
:value="module.moduleId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
@@ -54,6 +87,14 @@
|
||||
<el-button :disabled="!projectId || !database.tables.length" :loading="saving" @click="handleSaveDatabase">
|
||||
保存表结构
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!projectId || !database.tables.length"
|
||||
:loading="generatingBusinessBlueprint"
|
||||
@click="handleGenerateBusinessBlueprint"
|
||||
>
|
||||
生成业务蓝图
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
:icon="View"
|
||||
@@ -67,17 +108,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel app-blueprint-panel">
|
||||
<div v-if="hasAppBlueprint" class="panel app-blueprint-panel">
|
||||
<div class="panel-header database-header">
|
||||
<h2 class="panel-title">应用蓝图</h2>
|
||||
<div class="database-header-actions">
|
||||
<el-radio-group v-model="appBlueprintView" size="large">
|
||||
<el-radio-button label="summary">摘要</el-radio-button>
|
||||
<el-radio-button label="diagram">模块图</el-radio-button>
|
||||
<el-radio-button label="json">JSON</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="muted">角色:{{ appBlueprint.roles.length }}</span>
|
||||
<span class="muted">前台菜单:{{ appBlueprint.frontendMenus.length }}</span>
|
||||
<span class="muted">后台菜单:{{ appBlueprint.adminMenus.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasAppBlueprint" class="app-blueprint-content">
|
||||
<div class="app-blueprint-grid">
|
||||
<div class="app-blueprint-content">
|
||||
<div v-show="appBlueprintView === 'summary'" class="app-blueprint-grid">
|
||||
<section>
|
||||
<h3>角色</h3>
|
||||
<el-tag v-for="role in appBlueprint.roles" :key="role.code" effect="plain">
|
||||
@@ -97,12 +143,14 @@
|
||||
</el-tag>
|
||||
</section>
|
||||
</div>
|
||||
<el-input v-model="appBlueprintJson" type="textarea" :rows="12" spellcheck="false" />
|
||||
<div class="toolbar compact-toolbar">
|
||||
<el-button @click="applyAppBlueprintJson">应用调整</el-button>
|
||||
</div>
|
||||
<AppModuleDiagramView v-show="appBlueprintView === 'diagram'" :blueprint="appBlueprint" />
|
||||
<template v-if="appBlueprintView === 'json'">
|
||||
<el-input v-model="appBlueprintJson" type="textarea" :rows="12" spellcheck="false" />
|
||||
<div class="toolbar compact-toolbar">
|
||||
<el-button @click="applyAppBlueprintJson">应用调整</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<el-empty v-else description="生成应用蓝图后,这里会展示角色和前后台菜单" />
|
||||
</div>
|
||||
|
||||
<div class="panel designer-panel">
|
||||
@@ -148,25 +196,25 @@
|
||||
入参:{{ action.requestFields.join('、') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="Array.isArray(action.rules) && action.rules.length" class="blueprint-section">
|
||||
<div v-if="actionRuleItems(action).length" class="blueprint-section">
|
||||
<strong>规则</strong>
|
||||
<ul>
|
||||
<li v-for="rule in action.rules" :key="rule">{{ rule }}</li>
|
||||
<li v-for="(rule, ruleIndex) in actionRuleItems(action)" :key="`rule-${ruleIndex}-${rule}`">
|
||||
{{ rule }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="Array.isArray(action.effects) && action.effects.length" class="blueprint-section">
|
||||
<strong>影响</strong>
|
||||
<ul>
|
||||
<li v-for="effect in action.effects" :key="`${effect.type}-${effect.targetTable}-${effect.targetField}`">
|
||||
{{ effect.description || effect.type }}
|
||||
<span v-if="effect.targetTable"> / {{ effect.targetTable }}</span>
|
||||
<span v-if="effect.targetField">.{{ effect.targetField }}</span>
|
||||
<li v-for="(effect, effectIndex) in action.effects" :key="blueprintEffectKey(effect, effectIndex)">
|
||||
{{ formatEffect(effect) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty v-else description="生成数据库后,这里会展示 AI 识别出的业务动作草稿" />
|
||||
<el-empty v-else description="保存表结构后点击生成业务蓝图,这里会展示非 CRUD 业务动作" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -176,29 +224,36 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { DataAnalysis, View } from '@element-plus/icons-vue'
|
||||
import AppModuleDiagramView from '@/components/AppModuleDiagramView.vue'
|
||||
import DatabaseDesigner from '@/components/DatabaseDesigner.vue'
|
||||
import ErDiagramView from '@/components/ErDiagramView.vue'
|
||||
import { createProject, generateAppBlueprint, generateDatabase, generateProject, getDatabase, getProject, saveDatabase } from '@/api/project'
|
||||
import { createProject, generateAppBlueprint, generateBusinessBlueprint, generateDatabase, generateProject, getDatabase, getProject, listAvailableModules, saveDatabase, updateProject } from '@/api/project'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const generating = ref(false)
|
||||
const generatingAppBlueprint = ref(false)
|
||||
const generatingBusinessBlueprint = ref(false)
|
||||
const saving = ref(false)
|
||||
const previewing = ref(false)
|
||||
const loadingProject = ref(false)
|
||||
const loadingModules = ref(false)
|
||||
const databaseView = ref('editor')
|
||||
const appBlueprintView = ref('summary')
|
||||
const projectId = ref(normalizeProjectId(route.query.projectId))
|
||||
const errorMessage = ref('')
|
||||
const appBlueprint = ref(emptyAppBlueprint())
|
||||
const appBlueprintJson = ref(formatJson(emptyAppBlueprint()))
|
||||
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
|
||||
const availableModules = ref([])
|
||||
const projectForm = reactive({
|
||||
projectName: '',
|
||||
keyword: String(route.query.keyword || ''),
|
||||
projectDesc: '',
|
||||
industryTemplate: ''
|
||||
industryTemplate: '',
|
||||
frontendEnabled: true,
|
||||
moduleIds: []
|
||||
})
|
||||
|
||||
const LAST_PROJECT_KEY = 'easycode_last_project_id'
|
||||
@@ -272,12 +327,127 @@ function normalizeDatabase(payload) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBusinessActions(payload) {
|
||||
const data = unwrap(payload)
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
function normalizeProjectId(value) {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const id = Number(raw)
|
||||
return Number.isFinite(id) && id > 0 ? String(id) : ''
|
||||
}
|
||||
|
||||
function normalizeModuleIds(value) {
|
||||
return (Array.isArray(value) ? value : [])
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isFinite(item) && item > 0)
|
||||
}
|
||||
|
||||
function moduleLabel(module) {
|
||||
const alias = module.moduleAlias ? ` / ${module.moduleAlias}` : ''
|
||||
return `${module.moduleName || module.moduleId}${alias}`
|
||||
}
|
||||
|
||||
const ruleTypeLabels = {
|
||||
EXISTS: '必须存在',
|
||||
NOT_EXISTS: '必须不存在',
|
||||
FIELD_EQUALS: '字段等于',
|
||||
FIELD_NOT_EQUALS: '字段不等于',
|
||||
FIELD_IN: '字段属于',
|
||||
NUMBER_GTE: '数值不小于'
|
||||
}
|
||||
|
||||
const effectTypeLabels = {
|
||||
UPDATE_FIELD: '更新字段',
|
||||
INSERT_ROW: '新增记录',
|
||||
UPDATE_FIELDS: '更新多字段',
|
||||
SET_STATUS: '设置状态',
|
||||
INCREASE_NUMBER: '数值增加',
|
||||
DECREASE_NUMBER: '数值减少'
|
||||
}
|
||||
|
||||
function actionRuleItems(action) {
|
||||
const rules = []
|
||||
if (Array.isArray(action?.rules)) {
|
||||
rules.push(...action.rules.map(formatRule).filter(Boolean))
|
||||
}
|
||||
if (Array.isArray(action?.ruleChecks)) {
|
||||
rules.push(...action.ruleChecks.map(formatRule).filter(Boolean))
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
function formatRule(rule) {
|
||||
if (!rule) return ''
|
||||
if (typeof rule === 'string') return rule
|
||||
|
||||
const parts = [ruleTypeLabels[rule.type] || rule.type || '规则']
|
||||
const target = formatTarget(rule.targetTable, rule.targetField)
|
||||
if (target) parts.push(target)
|
||||
if (Array.isArray(rule.conditionFields) && rule.conditionFields.length) {
|
||||
parts.push(`条件 ${formatFieldList(rule.conditionFields)}`)
|
||||
}
|
||||
if (rule.expectedValue) parts.push(`期望 ${rule.expectedValue}`)
|
||||
if (Array.isArray(rule.expectedValues) && rule.expectedValues.length) {
|
||||
parts.push(`可选 ${formatFieldList(rule.expectedValues)}`)
|
||||
}
|
||||
if (rule.compareValue) parts.push(`比较 ${rule.compareValue}`)
|
||||
if (rule.message) parts.push(`提示 ${rule.message}`)
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
function formatEffect(effect) {
|
||||
if (!effect) return ''
|
||||
if (typeof effect === 'string') return effect
|
||||
|
||||
const parts = [effect.description || effectTypeLabels[effect.type] || effect.type || '影响']
|
||||
const target = formatTarget(effect.targetTable, effect.targetField)
|
||||
const values = formatValueMap(effect.values)
|
||||
if (target) parts.push(target)
|
||||
if (values) parts.push(values)
|
||||
if (effect.value) parts.push(`值 ${effect.value}`)
|
||||
if (effect.amount) parts.push(`数量 ${effect.amount}`)
|
||||
if (effect.minValue) parts.push(`下限 ${effect.minValue}`)
|
||||
if (Array.isArray(effect.conditionFields) && effect.conditionFields.length) {
|
||||
parts.push(`条件 ${formatFieldList(effect.conditionFields)}`)
|
||||
}
|
||||
if (effect.message) parts.push(`提示 ${effect.message}`)
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
function blueprintEffectKey(effect, index) {
|
||||
if (!effect || typeof effect === 'string') return `effect-${index}-${effect || ''}`
|
||||
return `effect-${index}-${effect.type || ''}-${effect.targetTable || ''}-${effect.targetField || ''}`
|
||||
}
|
||||
|
||||
function formatTarget(table, field) {
|
||||
if (table && field) return `${table}.${field}`
|
||||
return table || field || ''
|
||||
}
|
||||
|
||||
function formatValueMap(values) {
|
||||
if (!values || typeof values !== 'object' || Array.isArray(values)) return ''
|
||||
return Object.entries(values)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function formatFieldList(values) {
|
||||
return values.filter(Boolean).join('、')
|
||||
}
|
||||
|
||||
function projectPayload() {
|
||||
return {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
requirementKeyword: projectForm.keyword,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
frontendEnabled: projectForm.frontendEnabled,
|
||||
moduleIds: normalizeModuleIds(projectForm.moduleIds)
|
||||
}
|
||||
}
|
||||
|
||||
function rememberProject(id) {
|
||||
if (id) {
|
||||
localStorage.setItem(LAST_PROJECT_KEY, String(id))
|
||||
@@ -298,6 +468,8 @@ async function loadProjectDraft(value) {
|
||||
projectForm.projectDesc = project.projectDesc || ''
|
||||
projectForm.keyword = project.requirementKeyword || projectForm.keyword || ''
|
||||
projectForm.industryTemplate = project.industryTemplate || ''
|
||||
projectForm.frontendEnabled = project.frontendEnabled !== '0'
|
||||
projectForm.moduleIds = normalizeModuleIds(project.moduleIds)
|
||||
appBlueprint.value = normalizeAppBlueprint(project.appBlueprint)
|
||||
syncAppBlueprintJson()
|
||||
database.value = normalizeDatabase(databaseResult)
|
||||
@@ -310,14 +482,12 @@ async function loadProjectDraft(value) {
|
||||
}
|
||||
|
||||
async function ensureProject() {
|
||||
if (projectId.value) return projectId.value
|
||||
if (projectId.value) {
|
||||
await updateProject(projectId.value, projectPayload())
|
||||
return projectId.value
|
||||
}
|
||||
|
||||
const result = await createProject({
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
requirementKeyword: projectForm.keyword,
|
||||
industryTemplate: projectForm.industryTemplate
|
||||
})
|
||||
const result = await createProject(projectPayload())
|
||||
const data = unwrap(result)
|
||||
projectId.value = data.projectId || data.id
|
||||
|
||||
@@ -337,6 +507,18 @@ async function ensureProject() {
|
||||
return projectId.value
|
||||
}
|
||||
|
||||
async function loadModuleOptions() {
|
||||
loadingModules.value = true
|
||||
try {
|
||||
const result = await listAvailableModules()
|
||||
availableModules.value = Array.isArray(result) ? result : []
|
||||
} catch (error) {
|
||||
ElMessage.warning(error.message || '后台功能模块加载失败')
|
||||
} finally {
|
||||
loadingModules.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function validateRequirementInput() {
|
||||
if (!projectForm.projectName.trim()) {
|
||||
ElMessage.warning('请输入项目名称')
|
||||
@@ -421,6 +603,39 @@ async function handleGenerateDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGenerateBusinessBlueprint() {
|
||||
if (!validateRequirementInput()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!database.value.tables.length) {
|
||||
ElMessage.warning('请先生成并确认数据库设计')
|
||||
return
|
||||
}
|
||||
|
||||
generatingBusinessBlueprint.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const id = await ensureProject()
|
||||
await saveDatabase(id, database.value)
|
||||
const result = await generateBusinessBlueprint(id, {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
extraRequirements: projectForm.keyword
|
||||
})
|
||||
database.value = {
|
||||
...database.value,
|
||||
businessActions: normalizeBusinessActions(result)
|
||||
}
|
||||
ElMessage.success('业务蓝图已生成')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '生成业务蓝图失败,请确认表结构已保存后重试。'
|
||||
} finally {
|
||||
generatingBusinessBlueprint.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveDatabase() {
|
||||
if (!projectId.value) return
|
||||
|
||||
@@ -459,6 +674,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadModuleOptions()
|
||||
if (projectId.value) {
|
||||
loadProjectDraft(projectId.value)
|
||||
}
|
||||
@@ -481,6 +697,10 @@ onMounted(() => {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.module-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeType" class="type-tabs">
|
||||
<el-tab-pane label="后端" name="backend" />
|
||||
<el-tab-pane label="前台" name="frontend" />
|
||||
<el-tab-pane label="后台前端" name="admin_frontend" />
|
||||
<el-tab-pane
|
||||
v-for="type in templateTypes"
|
||||
:key="type.value"
|
||||
:label="type.label"
|
||||
:name="type.value"
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="tree-wrap">
|
||||
<el-empty v-if="!currentTree.length && !loading" description="暂无项目结构" />
|
||||
@@ -32,7 +35,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { Download, Refresh } from '@element-plus/icons-vue'
|
||||
import CodePreview from '@/components/CodePreview.vue'
|
||||
import ProjectStructureTree from '@/components/ProjectStructureTree.vue'
|
||||
import { downloadProject, getFileContent, getProjectStructure } from '@/api/project'
|
||||
import { downloadProject, getFileContent, getProject, getProjectStructure } from '@/api/project'
|
||||
import { saveBlob, sourceZipName } from '@/utils/download'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -43,27 +46,37 @@ const codeLoading = ref(false)
|
||||
const downloading = ref(false)
|
||||
const code = ref('')
|
||||
const selectedFileName = ref('')
|
||||
const projectConfig = ref({})
|
||||
const structures = reactive({
|
||||
backend: [],
|
||||
frontend: [],
|
||||
admin_frontend: []
|
||||
})
|
||||
|
||||
const templateTypes = computed(() => {
|
||||
const types = [{ label: '后端', value: 'backend' }]
|
||||
if (projectConfig.value.frontendEnabled !== '0') {
|
||||
types.push({ label: '前台', value: 'frontend' })
|
||||
}
|
||||
types.push({ label: '后台前端', value: 'admin_frontend' })
|
||||
return types
|
||||
})
|
||||
|
||||
const currentTree = computed(() => structures[activeType.value] || [])
|
||||
|
||||
function unwrap(result) {
|
||||
return result?.data || result || []
|
||||
}
|
||||
|
||||
function normalizeNodes(nodes, prefix = '') {
|
||||
function normalizeNodes(nodes, type, prefix = '') {
|
||||
return (Array.isArray(nodes) ? nodes : []).map((node, index) => {
|
||||
const label = node.label || node.name || node.path || `文件 ${index + 1}`
|
||||
const path = node.path || `${prefix}${label}`
|
||||
const children = normalizeNodes(node.children, `${path}/`)
|
||||
const children = normalizeNodes(node.children, type, `${path}/`)
|
||||
|
||||
return {
|
||||
...node,
|
||||
id: node.id || `${activeType.value}:${path}`,
|
||||
id: node.id || `${type}:${path}`,
|
||||
label,
|
||||
path,
|
||||
type: node.type || (children.length ? 'folder' : 'file'),
|
||||
@@ -83,13 +96,25 @@ function pickCodeContent(data) {
|
||||
async function loadType(type) {
|
||||
const result = await getProjectStructure(projectId.value, type)
|
||||
const data = unwrap(result)
|
||||
structures[type] = normalizeNodes(data)
|
||||
structures[type] = normalizeNodes(data, type)
|
||||
}
|
||||
|
||||
async function loadProjectConfig() {
|
||||
const result = await getProject(projectId.value)
|
||||
projectConfig.value = unwrap(result) || {}
|
||||
if (!templateTypes.value.some((type) => type.value === activeType.value)) {
|
||||
activeType.value = 'backend'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStructures() {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all(['backend', 'frontend', 'admin_frontend'].map(loadType))
|
||||
await loadProjectConfig()
|
||||
structures.backend = []
|
||||
structures.frontend = []
|
||||
structures.admin_frontend = []
|
||||
await Promise.all(templateTypes.value.map((type) => loadType(type.value)))
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '加载项目结构失败')
|
||||
} finally {
|
||||
|
||||
@@ -17,10 +17,12 @@ import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.front.FrontProject;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.PreviewFileRequest;
|
||||
import com.ruoyi.generator.service.front.IAiGenerateService;
|
||||
@@ -52,6 +54,12 @@ public class FrontProjectController extends BaseController
|
||||
return AjaxResult.success(frontProjectService.listProjects(userId));
|
||||
}
|
||||
|
||||
@GetMapping("/options/modules")
|
||||
public AjaxResult modules()
|
||||
{
|
||||
return AjaxResult.success(frontProjectService.listAvailableModules());
|
||||
}
|
||||
|
||||
@GetMapping("/{projectId}")
|
||||
public AjaxResult get(@PathVariable Long projectId)
|
||||
{
|
||||
@@ -101,12 +109,23 @@ public class FrontProjectController extends BaseController
|
||||
return success(aiGenerateService.generateAppBlueprint(userId, projectId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{projectId}/generate-business-blueprint")
|
||||
public AjaxResult generateBusinessBlueprint(@PathVariable Long projectId, @RequestBody GenerateBusinessBlueprintRequest request)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
return success(aiGenerateService.generateBusinessBlueprint(userId, projectId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{projectId}/preview")
|
||||
public AjaxResult preview(@PathVariable Long projectId)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
FrontProject project = frontProjectService.getProject(userId, projectId);
|
||||
frontProjectPreviewService.getStructure(userId, projectId, "backend");
|
||||
frontProjectPreviewService.getStructure(userId, projectId, "frontend");
|
||||
if (!"0".equals(project.getFrontendEnabled()))
|
||||
{
|
||||
frontProjectPreviewService.getStructure(userId, projectId, "frontend");
|
||||
}
|
||||
frontProjectPreviewService.getStructure(userId, projectId, "admin_frontend");
|
||||
return toAjax(frontProjectPreviewService.markPreviewReady(userId, projectId));
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ public class GenProject extends BaseEntity
|
||||
@Excel(name = "后端框架")
|
||||
private String backFramework;
|
||||
|
||||
private transient String frontendEnabled;
|
||||
|
||||
/** 状态(0正常 1停用) */
|
||||
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
|
||||
private String status;
|
||||
@@ -71,6 +73,8 @@ public class GenProject extends BaseEntity
|
||||
|
||||
private transient List<BusinessActionDesign> businessActions;
|
||||
|
||||
private transient List<SysProjectModule> modules;
|
||||
|
||||
private List<GenTable> tables;
|
||||
|
||||
public void setProjectId(Long projectId)
|
||||
@@ -163,6 +167,14 @@ public class GenProject extends BaseEntity
|
||||
return backFramework;
|
||||
}
|
||||
|
||||
public String getFrontendEnabled() {
|
||||
return frontendEnabled;
|
||||
}
|
||||
|
||||
public void setFrontendEnabled(String frontendEnabled) {
|
||||
this.frontendEnabled = frontendEnabled;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
@@ -181,6 +193,14 @@ public class GenProject extends BaseEntity
|
||||
this.tables = tables;
|
||||
}
|
||||
|
||||
public List<SysProjectModule> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public void setModules(List<SysProjectModule> modules) {
|
||||
this.modules = modules;
|
||||
}
|
||||
|
||||
public Boolean getFrontDraft() {
|
||||
return frontDraft;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.ruoyi.generator.domain.front;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.ruoyi.generator.domain.SysProjectModule;
|
||||
|
||||
public class FrontProject extends BaseEntity
|
||||
{
|
||||
@@ -18,6 +19,7 @@ public class FrontProject extends BaseEntity
|
||||
private String industryTemplate;
|
||||
private String frontFramework;
|
||||
private String backFramework;
|
||||
private String frontendEnabled;
|
||||
private String generateStatus;
|
||||
private String previewStatus;
|
||||
private String appBlueprint;
|
||||
@@ -25,6 +27,8 @@ public class FrontProject extends BaseEntity
|
||||
private String businessBlueprint;
|
||||
private String status;
|
||||
private List<FrontProjectTable> tables;
|
||||
private List<Long> moduleIds;
|
||||
private List<SysProjectModule> modules;
|
||||
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
@@ -48,6 +52,8 @@ public class FrontProject extends BaseEntity
|
||||
public void setFrontFramework(String frontFramework) { this.frontFramework = frontFramework; }
|
||||
public String getBackFramework() { return backFramework; }
|
||||
public void setBackFramework(String backFramework) { this.backFramework = backFramework; }
|
||||
public String getFrontendEnabled() { return frontendEnabled; }
|
||||
public void setFrontendEnabled(String frontendEnabled) { this.frontendEnabled = frontendEnabled; }
|
||||
public String getGenerateStatus() { return generateStatus; }
|
||||
public void setGenerateStatus(String generateStatus) { this.generateStatus = generateStatus; }
|
||||
public String getPreviewStatus() { return previewStatus; }
|
||||
@@ -62,4 +68,8 @@ public class FrontProject extends BaseEntity
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public List<FrontProjectTable> getTables() { return tables; }
|
||||
public void setTables(List<FrontProjectTable> tables) { this.tables = tables; }
|
||||
public List<Long> getModuleIds() { return moduleIds; }
|
||||
public void setModuleIds(List<Long> moduleIds) { this.moduleIds = moduleIds; }
|
||||
public List<SysProjectModule> getModules() { return modules; }
|
||||
public void setModules(List<SysProjectModule> modules) { this.modules = modules; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.generator.domain.front;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
public class FrontProjectModule extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private Long projectId;
|
||||
private Long moduleId;
|
||||
private String status;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public Long getProjectId() { return projectId; }
|
||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||
public Long getModuleId() { return moduleId; }
|
||||
public void setModuleId(Long moduleId) { this.moduleId = moduleId; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public class BusinessActionDesign
|
||||
private Boolean transaction;
|
||||
private List<String> requestFields;
|
||||
private List<String> rules;
|
||||
private List<BusinessActionRuleDesign> ruleChecks;
|
||||
private List<BusinessActionEffectDesign> effects;
|
||||
|
||||
public String getCode() { return code; }
|
||||
@@ -30,6 +31,8 @@ public class BusinessActionDesign
|
||||
public void setRequestFields(List<String> requestFields) { this.requestFields = requestFields; }
|
||||
public List<String> getRules() { return rules; }
|
||||
public void setRules(List<String> rules) { this.rules = rules; }
|
||||
public List<BusinessActionRuleDesign> getRuleChecks() { return ruleChecks; }
|
||||
public void setRuleChecks(List<BusinessActionRuleDesign> ruleChecks) { this.ruleChecks = ruleChecks; }
|
||||
public List<BusinessActionEffectDesign> getEffects() { return effects; }
|
||||
public void setEffects(List<BusinessActionEffectDesign> effects) { this.effects = effects; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.generator.domain.front.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class BusinessActionEffectDesign
|
||||
{
|
||||
@@ -8,7 +9,11 @@ public class BusinessActionEffectDesign
|
||||
private String targetTable;
|
||||
private String targetField;
|
||||
private String value;
|
||||
private Map<String, String> values;
|
||||
private String amount;
|
||||
private String minValue;
|
||||
private List<String> conditionFields;
|
||||
private String message;
|
||||
private String description;
|
||||
|
||||
public String getType() { return type; }
|
||||
@@ -19,8 +24,16 @@ public class BusinessActionEffectDesign
|
||||
public void setTargetField(String targetField) { this.targetField = targetField; }
|
||||
public String getValue() { return value; }
|
||||
public void setValue(String value) { this.value = value; }
|
||||
public Map<String, String> getValues() { return values; }
|
||||
public void setValues(Map<String, String> values) { this.values = values; }
|
||||
public String getAmount() { return amount; }
|
||||
public void setAmount(String amount) { this.amount = amount; }
|
||||
public String getMinValue() { return minValue; }
|
||||
public void setMinValue(String minValue) { this.minValue = minValue; }
|
||||
public List<String> getConditionFields() { return conditionFields; }
|
||||
public void setConditionFields(List<String> conditionFields) { this.conditionFields = conditionFields; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ruoyi.generator.domain.front.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BusinessActionRuleDesign
|
||||
{
|
||||
private String type;
|
||||
private String targetTable;
|
||||
private String targetField;
|
||||
private String expectedValue;
|
||||
private List<String> expectedValues;
|
||||
private String compareValue;
|
||||
private List<String> conditionFields;
|
||||
private String message;
|
||||
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getTargetTable() { return targetTable; }
|
||||
public void setTargetTable(String targetTable) { this.targetTable = targetTable; }
|
||||
public String getTargetField() { return targetField; }
|
||||
public void setTargetField(String targetField) { this.targetField = targetField; }
|
||||
public String getExpectedValue() { return expectedValue; }
|
||||
public void setExpectedValue(String expectedValue) { this.expectedValue = expectedValue; }
|
||||
public List<String> getExpectedValues() { return expectedValues; }
|
||||
public void setExpectedValues(List<String> expectedValues) { this.expectedValues = expectedValues; }
|
||||
public String getCompareValue() { return compareValue; }
|
||||
public void setCompareValue(String compareValue) { this.compareValue = compareValue; }
|
||||
public List<String> getConditionFields() { return conditionFields; }
|
||||
public void setConditionFields(List<String> conditionFields) { this.conditionFields = conditionFields; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.generator.domain.front.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FrontProjectCreateRequest
|
||||
{
|
||||
private String projectName;
|
||||
@@ -8,6 +10,8 @@ public class FrontProjectCreateRequest
|
||||
private String industryTemplate;
|
||||
private String packageName;
|
||||
private String version;
|
||||
private Boolean frontendEnabled;
|
||||
private List<Long> moduleIds;
|
||||
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
@@ -21,4 +25,8 @@ public class FrontProjectCreateRequest
|
||||
public void setPackageName(String packageName) { this.packageName = packageName; }
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String version) { this.version = version; }
|
||||
public Boolean getFrontendEnabled() { return frontendEnabled; }
|
||||
public void setFrontendEnabled(Boolean frontendEnabled) { this.frontendEnabled = frontendEnabled; }
|
||||
public List<Long> getModuleIds() { return moduleIds; }
|
||||
public void setModuleIds(List<Long> moduleIds) { this.moduleIds = moduleIds; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.generator.domain.front.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FrontProjectUpdateRequest
|
||||
{
|
||||
private String projectName;
|
||||
@@ -8,6 +10,8 @@ public class FrontProjectUpdateRequest
|
||||
private String industryTemplate;
|
||||
private String packageName;
|
||||
private String version;
|
||||
private Boolean frontendEnabled;
|
||||
private List<Long> moduleIds;
|
||||
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
@@ -21,4 +25,8 @@ public class FrontProjectUpdateRequest
|
||||
public void setPackageName(String packageName) { this.packageName = packageName; }
|
||||
public String getVersion() { return version; }
|
||||
public void setVersion(String version) { this.version = version; }
|
||||
public Boolean getFrontendEnabled() { return frontendEnabled; }
|
||||
public void setFrontendEnabled(Boolean frontendEnabled) { this.frontendEnabled = frontendEnabled; }
|
||||
public List<Long> getModuleIds() { return moduleIds; }
|
||||
public void setModuleIds(List<Long> moduleIds) { this.moduleIds = moduleIds; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.generator.domain.front.dto;
|
||||
|
||||
public class GenerateBusinessBlueprintRequest
|
||||
{
|
||||
private String projectName;
|
||||
private String projectDesc;
|
||||
private String industryTemplate;
|
||||
private String extraRequirements;
|
||||
|
||||
public String getProjectName() { return projectName; }
|
||||
public void setProjectName(String projectName) { this.projectName = projectName; }
|
||||
public String getProjectDesc() { return projectDesc; }
|
||||
public void setProjectDesc(String projectDesc) { this.projectDesc = projectDesc; }
|
||||
public String getIndustryTemplate() { return industryTemplate; }
|
||||
public void setIndustryTemplate(String industryTemplate) { this.industryTemplate = industryTemplate; }
|
||||
public String getExtraRequirements() { return extraRequirements; }
|
||||
public void setExtraRequirements(String extraRequirements) { this.extraRequirements = extraRequirements; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ruoyi.generator.mapper.front;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.generator.domain.SysProjectModule;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectModule;
|
||||
|
||||
public interface FrontProjectModuleMapper
|
||||
{
|
||||
public List<FrontProjectModule> selectModulesByProjectId(Long projectId);
|
||||
|
||||
public List<SysProjectModule> selectSysProjectModulesByProjectId(Long projectId);
|
||||
|
||||
public int insertFrontProjectModule(FrontProjectModule frontProjectModule);
|
||||
|
||||
public int deleteModulesByProjectId(Long projectId);
|
||||
}
|
||||
@@ -106,12 +106,17 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
List<SysProjectStructure> structureNodes = selectStructureNodes(project, projectTemplate);
|
||||
|
||||
Map<Long, SysProjectStructure> sourceNodeMap = new HashMap<Long, SysProjectStructure>();
|
||||
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<String, Map<String, Object>>();
|
||||
for (SysProjectStructure node : structureNodes) {
|
||||
sourceNodeMap.put(node.getNodeId(), node);
|
||||
}
|
||||
|
||||
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<String, Map<String, Object>>();
|
||||
Map<String, Map<String, Object>> mergedFolderMap = new LinkedHashMap<String, Map<String, Object>>();
|
||||
for (SysProjectStructure node : structureNodes) {
|
||||
if (isTableScopedNode(node)) {
|
||||
for (GenTable table : projectTables) {
|
||||
Map<String, Object> tableNodeData = createStructureNodeData(node, project, table);
|
||||
tableNodeData = mergeTableScopedFolderNode(mergedFolderMap, node, project, table, sourceNodeMap, tableNodeData);
|
||||
nodeMap.put(getNodeKey(node.getNodeId(), table.getTableId()), tableNodeData);
|
||||
}
|
||||
} else {
|
||||
@@ -239,7 +244,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
|
||||
private List<SysProjectModule> getProjectModules(GenProject project) {
|
||||
if (project.isFrontDraft()) {
|
||||
return new ArrayList<SysProjectModule>();
|
||||
return project.getModules() == null ? new ArrayList<SysProjectModule>() : project.getModules();
|
||||
}
|
||||
SysProjectModule projectModuleFilter = new SysProjectModule();
|
||||
projectModuleFilter.setProjectId(project.getProjectId());
|
||||
@@ -338,6 +343,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
result = result.replace("{projectName}", StringUtils.defaultString(project.getProjectFileName()));
|
||||
result = result.replace("{packageName}", StringUtils.defaultString(project.getPackageName()));
|
||||
if (table != null) {
|
||||
result = result.replace("{moduleName}", StringUtils.defaultString(table.getModuleName()));
|
||||
result = result.replace("{ClassName}", StringUtils.defaultString(table.getClassName()));
|
||||
result = result.replace("{className}", StringUtils.uncapitalize(StringUtils.defaultString(table.getClassName())));
|
||||
result = result.replace("{tableName}", StringUtils.defaultString(table.getTableName()));
|
||||
@@ -346,6 +352,45 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> mergeTableScopedFolderNode(Map<String, Map<String, Object>> mergedFolderMap,
|
||||
SysProjectStructure node, GenProject project, GenTable table,
|
||||
Map<Long, SysProjectStructure> sourceNodeMap,
|
||||
Map<String, Object> nodeData) {
|
||||
if (!"folder".equals(node.getNodeType()) || nodeData == null) {
|
||||
return nodeData;
|
||||
}
|
||||
String mergeKey = getTableScopedFolderMergeKey(node, project, table, sourceNodeMap);
|
||||
Map<String, Object> existingNode = mergedFolderMap.get(mergeKey);
|
||||
if (existingNode != null) {
|
||||
return existingNode;
|
||||
}
|
||||
mergedFolderMap.put(mergeKey, nodeData);
|
||||
return nodeData;
|
||||
}
|
||||
|
||||
private String getTableScopedFolderMergeKey(SysProjectStructure node, GenProject project, GenTable table,
|
||||
Map<Long, SysProjectStructure> sourceNodeMap) {
|
||||
List<String> pathParts = new ArrayList<String>();
|
||||
SysProjectStructure current = node;
|
||||
while (current != null) {
|
||||
GenTable scopedTable = isTableScopedNode(current) ? table : null;
|
||||
pathParts.add(0, current.getNodeId() + ":" + replaceNodeName(current.getNodeName(), project, scopedTable));
|
||||
if (current.getParentId() == null || current.getParentId() == 0) {
|
||||
break;
|
||||
}
|
||||
current = sourceNodeMap.get(current.getParentId());
|
||||
}
|
||||
|
||||
StringBuilder key = new StringBuilder();
|
||||
for (String part : pathParts) {
|
||||
if (key.length() > 0) {
|
||||
key.append("/");
|
||||
}
|
||||
key.append(part);
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
private void addNodeToParent(List<Map<String, Object>> roots, Map<String, Object> nodeData,
|
||||
SysProjectStructure node, Map<String, Map<String, Object>> nodeMap,
|
||||
Map<Long, SysProjectStructure> sourceNodeMap, GenTable table) {
|
||||
@@ -353,7 +398,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
return;
|
||||
}
|
||||
if (node.getParentId() == null || node.getParentId() == 0) {
|
||||
roots.add(nodeData);
|
||||
addNodeIfAbsent(roots, nodeData);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,10 +412,19 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> children = (List<Map<String, Object>>) parentNode.get("children");
|
||||
if (children != null) {
|
||||
children.add(nodeData);
|
||||
addNodeIfAbsent(children, nodeData);
|
||||
}
|
||||
}
|
||||
|
||||
private void addNodeIfAbsent(List<Map<String, Object>> nodes, Map<String, Object> nodeData) {
|
||||
for (Map<String, Object> node : nodes) {
|
||||
if (node == nodeData) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
nodes.add(nodeData);
|
||||
}
|
||||
|
||||
private boolean matchesTemplateFile(TemplateFile templateFile, String templateFileName) {
|
||||
if (templateFile == null || StringUtils.isEmpty(templateFileName)) {
|
||||
return false;
|
||||
|
||||
@@ -3,13 +3,16 @@ package com.ruoyi.generator.service.front;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
||||
@@ -22,6 +25,8 @@ public class AiBusinessBlueprintValidator
|
||||
private static final int MAX_EFFECTS = 12;
|
||||
private static final Pattern ACTION_CODE = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
|
||||
private static final Pattern API_PATH = Pattern.compile("^/[A-Za-z0-9_./-]{1,180}$");
|
||||
private static final Pattern PARAM_PLACEHOLDER = Pattern.compile("^\\$\\{param\\.([a-z][a-z0-9_]*)}$");
|
||||
private static final Pattern NUMBER_LITERAL = Pattern.compile("^-?\\d+(\\.\\d+)?$");
|
||||
|
||||
public void validate(DatabaseDesignResponse response)
|
||||
{
|
||||
@@ -31,7 +36,7 @@ public class AiBusinessBlueprintValidator
|
||||
}
|
||||
if (response.getBusinessActions().size() > MAX_ACTIONS)
|
||||
{
|
||||
throw new ServiceException("业务动作数量超过限制");
|
||||
throw new ServiceException("Business action count exceeds limit");
|
||||
}
|
||||
Set<String> tableNames = collectTableNames(response.getTables());
|
||||
Set<String> columnNames = collectColumnNames(response.getTables());
|
||||
@@ -46,19 +51,19 @@ public class AiBusinessBlueprintValidator
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ServiceException("业务动作不能为空");
|
||||
throw new ServiceException("Business action cannot be empty");
|
||||
}
|
||||
if (StringUtils.isBlank(action.getCode()) || !ACTION_CODE.matcher(action.getCode()).matches())
|
||||
{
|
||||
throw new ServiceException("业务动作编码只能包含小写字母、数字和下划线");
|
||||
throw new ServiceException("Business action code is invalid");
|
||||
}
|
||||
if (!actionCodes.add(action.getCode()))
|
||||
{
|
||||
throw new ServiceException("业务动作编码重复:" + action.getCode());
|
||||
throw new ServiceException("Business action code duplicated: " + action.getCode());
|
||||
}
|
||||
if (StringUtils.isBlank(action.getName()) || action.getName().length() > 80)
|
||||
{
|
||||
throw new ServiceException("业务动作名称不能为空且不能超过80个字符");
|
||||
throw new ServiceException("Business action name is required and cannot exceed 80 characters");
|
||||
}
|
||||
if (StringUtils.isBlank(action.getOwnerTable()) || !tableNames.contains(action.getOwnerTable()))
|
||||
{
|
||||
@@ -66,10 +71,11 @@ public class AiBusinessBlueprintValidator
|
||||
}
|
||||
if (StringUtils.isNotBlank(action.getPath()) && !API_PATH.matcher(action.getPath()).matches())
|
||||
{
|
||||
throw new ServiceException("业务动作接口路径不合法");
|
||||
throw new ServiceException("Business action API path is invalid");
|
||||
}
|
||||
validateRequestFields(action, columnNames);
|
||||
validateRules(action);
|
||||
validateRuleChecks(action, tableNames, columnNames);
|
||||
validateEffects(action, tableNames, columnNames);
|
||||
}
|
||||
|
||||
@@ -83,7 +89,7 @@ public class AiBusinessBlueprintValidator
|
||||
{
|
||||
if (StringUtils.isBlank(field) || !columnNames.contains(field))
|
||||
{
|
||||
throw new ServiceException("业务动作请求字段不存在:" + StringUtils.defaultString(field));
|
||||
throw new ServiceException("Business action request field does not exist: " + StringUtils.defaultString(field));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,13 +102,71 @@ public class AiBusinessBlueprintValidator
|
||||
}
|
||||
if (action.getRules().size() > MAX_RULES)
|
||||
{
|
||||
throw new ServiceException("业务规则数量超过限制");
|
||||
throw new ServiceException("Business rule count exceeds limit");
|
||||
}
|
||||
for (String rule : action.getRules())
|
||||
{
|
||||
if (StringUtils.isBlank(rule) || rule.length() > 200)
|
||||
{
|
||||
throw new ServiceException("业务规则不能为空且不能超过200个字符");
|
||||
throw new ServiceException("Business rule is required and cannot exceed 200 characters");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRuleChecks(BusinessActionDesign action, Set<String> tableNames, Set<String> columnNames)
|
||||
{
|
||||
if (StringUtils.isEmpty(action.getRuleChecks()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (action.getRuleChecks().size() > MAX_RULES)
|
||||
{
|
||||
throw new ServiceException("Business rule check count exceeds limit");
|
||||
}
|
||||
for (BusinessActionRuleDesign rule : action.getRuleChecks())
|
||||
{
|
||||
if (rule == null)
|
||||
{
|
||||
throw new ServiceException("Business rule check cannot be empty");
|
||||
}
|
||||
String type = StringUtils.defaultString(rule.getType()).toUpperCase(Locale.ENGLISH);
|
||||
if (!isSupportedRuleType(type))
|
||||
{
|
||||
throw new ServiceException("Unsupported business rule type: " + type);
|
||||
}
|
||||
validateTargetTable(rule.getTargetTable(), tableNames, "Business rule target table does not exist: ");
|
||||
validateConditionFields(rule.getConditionFields(), columnNames, "Business rule condition field does not exist: ");
|
||||
if (requiresRuleTargetField(type) && StringUtils.isBlank(rule.getTargetField()))
|
||||
{
|
||||
throw new ServiceException("Business rule target field is required: " + type);
|
||||
}
|
||||
if (StringUtils.isNotBlank(rule.getTargetField()) && !columnNames.contains(rule.getTargetField()))
|
||||
{
|
||||
throw new ServiceException("Business rule target field does not exist: " + rule.getTargetField());
|
||||
}
|
||||
if (requiresExpectedValue(type) && StringUtils.isBlank(rule.getExpectedValue()))
|
||||
{
|
||||
throw new ServiceException("Business rule expected value is required: " + type);
|
||||
}
|
||||
if ("FIELD_IN".equals(type) && StringUtils.isEmpty(rule.getExpectedValues()))
|
||||
{
|
||||
throw new ServiceException("Business rule expected values are required: " + type);
|
||||
}
|
||||
if ("NUMBER_GTE".equals(type))
|
||||
{
|
||||
validateNumberExpression(rule.getCompareValue(), columnNames, "Business rule compare value is invalid");
|
||||
}
|
||||
validateSafeValue(rule.getExpectedValue(), columnNames);
|
||||
if (rule.getExpectedValues() != null)
|
||||
{
|
||||
for (String expectedValue : rule.getExpectedValues())
|
||||
{
|
||||
validateSafeValue(expectedValue, columnNames);
|
||||
}
|
||||
}
|
||||
if (StringUtils.defaultString(rule.getMessage()).length() > 200)
|
||||
{
|
||||
throw new ServiceException("Business rule message cannot exceed 200 characters");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,33 +179,61 @@ public class AiBusinessBlueprintValidator
|
||||
}
|
||||
if (action.getEffects().size() > MAX_EFFECTS)
|
||||
{
|
||||
throw new ServiceException("业务影响数量超过限制");
|
||||
throw new ServiceException("Business effect count exceeds limit");
|
||||
}
|
||||
for (BusinessActionEffectDesign effect : action.getEffects())
|
||||
{
|
||||
if (effect == null)
|
||||
validateEffect(effect, tableNames, columnNames);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEffect(BusinessActionEffectDesign effect, Set<String> tableNames, Set<String> columnNames)
|
||||
{
|
||||
if (effect == null)
|
||||
{
|
||||
throw new ServiceException("Business effect cannot be empty");
|
||||
}
|
||||
String type = StringUtils.defaultString(effect.getType()).toUpperCase(Locale.ENGLISH);
|
||||
if (StringUtils.isBlank(type) || type.length() > 50)
|
||||
{
|
||||
throw new ServiceException("Business effect type is required and cannot exceed 50 characters");
|
||||
}
|
||||
if (!isSupportedEffectType(type))
|
||||
{
|
||||
throw new ServiceException("Unsupported business effect type: " + type);
|
||||
}
|
||||
validateTargetTable(effect.getTargetTable(), tableNames, "Business effect target table does not exist: ");
|
||||
if (requiresEffectTargetField(type) && StringUtils.isBlank(effect.getTargetField()))
|
||||
{
|
||||
throw new ServiceException("Business effect target field is required: " + type);
|
||||
}
|
||||
if (StringUtils.isNotBlank(effect.getTargetField()) && !columnNames.contains(effect.getTargetField()))
|
||||
{
|
||||
throw new ServiceException("Business effect target field does not exist: " + effect.getTargetField());
|
||||
}
|
||||
validateEffectConditionFields(effect, columnNames);
|
||||
validateEffectValues(effect, columnNames);
|
||||
if (requiresEffectValues(type) && (effect.getValues() == null || effect.getValues().isEmpty()))
|
||||
{
|
||||
throw new ServiceException("Business effect values are required: " + type);
|
||||
}
|
||||
if ("INCREASE_NUMBER".equals(type) || "DECREASE_NUMBER".equals(type))
|
||||
{
|
||||
validateNumberExpression(effect.getAmount(), columnNames, "Business effect amount is invalid");
|
||||
if (StringUtils.isNotBlank(effect.getMinValue()))
|
||||
{
|
||||
throw new ServiceException("业务影响不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(effect.getType()) || effect.getType().length() > 50)
|
||||
{
|
||||
throw new ServiceException("业务影响类型不能为空且不能超过50个字符");
|
||||
}
|
||||
if (StringUtils.isNotBlank(effect.getTargetTable()) && !tableNames.contains(effect.getTargetTable()))
|
||||
{
|
||||
throw new ServiceException("业务影响目标表不存在:" + effect.getTargetTable());
|
||||
}
|
||||
if (StringUtils.isNotBlank(effect.getTargetField()) && !columnNames.contains(effect.getTargetField()))
|
||||
{
|
||||
throw new ServiceException("业务影响目标字段不存在:" + effect.getTargetField());
|
||||
}
|
||||
validateEffectConditionFields(effect, columnNames);
|
||||
if (StringUtils.defaultString(effect.getValue()).length() > 200
|
||||
|| StringUtils.defaultString(effect.getDescription()).length() > 200)
|
||||
{
|
||||
throw new ServiceException("业务影响描述不能超过200个字符");
|
||||
validateNumberExpression(effect.getMinValue(), columnNames, "Business effect min value is invalid");
|
||||
}
|
||||
}
|
||||
validateSafeValue(effect.getValue(), columnNames);
|
||||
if (StringUtils.defaultString(effect.getValue()).length() > 200
|
||||
|| StringUtils.defaultString(effect.getAmount()).length() > 80
|
||||
|| StringUtils.defaultString(effect.getMinValue()).length() > 80
|
||||
|| StringUtils.defaultString(effect.getMessage()).length() > 200
|
||||
|| StringUtils.defaultString(effect.getDescription()).length() > 200)
|
||||
{
|
||||
throw new ServiceException("Business effect text cannot exceed limit");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEffectConditionFields(BusinessActionEffectDesign effect, Set<String> columnNames)
|
||||
@@ -154,11 +246,132 @@ public class AiBusinessBlueprintValidator
|
||||
{
|
||||
if (StringUtils.isBlank(field) || !columnNames.contains(field))
|
||||
{
|
||||
throw new ServiceException("业务影响条件字段不存在:" + StringUtils.defaultString(field));
|
||||
throw new ServiceException("Business effect condition field does not exist: " + StringUtils.defaultString(field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEffectValues(BusinessActionEffectDesign effect, Set<String> columnNames)
|
||||
{
|
||||
if (effect.getValues() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, String> entry : effect.getValues().entrySet())
|
||||
{
|
||||
if (StringUtils.isBlank(entry.getKey()) || !columnNames.contains(entry.getKey()))
|
||||
{
|
||||
throw new ServiceException("Business effect value field does not exist: " + StringUtils.defaultString(entry.getKey()));
|
||||
}
|
||||
validateSafeValue(entry.getValue(), columnNames);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTargetTable(String targetTable, Set<String> tableNames, String messagePrefix)
|
||||
{
|
||||
if (StringUtils.isBlank(targetTable) || !tableNames.contains(targetTable))
|
||||
{
|
||||
throw new ServiceException(messagePrefix + StringUtils.defaultString(targetTable));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConditionFields(List<String> conditionFields, Set<String> columnNames, String messagePrefix)
|
||||
{
|
||||
if (StringUtils.isEmpty(conditionFields))
|
||||
{
|
||||
throw new ServiceException(messagePrefix + "");
|
||||
}
|
||||
for (String field : conditionFields)
|
||||
{
|
||||
if (StringUtils.isBlank(field) || !columnNames.contains(field))
|
||||
{
|
||||
throw new ServiceException(messagePrefix + StringUtils.defaultString(field));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSupportedRuleType(String type)
|
||||
{
|
||||
return "EXISTS".equals(type) || "NOT_EXISTS".equals(type) || "FIELD_EQUALS".equals(type)
|
||||
|| "FIELD_NOT_EQUALS".equals(type) || "FIELD_IN".equals(type) || "NUMBER_GTE".equals(type);
|
||||
}
|
||||
|
||||
private boolean requiresRuleTargetField(String type)
|
||||
{
|
||||
return "FIELD_EQUALS".equals(type) || "FIELD_NOT_EQUALS".equals(type) || "FIELD_IN".equals(type)
|
||||
|| "NUMBER_GTE".equals(type);
|
||||
}
|
||||
|
||||
private boolean requiresExpectedValue(String type)
|
||||
{
|
||||
return "FIELD_EQUALS".equals(type) || "FIELD_NOT_EQUALS".equals(type);
|
||||
}
|
||||
|
||||
private boolean isSupportedEffectType(String type)
|
||||
{
|
||||
return "UPDATE_FIELD".equals(type) || "INSERT_ROW".equals(type) || "UPDATE_FIELDS".equals(type)
|
||||
|| "SET_STATUS".equals(type) || "INCREASE_NUMBER".equals(type) || "DECREASE_NUMBER".equals(type);
|
||||
}
|
||||
|
||||
private boolean requiresEffectTargetField(String type)
|
||||
{
|
||||
return "UPDATE_FIELD".equals(type) || "SET_STATUS".equals(type) || "INCREASE_NUMBER".equals(type)
|
||||
|| "DECREASE_NUMBER".equals(type);
|
||||
}
|
||||
|
||||
private boolean requiresEffectValues(String type)
|
||||
{
|
||||
return "INSERT_ROW".equals(type) || "UPDATE_FIELDS".equals(type);
|
||||
}
|
||||
|
||||
private void validateSafeValue(String value, Set<String> columnNames)
|
||||
{
|
||||
if (StringUtils.isBlank(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Matcher matcher = PARAM_PLACEHOLDER.matcher(value);
|
||||
if (matcher.matches())
|
||||
{
|
||||
String field = matcher.group(1);
|
||||
if (!columnNames.contains(field))
|
||||
{
|
||||
throw new ServiceException("Business effect value references missing field: " + field);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ("${now}".equals(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (value.contains(";") || value.contains("--") || value.contains("`") || value.length() > 200)
|
||||
{
|
||||
throw new ServiceException("Business effect value is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNumberExpression(String value, Set<String> columnNames, String message)
|
||||
{
|
||||
if (StringUtils.isBlank(value))
|
||||
{
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
Matcher matcher = PARAM_PLACEHOLDER.matcher(value);
|
||||
if (matcher.matches())
|
||||
{
|
||||
String field = matcher.group(1);
|
||||
if (!columnNames.contains(field))
|
||||
{
|
||||
throw new ServiceException("Business effect value references missing field: " + field);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!NUMBER_LITERAL.matcher(value).matches())
|
||||
{
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> collectTableNames(List<DatabaseTableDesign> tables)
|
||||
{
|
||||
Set<String> tableNames = new HashSet<String>();
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.generator.service.front;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -26,10 +27,12 @@ import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppRoleDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
|
||||
@@ -99,7 +102,6 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
FrontProject project = getProject(userId, projectId);
|
||||
String confirmedAppBlueprint = resolveConfirmedAppBlueprint(project, request);
|
||||
String prompt = buildPrompt(project, request);
|
||||
long start = System.currentTimeMillis();
|
||||
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "database");
|
||||
@@ -109,11 +111,10 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
DatabaseDesignResponse response = parseResponse(aiContent);
|
||||
response.setProjectId(projectId);
|
||||
normalizeAndBuildSql(response);
|
||||
normalizeBusinessActions(response);
|
||||
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
|
||||
validator.validate(response, maxTables(), maxColumnsPerTable());
|
||||
businessBlueprintValidator.validate(response);
|
||||
persistDatabase(userId, projectId, response);
|
||||
markDatabaseGenerated(projectId, response, confirmedAppBlueprint, request);
|
||||
markDatabaseGenerated(projectId, request);
|
||||
generation.setResponsePayload(aiContent);
|
||||
generation.setSuccess("1");
|
||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||
@@ -130,6 +131,39 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<BusinessActionDesign> generateBusinessBlueprint(Long userId, Long projectId, GenerateBusinessBlueprintRequest request)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
FrontProject project = getProject(userId, projectId);
|
||||
DatabaseDesignResponse database = loadSavedDatabaseDesign(projectId);
|
||||
String prompt = buildBusinessBlueprintPrompt(project, request, database);
|
||||
long start = System.currentTimeMillis();
|
||||
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "business_blueprint");
|
||||
try
|
||||
{
|
||||
String aiContent = deepSeekClient.chat(prompt);
|
||||
DatabaseDesignResponse response = parseBusinessBlueprintResponse(aiContent, database);
|
||||
normalizeBusinessActions(response);
|
||||
businessBlueprintValidator.validate(response);
|
||||
markBusinessBlueprintGenerated(projectId, response.getBusinessActions(), request);
|
||||
generation.setResponsePayload(aiContent);
|
||||
generation.setSuccess("1");
|
||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||
generationRecordService.insert(generation);
|
||||
return response.getBusinessActions();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
generation.setSuccess("0");
|
||||
generation.setErrorMessage(sanitizeErrorMessage(e.getMessage()));
|
||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||
generationRecordService.insert(generation);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFrontUserLoggedIn(Long userId)
|
||||
{
|
||||
if (userId == null)
|
||||
@@ -157,6 +191,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
prompt.append("\"adminMenus\":[{\"code\":\"book_admin\",\"name\":\"Book Admin\",\"path\":\"/admin/books\",\"requiresLogin\":true,\"visibleRoles\":[\"admin\"],\"dataScope\":\"ROLE\"}]}.\n");
|
||||
prompt.append("This blueprint is for the generated business project, not for the current EasyCode/RuoYi platform.\n");
|
||||
prompt.append("Do not generate database tables, menu tables, role tables, permission tables, or code. Only return static role and menu configuration.\n");
|
||||
prompt.append("role.name must be Simplified Chinese so the blueprint summary can display Chinese role names. Keep role.code in lowercase English snake_case.\n");
|
||||
prompt.append("Every menu must include requiresLogin, visibleRoles, and dataScope. dataScope must be PUBLIC, CURRENT_USER, or ROLE.\n");
|
||||
prompt.append("If a menu is for personal data such as my orders, my borrows, profile, reservations, or payments, use requiresLogin=true and dataScope=CURRENT_USER.\n");
|
||||
prompt.append("Project name: ").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
|
||||
@@ -170,9 +205,10 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
{
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
appendAppBlueprintConstraints(prompt, project, request);
|
||||
prompt.append("请为企业管理系统生成MySQL数据库设计和业务蓝图草稿,只返回JSON,格式为{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"车辆信息表\",\"columns\":[]}],\"businessActions\":[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"borrow_record\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\",\"transaction\":true,\"requestFields\":[\"book_id\"],\"rules\":[\"图书必须可借\"],\"effects\":[{\"type\":\"UPDATE_FIELD\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"value\":\"stock - 1\",\"conditionFields\":[\"book_id\"],\"description\":\"库存减一\"}]}]}。\n");
|
||||
prompt.append("Generate a MySQL database design for an enterprise management system. Return JSON only in this shape: ");
|
||||
prompt.append("{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"Car info table\",\"columns\":[]}]}.\n");
|
||||
prompt.append("Do not generate businessActions, business blueprint, workflows, code, menu tables, role tables, or permission tables in this step.\n");
|
||||
prompt.append("限制:表名和字段名使用小写字母、数字、下划线;必须且只能有一个主键;字段类型只能使用bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。\n");
|
||||
prompt.append("业务蓝图限制:businessActions只描述业务动作DSL,不直接返回代码;ownerTable、requestFields、effects.targetTable、effects.targetField、effects.conditionFields必须引用已生成的表和字段;method只用GET、POST、PUT、DELETE;effects.type优先使用UPDATE_FIELD,conditionFields表示更新或删除时的WHERE条件字段。\n");
|
||||
prompt.append("项目名称:").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
|
||||
prompt.append("项目描述:").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
|
||||
prompt.append("行业模板:").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
|
||||
@@ -181,11 +217,6 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
private String firstNonBlank(String first, String second)
|
||||
{
|
||||
return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second);
|
||||
}
|
||||
|
||||
private void appendAppBlueprintConstraints(StringBuilder prompt, FrontProject project, GenerateDatabaseRequest request)
|
||||
{
|
||||
String confirmedAppBlueprint = resolveConfirmedAppBlueprint(project, request);
|
||||
@@ -198,7 +229,6 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
prompt.append("- Do not generate menu or permission tables for frontendMenus, adminMenus, roles, or visibleRoles.\n");
|
||||
prompt.append("- Menus are static generated project configuration, not database rows.\n");
|
||||
prompt.append("- Any feature behind requiresLogin=true and dataScope=CURRENT_USER must associate data with the current user, usually through a user_id field.\n");
|
||||
prompt.append("- Business actions must align with the confirmed menus and roles.\n");
|
||||
}
|
||||
|
||||
private String resolveConfirmedAppBlueprint(FrontProject project, GenerateDatabaseRequest request)
|
||||
@@ -206,6 +236,64 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return firstNonBlank(request == null ? null : request.getAppBlueprint(), project.getAppBlueprint());
|
||||
}
|
||||
|
||||
private String buildBusinessBlueprintPrompt(FrontProject project, GenerateBusinessBlueprintRequest request, DatabaseDesignResponse database)
|
||||
{
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
prompt.append("Generate a business blueprint from the saved database schema. Return JSON only in this shape: ");
|
||||
prompt.append("{\"businessActions\":[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"borrow_record\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\",\"transaction\":true,\"requestFields\":[\"book_id\"],\"rules\":[\"图书必须可借\"],\"ruleChecks\":[{\"type\":\"NUMBER_GTE\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"compareValue\":\"1\",\"conditionFields\":[\"book_id\"],\"message\":\"库存不足\"}],\"effects\":[{\"type\":\"INSERT_ROW\",\"targetTable\":\"borrow_record\",\"values\":{\"book_id\":\"${param.book_id}\",\"status\":\"borrowed\"},\"description\":\"创建借阅记录\"},{\"type\":\"DECREASE_NUMBER\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"amount\":\"1\",\"minValue\":\"0\",\"conditionFields\":[\"book_id\"],\"message\":\"库存扣减失败\",\"description\":\"库存减一\"}]}]}.\n");
|
||||
prompt.append("Do not generate CRUD. Do not generate create, read, update, delete, list, detail, import, export, or simple table maintenance actions.\n");
|
||||
prompt.append("Generate only business workflows that go beyond CRUD, such as approval, payment confirmation, borrow, return, stock adjustment by workflow, scheduling, check-in, settlement, or status transition.\n");
|
||||
prompt.append("Do not generate database tables or code. businessActions only describes a safe DSL.\n");
|
||||
prompt.append("Supported ruleChecks are EXISTS, NOT_EXISTS, FIELD_EQUALS, FIELD_NOT_EQUALS, FIELD_IN, NUMBER_GTE.\n");
|
||||
prompt.append("Supported effects are INSERT_ROW, UPDATE_FIELDS, SET_STATUS, INCREASE_NUMBER, DECREASE_NUMBER.\n");
|
||||
prompt.append("businessActions.name, rules, ruleChecks.message, effects.message, and effects.description must be Simplified Chinese. Keep API paths and request parameter fields as technical identifiers.\n");
|
||||
prompt.append("ownerTable, requestFields, effects.targetTable, effects.targetField, and effects.conditionFields must reference the saved database schema below.\n");
|
||||
prompt.append("method must be GET, POST, PUT, or DELETE. Prefer POST for command workflows.\n");
|
||||
prompt.append("Project name: ").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
|
||||
prompt.append("Project description: ").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
|
||||
prompt.append("Industry template: ").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
|
||||
prompt.append("Extra requirements: ").append(request == null ? "" : StringUtils.defaultString(request.getExtraRequirements())).append("\n");
|
||||
prompt.append("Saved database schema:\n");
|
||||
appendSavedDatabaseSchema(prompt, database);
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
private void appendSavedDatabaseSchema(StringBuilder prompt, DatabaseDesignResponse database)
|
||||
{
|
||||
if (database == null || database.getTables() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (DatabaseTableDesign table : database.getTables())
|
||||
{
|
||||
if (table == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
prompt.append("- ").append(table.getTableName()).append(" (")
|
||||
.append(StringUtils.defaultString(table.getTableComment())).append(")\n");
|
||||
if (table.getColumns() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (DatabaseColumnDesign column : table.getColumns())
|
||||
{
|
||||
if (column == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
prompt.append(" - ").append(column.getColumnName()).append(" ")
|
||||
.append(StringUtils.defaultString(column.getColumnType())).append(" ")
|
||||
.append(StringUtils.defaultString(column.getColumnComment())).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String firstNonBlank(String first, String second)
|
||||
{
|
||||
return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second);
|
||||
}
|
||||
|
||||
private AppBlueprintDesign parseAppBlueprintResponse(String aiContent)
|
||||
{
|
||||
String content = stripMarkdownFence(aiContent);
|
||||
@@ -356,6 +444,29 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return response;
|
||||
}
|
||||
|
||||
private DatabaseDesignResponse parseBusinessBlueprintResponse(String aiContent, DatabaseDesignResponse savedDatabase)
|
||||
{
|
||||
String content = stripMarkdownFence(aiContent);
|
||||
JSONObject json = JSON.parseObject(content);
|
||||
if (json == null)
|
||||
{
|
||||
throw new ServiceException("AI business blueprint is empty");
|
||||
}
|
||||
DatabaseDesignResponse response = new DatabaseDesignResponse();
|
||||
response.setProjectId(savedDatabase.getProjectId());
|
||||
response.setTables(savedDatabase.getTables());
|
||||
JSONArray actions = json.getJSONArray("businessActions");
|
||||
if (actions == null)
|
||||
{
|
||||
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
|
||||
}
|
||||
else
|
||||
{
|
||||
response.setBusinessActions(actions.toJavaList(BusinessActionDesign.class));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private void normalizeResponseAliases(JSONObject response)
|
||||
{
|
||||
if (response == null)
|
||||
@@ -512,6 +623,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
Set<String> actionCodes = new HashSet<String>();
|
||||
Set<String> columnNames = collectColumnNames(response);
|
||||
Map<String, Set<String>> columnNamesByTable = collectColumnNamesByTable(response);
|
||||
List<BusinessActionDesign> normalizedActions = new ArrayList<BusinessActionDesign>();
|
||||
int actionIndex = 1;
|
||||
for (BusinessActionDesign action : response.getBusinessActions())
|
||||
{
|
||||
@@ -528,9 +640,53 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
action.setTransaction(action.getTransaction() == null ? Boolean.TRUE : action.getTransaction());
|
||||
action.setRequestFields(normalizeExistingFieldList(action.getRequestFields(), columnNames));
|
||||
action.setRules(trimStringList(action.getRules(), 200));
|
||||
normalizeBusinessRuleChecks(action, columnNames, columnNamesByTable);
|
||||
normalizeBusinessEffects(action, columnNames, columnNamesByTable);
|
||||
if (!isCrudBusinessAction(action))
|
||||
{
|
||||
normalizedActions.add(action);
|
||||
}
|
||||
actionIndex++;
|
||||
}
|
||||
response.setBusinessActions(normalizedActions);
|
||||
}
|
||||
|
||||
private boolean isCrudBusinessAction(BusinessActionDesign action)
|
||||
{
|
||||
String code = StringUtils.defaultString(action.getCode()).toLowerCase(Locale.ENGLISH);
|
||||
String name = normalizeDatabaseName(action.getName(), "action");
|
||||
String path = StringUtils.defaultString(action.getPath()).toLowerCase(Locale.ENGLISH);
|
||||
return startsWithCrudToken(code) || startsWithCrudToken(name) || containsCrudPathToken(path);
|
||||
}
|
||||
|
||||
private boolean startsWithCrudToken(String value)
|
||||
{
|
||||
String[] tokens = new String[] {"create", "add", "new", "read", "query", "list", "page", "get", "view",
|
||||
"detail", "edit", "update", "delete", "remove", "import", "export"};
|
||||
String normalized = StringUtils.defaultString(value).toLowerCase(Locale.ENGLISH);
|
||||
for (String token : tokens)
|
||||
{
|
||||
if (normalized.equals(token) || normalized.startsWith(token + "_") || normalized.startsWith(token + "-"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsCrudPathToken(String path)
|
||||
{
|
||||
String[] tokens = new String[] {"create", "add", "list", "page", "detail", "edit", "update", "delete",
|
||||
"remove", "import", "export"};
|
||||
String normalized = StringUtils.defaultString(path).toLowerCase(Locale.ENGLISH);
|
||||
for (String token : tokens)
|
||||
{
|
||||
if (normalized.endsWith("/" + token) || normalized.contains("/" + token + "/"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String uniqueBusinessCode(String rawCode, int index, Set<String> usedCodes)
|
||||
@@ -663,6 +819,57 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return normalizedValues;
|
||||
}
|
||||
|
||||
private void normalizeBusinessRuleChecks(BusinessActionDesign action, Set<String> columnNames, Map<String, Set<String>> columnNamesByTable)
|
||||
{
|
||||
if (action.getRuleChecks() == null)
|
||||
{
|
||||
action.setRuleChecks(new ArrayList<BusinessActionRuleDesign>());
|
||||
return;
|
||||
}
|
||||
List<BusinessActionRuleDesign> normalizedRules = new ArrayList<BusinessActionRuleDesign>();
|
||||
int ruleIndex = 1;
|
||||
for (BusinessActionRuleDesign rule : action.getRuleChecks())
|
||||
{
|
||||
if (rule == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
rule.setType(normalizeRuleType(rule.getType(), ruleIndex));
|
||||
rule.setTargetTable(StringUtils.isBlank(rule.getTargetTable()) ? "" : normalizeDatabaseName(rule.getTargetTable(), "table_" + ruleIndex));
|
||||
String rawTargetField = StringUtils.isBlank(rule.getTargetField()) ? "" : normalizeDatabaseName(rule.getTargetField(), "field_" + ruleIndex);
|
||||
rule.setTargetField(resolveStrictTargetField(rawTargetField, rule.getTargetTable(), columnNamesByTable, columnNames));
|
||||
rule.setExpectedValue(trimToLength(rule.getExpectedValue(), 200));
|
||||
rule.setExpectedValues(trimStringList(rule.getExpectedValues(), 200));
|
||||
rule.setCompareValue(trimToLength(rule.getCompareValue(), 80));
|
||||
rule.setConditionFields(normalizeExistingFieldList(rule.getConditionFields(), columnNames));
|
||||
rule.setMessage(trimToLength(rule.getMessage(), 200));
|
||||
normalizedRules.add(rule);
|
||||
ruleIndex++;
|
||||
}
|
||||
action.setRuleChecks(normalizedRules);
|
||||
}
|
||||
|
||||
private String resolveStrictTargetField(String targetField, String targetTable, Map<String, Set<String>> columnNamesByTable, Set<String> columnNames)
|
||||
{
|
||||
if (StringUtils.isBlank(targetField))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
Set<String> tableColumnNames = columnNamesByTable == null ? null : columnNamesByTable.get(targetTable);
|
||||
if (tableColumnNames != null)
|
||||
{
|
||||
return tableColumnNames.contains(targetField) ? targetField : "";
|
||||
}
|
||||
return columnNames != null && columnNames.contains(targetField) ? targetField : "";
|
||||
}
|
||||
|
||||
private String normalizeRuleType(String type, int index)
|
||||
{
|
||||
String normalized = StringUtils.defaultString(type).trim().toUpperCase(Locale.ENGLISH);
|
||||
normalized = normalized.replaceAll("[^A-Z0-9]+", "_").replaceAll("_+", "_").replaceAll("^_+|_+$", "");
|
||||
return StringUtils.defaultIfEmpty(normalized, "RULE_" + index);
|
||||
}
|
||||
|
||||
private void normalizeBusinessEffects(BusinessActionDesign action, Set<String> columnNames, Map<String, Set<String>> columnNamesByTable)
|
||||
{
|
||||
if (action.getEffects() == null)
|
||||
@@ -689,11 +896,15 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
effect.setTargetField(targetField);
|
||||
effect.setValue(trimToLength(effect.getValue(), 200));
|
||||
effect.setValues(normalizeEffectValues(effect.getValues(), effect.getTargetTable(), columnNamesByTable, columnNames));
|
||||
effect.setAmount(trimToLength(effect.getAmount(), 80));
|
||||
effect.setMinValue(trimToLength(effect.getMinValue(), 80));
|
||||
if (StringUtils.isNotBlank(rawTargetField) && StringUtils.isNotBlank(targetField) && !rawTargetField.equals(targetField))
|
||||
{
|
||||
effect.setValue(rewriteEffectValue(effect.getValue(), rawTargetField, targetField));
|
||||
}
|
||||
effect.setConditionFields(normalizeExistingFieldList(effect.getConditionFields(), columnNames));
|
||||
effect.setMessage(trimToLength(effect.getMessage(), 200));
|
||||
effect.setDescription(trimToLength(effect.getDescription(), 200));
|
||||
normalizedEffects.add(effect);
|
||||
effectIndex++;
|
||||
@@ -701,6 +912,30 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
action.setEffects(normalizedEffects);
|
||||
}
|
||||
|
||||
private Map<String, String> normalizeEffectValues(Map<String, String> values, String targetTable,
|
||||
Map<String, Set<String>> columnNamesByTable, Set<String> columnNames)
|
||||
{
|
||||
Map<String, String> normalizedValues = new LinkedHashMap<String, String>();
|
||||
if (values == null)
|
||||
{
|
||||
return normalizedValues;
|
||||
}
|
||||
Set<String> tableColumnNames = columnNamesByTable == null ? null : columnNamesByTable.get(targetTable);
|
||||
int valueIndex = 1;
|
||||
for (Map.Entry<String, String> entry : values.entrySet())
|
||||
{
|
||||
String fieldName = normalizeDatabaseName(entry.getKey(), "field_" + valueIndex);
|
||||
boolean exists = tableColumnNames != null ? tableColumnNames.contains(fieldName)
|
||||
: columnNames != null && columnNames.contains(fieldName);
|
||||
if (exists)
|
||||
{
|
||||
normalizedValues.put(fieldName, trimToLength(entry.getValue(), 200));
|
||||
}
|
||||
valueIndex++;
|
||||
}
|
||||
return normalizedValues;
|
||||
}
|
||||
|
||||
private String resolveEffectTargetField(String targetField, String targetTable, Map<String, Set<String>> columnNamesByTable, Set<String> columnNames)
|
||||
{
|
||||
if (StringUtils.isBlank(targetField))
|
||||
@@ -1052,6 +1287,43 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseDesignResponse loadSavedDatabaseDesign(Long projectId)
|
||||
{
|
||||
List<FrontProjectTable> savedTables = frontProjectTableMapper.selectTablesByProjectId(projectId);
|
||||
if (savedTables == null || savedTables.isEmpty())
|
||||
{
|
||||
throw new ServiceException("Please save database design before generating business blueprint");
|
||||
}
|
||||
DatabaseDesignResponse response = new DatabaseDesignResponse();
|
||||
response.setProjectId(projectId);
|
||||
List<DatabaseTableDesign> tables = new ArrayList<DatabaseTableDesign>();
|
||||
for (FrontProjectTable savedTable : savedTables)
|
||||
{
|
||||
if (savedTable == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
DatabaseTableDesign table = toDatabaseTableDesign(savedTable);
|
||||
List<FrontProjectColumn> savedColumns = frontProjectColumnMapper.selectColumnsByTableId(savedTable.getTableId());
|
||||
List<DatabaseColumnDesign> columns = new ArrayList<DatabaseColumnDesign>();
|
||||
if (savedColumns != null)
|
||||
{
|
||||
for (FrontProjectColumn savedColumn : savedColumns)
|
||||
{
|
||||
if (savedColumn != null)
|
||||
{
|
||||
columns.add(toDatabaseColumnDesign(savedColumn));
|
||||
}
|
||||
}
|
||||
}
|
||||
table.setColumns(columns);
|
||||
tables.add(table);
|
||||
}
|
||||
response.setTables(tables);
|
||||
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
|
||||
return response;
|
||||
}
|
||||
|
||||
private void persistDatabase(Long userId, Long projectId, DatabaseDesignResponse response)
|
||||
{
|
||||
frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
|
||||
@@ -1068,20 +1340,28 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
}
|
||||
}
|
||||
|
||||
private void markDatabaseGenerated(Long projectId, DatabaseDesignResponse response, String confirmedAppBlueprint, GenerateDatabaseRequest request)
|
||||
private void markDatabaseGenerated(Long projectId, GenerateDatabaseRequest request)
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(projectId);
|
||||
project.setGenerateStatus("1");
|
||||
project.setPreviewStatus("0");
|
||||
if (StringUtils.isNotBlank(confirmedAppBlueprint))
|
||||
{
|
||||
project.setAppBlueprint(confirmedAppBlueprint);
|
||||
}
|
||||
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
||||
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
||||
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
||||
project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions()));
|
||||
project.setBusinessBlueprint("[]");
|
||||
frontProjectMapper.updateFrontProject(project);
|
||||
}
|
||||
|
||||
private void markBusinessBlueprintGenerated(Long projectId, List<BusinessActionDesign> actions, GenerateBusinessBlueprintRequest request)
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(projectId);
|
||||
project.setPreviewStatus("0");
|
||||
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
|
||||
project.setProjectDesc(request == null ? null : request.getProjectDesc());
|
||||
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
|
||||
project.setBusinessBlueprint(JSON.toJSONString(actions == null ? new ArrayList<BusinessActionDesign>() : actions));
|
||||
frontProjectMapper.updateFrontProject(project);
|
||||
}
|
||||
|
||||
@@ -1124,6 +1404,49 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
||||
return sanitized.substring(0, MAX_ERROR_MESSAGE_LENGTH);
|
||||
}
|
||||
|
||||
private DatabaseTableDesign toDatabaseTableDesign(FrontProjectTable source)
|
||||
{
|
||||
DatabaseTableDesign table = new DatabaseTableDesign();
|
||||
table.setTableId(source.getTableId());
|
||||
table.setTableName(source.getTableName());
|
||||
table.setTableComment(source.getTableComment());
|
||||
table.setClassName(source.getClassName());
|
||||
table.setModuleName(source.getModuleName());
|
||||
table.setBusinessName(source.getBusinessName());
|
||||
table.setFunctionName(source.getFunctionName());
|
||||
table.setTplCategory(source.getTplCategory());
|
||||
table.setTplWebType(source.getTplWebType());
|
||||
table.setOptions(source.getOptions());
|
||||
table.setCreateTableSql(source.getCreateTableSql());
|
||||
table.setSort(source.getSort());
|
||||
return table;
|
||||
}
|
||||
|
||||
private DatabaseColumnDesign toDatabaseColumnDesign(FrontProjectColumn source)
|
||||
{
|
||||
DatabaseColumnDesign column = new DatabaseColumnDesign();
|
||||
column.setColumnId(source.getColumnId());
|
||||
column.setTableId(source.getTableId());
|
||||
column.setColumnName(source.getColumnName());
|
||||
column.setColumnComment(source.getColumnComment());
|
||||
column.setColumnType(source.getColumnType());
|
||||
column.setJavaType(source.getJavaType());
|
||||
column.setJavaField(source.getJavaField());
|
||||
column.setIsPk(source.getIsPk());
|
||||
column.setIsIncrement(source.getIsIncrement());
|
||||
column.setIsRequired(source.getIsRequired());
|
||||
column.setIsInsert(source.getIsInsert());
|
||||
column.setIsEdit(source.getIsEdit());
|
||||
column.setIsList(source.getIsList());
|
||||
column.setIsQuery(source.getIsQuery());
|
||||
column.setQueryType(source.getQueryType());
|
||||
column.setHtmlType(source.getHtmlType());
|
||||
column.setDictType(source.getDictType());
|
||||
column.setDefaultValue(source.getDefaultValue());
|
||||
column.setSort(source.getSort());
|
||||
return column;
|
||||
}
|
||||
|
||||
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
|
||||
@@ -30,7 +30,9 @@ public class FrontProjectConverter
|
||||
genProject.setProjectDesc(project.getProjectDesc());
|
||||
genProject.setFrontFramework(project.getFrontFramework());
|
||||
genProject.setBackFramework(project.getBackFramework());
|
||||
genProject.setFrontendEnabled(project.getFrontendEnabled());
|
||||
genProject.setStatus(project.getStatus());
|
||||
genProject.setModules(project.getModules());
|
||||
genProject.setTables(toGenTables(project.getTables(), project.getPackageName()));
|
||||
AppBlueprintDesign appBlueprint = parseAppBlueprint(project.getAppBlueprint());
|
||||
genProject.setAppBlueprint(appBlueprint);
|
||||
|
||||
@@ -40,6 +40,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
public List<Map<String, Object>> getStructure(Long userId, Long projectId, String templateType)
|
||||
{
|
||||
GenProject genProject = loadGenProject(userId, projectId);
|
||||
assertTemplateTypeEnabled(genProject, templateType);
|
||||
return genProjectService.getProjectStructure(genProject, templateType);
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
throw new ServiceException("预览文件参数不能为空");
|
||||
}
|
||||
GenProject genProject = loadGenProject(userId, projectId);
|
||||
assertTemplateTypeEnabled(genProject, request.getTemplateType());
|
||||
return genProjectService.previewCode(genProject, request.getTableId(), request.getCategory(), request.getTemplateType());
|
||||
}
|
||||
|
||||
@@ -58,6 +60,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
public byte[] download(Long userId, Long projectId, String templateType)
|
||||
{
|
||||
GenProject genProject = loadGenProject(userId, projectId);
|
||||
assertTemplateTypeEnabled(genProject, templateType);
|
||||
return genProjectService.downloadStructure(genProject, templateType);
|
||||
}
|
||||
|
||||
@@ -69,7 +72,10 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
try (ZipOutputStream zip = new ZipOutputStream(outputStream))
|
||||
{
|
||||
appendZip(zip, "backend", genProjectService.downloadStructure(genProject, "backend"));
|
||||
appendZip(zip, "front-web", genProjectService.downloadStructure(genProject, "frontend"));
|
||||
if (isFrontendEnabled(genProject))
|
||||
{
|
||||
appendZip(zip, "front-web", genProjectService.downloadStructure(genProject, "frontend"));
|
||||
}
|
||||
appendZip(zip, "admin-web", genProjectService.downloadStructure(genProject, "admin_frontend"));
|
||||
}
|
||||
catch (IOException e)
|
||||
@@ -108,6 +114,19 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
|
||||
return FrontProjectConverter.toGenProject(frontProject);
|
||||
}
|
||||
|
||||
private void assertTemplateTypeEnabled(GenProject project, String templateType)
|
||||
{
|
||||
if ("frontend".equals(templateType) && !isFrontendEnabled(project))
|
||||
{
|
||||
throw new ServiceException("该项目未启用前台前端");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFrontendEnabled(GenProject project)
|
||||
{
|
||||
return project == null || !"0".equals(project.getFrontendEnabled());
|
||||
}
|
||||
|
||||
private void appendZip(ZipOutputStream targetZip, String rootFolder, byte[] sourceData) throws IOException
|
||||
{
|
||||
ZipInputStream sourceZip = new ZipInputStream(new ByteArrayInputStream(sourceData));
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@@ -11,9 +13,13 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.SysModule;
|
||||
import com.ruoyi.generator.domain.SysProjectModule;
|
||||
import com.ruoyi.generator.domain.front.FrontProject;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectModule;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
|
||||
@@ -25,7 +31,9 @@ import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectFileMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectModuleMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
|
||||
import com.ruoyi.generator.service.ISysModuleService;
|
||||
|
||||
@Service
|
||||
public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
@@ -49,8 +57,13 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
private FrontProjectFileMapper frontProjectFileMapper;
|
||||
@Autowired
|
||||
private FrontProjectGenerationMapper frontProjectGenerationMapper;
|
||||
@Autowired
|
||||
private FrontProjectModuleMapper frontProjectModuleMapper;
|
||||
@Autowired
|
||||
private ISysModuleService sysModuleService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public FrontProject createProject(Long userId, FrontProjectCreateRequest request)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
@@ -69,10 +82,13 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
project.setProjectFileName(toProjectFileName(request.getProjectName()));
|
||||
project.setFrontFramework("Vue3 + Element Plus");
|
||||
project.setBackFramework("Spring Boot + MyBatis");
|
||||
project.setFrontendEnabled(toFrontendEnabled(request.getFrontendEnabled()));
|
||||
project.setGenerateStatus("0");
|
||||
project.setPreviewStatus("0");
|
||||
project.setStatus("0");
|
||||
frontProjectMapper.insertFrontProject(project);
|
||||
saveProjectModules(project.getProjectId(), request.getModuleIds());
|
||||
attachProjectModules(project);
|
||||
return project;
|
||||
}
|
||||
|
||||
@@ -82,7 +98,15 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
assertFrontUserLoggedIn(userId);
|
||||
FrontProject query = new FrontProject();
|
||||
query.setUserId(userId);
|
||||
return frontProjectMapper.selectFrontProjectList(query);
|
||||
List<FrontProject> projects = frontProjectMapper.selectFrontProjectList(query);
|
||||
if (projects != null)
|
||||
{
|
||||
for (FrontProject project : projects)
|
||||
{
|
||||
attachProjectModules(project);
|
||||
}
|
||||
}
|
||||
return projects;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,10 +118,12 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
{
|
||||
throw new ServiceException("项目不存在或无权限");
|
||||
}
|
||||
attachProjectModules(project);
|
||||
return project;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateProject(Long userId, Long projectId, FrontProjectUpdateRequest request)
|
||||
{
|
||||
assertFrontUserLoggedIn(userId);
|
||||
@@ -115,7 +141,13 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
project.setIndustryTemplate(request.getIndustryTemplate());
|
||||
project.setPackageName(request.getPackageName());
|
||||
project.setVersion(request.getVersion());
|
||||
return frontProjectMapper.updateFrontProject(project);
|
||||
project.setFrontendEnabled(toNullableFrontendEnabled(request.getFrontendEnabled()));
|
||||
int rows = frontProjectMapper.updateFrontProject(project);
|
||||
if (request.getModuleIds() != null)
|
||||
{
|
||||
saveProjectModules(projectId, request.getModuleIds());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,6 +158,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
getProject(userId, projectId);
|
||||
frontProjectFileMapper.deleteFilesByProjectId(projectId);
|
||||
frontProjectGenerationMapper.deleteGenerationsByProjectId(projectId);
|
||||
frontProjectModuleMapper.deleteModulesByProjectId(projectId);
|
||||
frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
|
||||
frontProjectTableMapper.deleteTablesByProjectId(projectId);
|
||||
return frontProjectMapper.deleteFrontProjectByUserAndId(userId, projectId);
|
||||
@@ -180,6 +213,15 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
return getDatabase(userId, projectId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysModule> listAvailableModules()
|
||||
{
|
||||
SysModule query = new SysModule();
|
||||
query.setStatus("0");
|
||||
query.setDelFlag("0");
|
||||
return sysModuleService.selectSysModuleList(query);
|
||||
}
|
||||
|
||||
private void assertFrontUserLoggedIn(Long userId)
|
||||
{
|
||||
if (userId == null)
|
||||
@@ -202,6 +244,65 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private String toFrontendEnabled(Boolean frontendEnabled)
|
||||
{
|
||||
return Boolean.FALSE.equals(frontendEnabled) ? "0" : "1";
|
||||
}
|
||||
|
||||
private String toNullableFrontendEnabled(Boolean frontendEnabled)
|
||||
{
|
||||
return frontendEnabled == null ? null : toFrontendEnabled(frontendEnabled);
|
||||
}
|
||||
|
||||
private void attachProjectModules(FrontProject project)
|
||||
{
|
||||
if (project == null || project.getProjectId() == null || frontProjectModuleMapper == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<SysProjectModule> modules = frontProjectModuleMapper.selectSysProjectModulesByProjectId(project.getProjectId());
|
||||
project.setModules(modules);
|
||||
List<Long> moduleIds = new ArrayList<Long>();
|
||||
if (modules != null)
|
||||
{
|
||||
for (SysProjectModule module : modules)
|
||||
{
|
||||
if (module != null && module.getModuleId() != null)
|
||||
{
|
||||
moduleIds.add(module.getModuleId());
|
||||
}
|
||||
}
|
||||
}
|
||||
project.setModuleIds(moduleIds);
|
||||
}
|
||||
|
||||
private void saveProjectModules(Long projectId, List<Long> moduleIds)
|
||||
{
|
||||
if (projectId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
frontProjectModuleMapper.deleteModulesByProjectId(projectId);
|
||||
if (moduleIds == null || moduleIds.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
Set<Long> uniqueModuleIds = new LinkedHashSet<Long>(moduleIds);
|
||||
for (Long moduleId : uniqueModuleIds)
|
||||
{
|
||||
if (moduleId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FrontProjectModule module = new FrontProjectModule();
|
||||
module.setProjectId(projectId);
|
||||
module.setModuleId(moduleId);
|
||||
module.setStatus("0");
|
||||
module.setCreateTime(DateUtils.getNowDate());
|
||||
frontProjectModuleMapper.insertFrontProjectModule(module);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDatabaseDesign(DatabaseDesignResponse request)
|
||||
{
|
||||
if (request == null || request.getTables() == null || request.getTables().isEmpty())
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
|
||||
|
||||
public interface IAiGenerateService
|
||||
@@ -10,4 +13,6 @@ public interface IAiGenerateService
|
||||
AppBlueprintDesign generateAppBlueprint(Long userId, Long projectId, GenerateAppBlueprintRequest request);
|
||||
|
||||
DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request);
|
||||
|
||||
List<BusinessActionDesign> generateBusinessBlueprint(Long userId, Long projectId, GenerateBusinessBlueprintRequest request);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.generator.domain.SysModule;
|
||||
import com.ruoyi.generator.domain.front.FrontProject;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
|
||||
@@ -15,4 +16,5 @@ public interface IFrontProjectService
|
||||
public int deleteProject(Long userId, Long projectId);
|
||||
public DatabaseDesignResponse getDatabase(Long userId, Long projectId);
|
||||
public DatabaseDesignResponse saveDatabase(Long userId, Long projectId, DatabaseDesignResponse request);
|
||||
public List<SysModule> listAvailableModules();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="industryTemplate" column="industry_template" />
|
||||
<result property="frontFramework" column="front_framework" />
|
||||
<result property="backFramework" column="back_framework" />
|
||||
<result property="frontendEnabled" column="frontend_enabled" />
|
||||
<result property="generateStatus" column="generate_status" />
|
||||
<result property="previewStatus" column="preview_status" />
|
||||
<result property="appBlueprint" column="app_blueprint" />
|
||||
@@ -28,7 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectFrontProjectVo">
|
||||
select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, requirement_keyword, industry_template, front_framework, back_framework, generate_status, preview_status, app_blueprint, er_diagram, business_blueprint, status, create_time, update_time, remark from front_project
|
||||
select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, requirement_keyword, industry_template, front_framework, back_framework, frontend_enabled, generate_status, preview_status, app_blueprint, er_diagram, business_blueprint, status, create_time, update_time, remark from front_project
|
||||
</sql>
|
||||
|
||||
<select id="selectFrontProjectById" parameterType="Long" resultMap="FrontProjectResult">
|
||||
@@ -67,6 +68,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="industryTemplate != null">industry_template,</if>
|
||||
<if test="frontFramework != null">front_framework,</if>
|
||||
<if test="backFramework != null">back_framework,</if>
|
||||
<if test="frontendEnabled != null">frontend_enabled,</if>
|
||||
<if test="generateStatus != null">generate_status,</if>
|
||||
<if test="previewStatus != null">preview_status,</if>
|
||||
<if test="appBlueprint != null">app_blueprint,</if>
|
||||
@@ -87,6 +89,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="industryTemplate != null">#{industryTemplate},</if>
|
||||
<if test="frontFramework != null">#{frontFramework},</if>
|
||||
<if test="backFramework != null">#{backFramework},</if>
|
||||
<if test="frontendEnabled != null">#{frontendEnabled},</if>
|
||||
<if test="generateStatus != null">#{generateStatus},</if>
|
||||
<if test="previewStatus != null">#{previewStatus},</if>
|
||||
<if test="appBlueprint != null">#{appBlueprint},</if>
|
||||
@@ -110,6 +113,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="industryTemplate != null">industry_template = #{industryTemplate},</if>
|
||||
<if test="frontFramework != null">front_framework = #{frontFramework},</if>
|
||||
<if test="backFramework != null">back_framework = #{backFramework},</if>
|
||||
<if test="frontendEnabled != null">frontend_enabled = #{frontendEnabled},</if>
|
||||
<if test="generateStatus != null">generate_status = #{generateStatus},</if>
|
||||
<if test="previewStatus != null">preview_status = #{previewStatus},</if>
|
||||
<if test="appBlueprint != null">app_blueprint = #{appBlueprint},</if>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.generator.mapper.front.FrontProjectModuleMapper">
|
||||
|
||||
<resultMap type="FrontProjectModule" id="FrontProjectModuleResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="moduleId" column="module_id" />
|
||||
<result property="status" column="status" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="SysProjectModule" id="SysProjectModuleResult">
|
||||
<id property="id" column="id" />
|
||||
<result property="projectId" column="project_id" />
|
||||
<result property="moduleId" column="module_id" />
|
||||
<result property="status" column="status" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<select id="selectModulesByProjectId" parameterType="Long" resultMap="FrontProjectModuleResult">
|
||||
select id, project_id, module_id, status, create_time, update_time, remark
|
||||
from front_project_module
|
||||
where project_id = #{projectId}
|
||||
and status = '0'
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectSysProjectModulesByProjectId" parameterType="Long" resultMap="SysProjectModuleResult">
|
||||
select id, project_id, module_id, status, create_time, update_time, remark
|
||||
from front_project_module
|
||||
where project_id = #{projectId}
|
||||
and status = '0'
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<insert id="insertFrontProjectModule" parameterType="FrontProjectModule" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into front_project_module
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="projectId != null">project_id,</if>
|
||||
<if test="moduleId != null">module_id,</if>
|
||||
<if test="status != null">status,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
<if test="remark != null">remark,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="projectId != null">#{projectId},</if>
|
||||
<if test="moduleId != null">#{moduleId},</if>
|
||||
<if test="status != null">#{status},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
<if test="remark != null">#{remark},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<delete id="deleteModulesByProjectId" parameterType="Long">
|
||||
delete from front_project_module where project_id = #{projectId}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -29,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="moduleAlias != null and moduleAlias != ''"> and module_alias = #{moduleAlias}</if>
|
||||
<if test="moduleDesc != null and moduleDesc != ''"> and module_desc = #{moduleDesc}</if>
|
||||
<if test="status != null and status != ''"> and status = #{status}</if>
|
||||
<if test="delFlag != null and delFlag != ''"> and del_flag = #{delFlag}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
@@ -92,4 +93,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
#{moduleId}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -13,6 +13,10 @@ import com.ruoyi.generator.domain.TemplateFile;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.mapper.GenProjectMapper;
|
||||
import com.ruoyi.generator.mapper.GenTableMapper;
|
||||
import com.ruoyi.generator.util.VelocityInitializer;
|
||||
import com.ruoyi.generator.util.VelocityUtils;
|
||||
import org.apache.velocity.VelocityContext;
|
||||
import org.apache.velocity.app.Velocity;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -20,6 +24,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -183,6 +188,14 @@ public class GenProjectServiceImplTest {
|
||||
|
||||
assertFalse(sql.contains("Business action not implemented"));
|
||||
assertTrue(sql.contains("executeUpdateFieldEffect"));
|
||||
assertTrue(sql.contains("assertExists"));
|
||||
assertTrue(sql.contains("assertFieldEquals"));
|
||||
assertTrue(sql.contains("assertNumberGte"));
|
||||
assertTrue(sql.contains("insertRow"));
|
||||
assertTrue(sql.contains("updateFields"));
|
||||
assertTrue(sql.contains("setStatus"));
|
||||
assertTrue(sql.contains("increaseNumberField"));
|
||||
assertTrue(sql.contains("decreaseNumberField"));
|
||||
assertTrue(sql.contains("jdbcTemplate.update"));
|
||||
assertTrue(sql.contains("conditionFields"));
|
||||
}
|
||||
@@ -204,6 +217,24 @@ public class GenProjectServiceImplTest {
|
||||
assertFalse(sql.contains("insert into generated_menu"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintSqlVueTemplatesAreVelocityParseable() throws Exception {
|
||||
String sql = readBusinessBlueprintSql();
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
table.setPackageName("com.example.demo");
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
context.put("hasTableBusinessActions", true);
|
||||
context.put("tableBusinessActions", Arrays.asList(businessAction("sync_user", "sys_user")));
|
||||
VelocityInitializer.initVelocity();
|
||||
|
||||
for (String templateFileId : Arrays.asList("910108", "910203", "910308", "910309")) {
|
||||
StringWriter output = new StringWriter();
|
||||
Velocity.evaluate(context, output, "", extractTemplateSqlContent(sql, templateFileId));
|
||||
assertTrue(output.toString().contains("sync_user"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
|
||||
GenProject project = project();
|
||||
@@ -266,6 +297,47 @@ public class GenProjectServiceImplTest {
|
||||
assertEquals("0", structureCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectStructureReplacesAndGroupsTableScopedModuleFolders() {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
GenTable userTable = table(11L, "sys_user", "User");
|
||||
GenTable bookTable = table(12L, "book", "Book");
|
||||
userTable.setModuleName("library");
|
||||
bookTable.setModuleName("library");
|
||||
project.setTables(Arrays.asList(userTable, bookTable));
|
||||
|
||||
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(102L, 0L)));
|
||||
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
|
||||
structure(1L, 0L, "{projectName}-admin", "folder", null, null),
|
||||
structure(2L, 1L, "src", "folder", null, null),
|
||||
structure(3L, 2L, "api", "folder", null, null),
|
||||
structure(4L, 3L, "{moduleName}", "folder", 0L, null),
|
||||
structure(5L, 4L, "{businessName}.js", "file", 0L, "api.js.vm"),
|
||||
structure(6L, 2L, "views", "folder", null, null),
|
||||
structure(7L, 6L, "{moduleName}", "folder", 0L, null),
|
||||
structure(8L, 7L, "{businessName}", "folder", 0L, null),
|
||||
structure(9L, 8L, "index.vue", "file", 0L, "index.vue.vm")
|
||||
));
|
||||
|
||||
List<Map<String, Object>> structure = service.getProjectStructure(project, "admin_frontend");
|
||||
|
||||
Map<String, Object> src = child(structure.get(0), 0);
|
||||
Map<String, Object> api = child(src, 0);
|
||||
assertEquals(1, children(api).size());
|
||||
Map<String, Object> apiModuleFolder = child(api, 0);
|
||||
assertEquals("library", apiModuleFolder.get("name"));
|
||||
assertEquals("user.js", child(apiModuleFolder, 0).get("name"));
|
||||
assertEquals("book.js", child(apiModuleFolder, 1).get("name"));
|
||||
|
||||
Map<String, Object> views = child(src, 1);
|
||||
assertEquals(1, children(views).size());
|
||||
Map<String, Object> viewsModuleFolder = child(views, 0);
|
||||
assertEquals("library", viewsModuleFolder.get("name"));
|
||||
assertEquals("user", child(viewsModuleFolder, 0).get("name"));
|
||||
assertEquals("book", child(viewsModuleFolder, 1).get("name"));
|
||||
}
|
||||
|
||||
@Test(expected = ServiceException.class)
|
||||
public void frontDraftPreviewRejectsTableOutsideDraftWithoutMapperFallback() {
|
||||
GenProject project = project();
|
||||
@@ -284,9 +356,38 @@ public class GenProjectServiceImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frontDraftPreviewAppliesSelectedModuleSnippets() {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
project.setModules(Arrays.asList(projectModule(1L)));
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
project.setTables(Arrays.asList(table));
|
||||
SysCodeSnippet selectedModule = snippet(1L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void selected() {}");
|
||||
SysCodeSnippet unselectedModule = snippet(2L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void skipped() {}");
|
||||
|
||||
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(101L, 0L)));
|
||||
when(templateFileService.selectTemplateFilesByTemplateId(101L)).thenReturn(Arrays.asList(
|
||||
templateFile("controller.java.vm", "class UserController { $controllerExtraMethods }")
|
||||
));
|
||||
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList(selectedModule, unselectedModule));
|
||||
|
||||
Map<String, String> preview = service.previewCode(project, 11L, "controller.java.vm", "backend");
|
||||
|
||||
String content = preview.get("controller.java.vm");
|
||||
assertTrue(content.contains("public void selected() {}"));
|
||||
assertFalse(content.contains("public void skipped() {}"));
|
||||
verify(sysProjectModuleService, never()).selectSysProjectModuleList(any(SysProjectModule.class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> child(Map<String, Object> node, int index) {
|
||||
return ((List<Map<String, Object>>) node.get("children")).get(index);
|
||||
return children(node).get(index);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> children(Map<String, Object> node) {
|
||||
return (List<Map<String, Object>>) node.get("children");
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception {
|
||||
@@ -398,4 +499,24 @@ public class GenProjectServiceImplTest {
|
||||
action.setPath("/" + code);
|
||||
return action;
|
||||
}
|
||||
|
||||
private String readBusinessBlueprintSql() throws Exception {
|
||||
Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
|
||||
if (!Files.exists(sqlPath)) {
|
||||
sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
|
||||
}
|
||||
return new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String extractTemplateSqlContent(String sql, String templateFileId) {
|
||||
String marker = "(" + templateFileId + ",";
|
||||
int rowStart = sql.indexOf(marker);
|
||||
assertTrue("Template row not found: " + templateFileId, rowStart >= 0);
|
||||
int contentStart = sql.indexOf("\n'", rowStart);
|
||||
assertTrue("Template content start not found: " + templateFileId, contentStart >= 0);
|
||||
contentStart += 2;
|
||||
int contentEnd = sql.indexOf("\n', 'admin'", contentStart);
|
||||
assertTrue("Template content end not found: " + templateFileId, contentEnd >= 0);
|
||||
return sql.substring(contentStart, contentEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
import org.junit.Before;
|
||||
@@ -31,8 +32,10 @@ import com.ruoyi.generator.domain.front.FrontProjectTable;
|
||||
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
|
||||
@@ -93,15 +96,17 @@ public class AiGenerateServiceImplTest
|
||||
assertEquals(2, columnCaptor.getAllValues().size());
|
||||
assertEquals("1", generationCaptor.getValue().getSuccess());
|
||||
assertTrue(response.getSql().contains("CREATE TABLE `car_info`"));
|
||||
assertTrue(response.getBusinessActions().isEmpty());
|
||||
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(deepSeekClient).chat(promptCaptor.capture());
|
||||
assertTrue(promptCaptor.getValue().contains("businessActions"));
|
||||
assertTrue(promptCaptor.getValue().contains("conditionFields"));
|
||||
assertFalse(promptCaptor.getValue().contains("\"businessActions\""));
|
||||
assertFalse(promptCaptor.getValue().contains("conditionFields"));
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
||||
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
|
||||
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
|
||||
assertEquals("[]", projectCaptor.getValue().getBusinessBlueprint());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -112,6 +117,11 @@ public class AiGenerateServiceImplTest
|
||||
|
||||
AppBlueprintDesign blueprint = service.generateAppBlueprint(7L, 10L, appBlueprintRequest());
|
||||
|
||||
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(deepSeekClient).chat(promptCaptor.capture());
|
||||
assertTrue(promptCaptor.getValue().contains("role.name"));
|
||||
assertTrue(promptCaptor.getValue().contains("Simplified Chinese"));
|
||||
|
||||
assertEquals(2, blueprint.getRoles().size());
|
||||
assertEquals("reader", blueprint.getRoles().get(0).getCode());
|
||||
assertEquals("admin", blueprint.getRoles().get(1).getCode());
|
||||
@@ -152,7 +162,118 @@ public class AiGenerateServiceImplTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseIncludesConfirmedAppBlueprintConstraintsInPrompt()
|
||||
public void generateBusinessBlueprintUsesSavedDatabaseAndPersistsActions()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedLibraryTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(businessBlueprintOnlyResponse());
|
||||
|
||||
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
|
||||
assertEquals(1, actions.size());
|
||||
BusinessActionDesign action = actions.get(0);
|
||||
assertEquals("borrow_book", action.getCode());
|
||||
assertEquals("book_borrow", action.getOwnerTable());
|
||||
assertEquals("book_id", action.getRequestFields().get(0));
|
||||
assertEquals("UPDATE_FIELD", action.getEffects().get(0).getType());
|
||||
assertEquals("stock", action.getEffects().get(0).getTargetField());
|
||||
|
||||
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(deepSeekClient).chat(promptCaptor.capture());
|
||||
String prompt = promptCaptor.getValue();
|
||||
assertTrue(prompt.contains("Saved database schema"));
|
||||
assertTrue(prompt.contains("book_info"));
|
||||
assertTrue(prompt.contains("stock"));
|
||||
assertTrue(prompt.contains("Do not generate CRUD"));
|
||||
assertTrue(prompt.contains("Do not generate create, read, update, delete"));
|
||||
assertTrue(prompt.contains("businessActions.name"));
|
||||
assertTrue(prompt.contains("rules"));
|
||||
assertTrue(prompt.contains("effects.description"));
|
||||
assertTrue(prompt.contains("Simplified Chinese"));
|
||||
assertTrue(prompt.contains("Keep API paths and request parameter fields as technical identifiers"));
|
||||
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
||||
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
|
||||
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
|
||||
|
||||
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
|
||||
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
|
||||
assertEquals("business_blueprint", generationCaptor.getValue().getGenerateType());
|
||||
assertEquals("1", generationCaptor.getValue().getSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateBusinessBlueprintParsesExecutableRulesAndWorkflowEffects()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedLibraryTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(executableBusinessBlueprintResponse());
|
||||
|
||||
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
|
||||
assertEquals(1, actions.size());
|
||||
BusinessActionDesign action = actions.get(0);
|
||||
assertEquals("borrow_book", action.getCode());
|
||||
assertEquals(2, action.getRuleChecks().size());
|
||||
|
||||
BusinessActionRuleDesign existsRule = action.getRuleChecks().get(0);
|
||||
assertEquals("EXISTS", existsRule.getType());
|
||||
assertEquals("book_info", existsRule.getTargetTable());
|
||||
assertEquals("book_id", existsRule.getConditionFields().get(0));
|
||||
|
||||
BusinessActionRuleDesign stockRule = action.getRuleChecks().get(1);
|
||||
assertEquals("NUMBER_GTE", stockRule.getType());
|
||||
assertEquals("stock", stockRule.getTargetField());
|
||||
assertEquals("1", stockRule.getCompareValue());
|
||||
|
||||
BusinessActionEffectDesign insertEffect = action.getEffects().get(0);
|
||||
assertEquals("INSERT_ROW", insertEffect.getType());
|
||||
assertEquals("book_borrow", insertEffect.getTargetTable());
|
||||
assertEquals("${param.book_id}", insertEffect.getValues().get("book_id"));
|
||||
assertEquals("borrowed", insertEffect.getValues().get("borrow_status"));
|
||||
|
||||
BusinessActionEffectDesign decreaseEffect = action.getEffects().get(1);
|
||||
assertEquals("DECREASE_NUMBER", decreaseEffect.getType());
|
||||
assertEquals("book_info", decreaseEffect.getTargetTable());
|
||||
assertEquals("stock", decreaseEffect.getTargetField());
|
||||
assertEquals("1", decreaseEffect.getAmount());
|
||||
assertEquals("0", decreaseEffect.getMinValue());
|
||||
|
||||
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(deepSeekClient).chat(promptCaptor.capture());
|
||||
assertTrue(promptCaptor.getValue().contains("ruleChecks"));
|
||||
assertTrue(promptCaptor.getValue().contains("INSERT_ROW"));
|
||||
assertTrue(promptCaptor.getValue().contains("DECREASE_NUMBER"));
|
||||
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||
String persisted = projectCaptor.getValue().getBusinessBlueprint();
|
||||
assertTrue(persisted.contains("ruleChecks"));
|
||||
assertTrue(persisted.contains("INSERT_ROW"));
|
||||
assertTrue(persisted.contains("DECREASE_NUMBER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateBusinessBlueprintDropsCrudActionsReturnedByAi()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedLibraryTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(businessBlueprintWithCrudActionsResponse());
|
||||
|
||||
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
|
||||
assertEquals(1, actions.size());
|
||||
assertEquals("borrow_book", actions.get(0).getCode());
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
|
||||
assertFalse(projectCaptor.getValue().getBusinessBlueprint().contains("create_book"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseIncludesConfirmedAppBlueprintConstraintsButNoBusinessActions()
|
||||
{
|
||||
FrontProject project = project();
|
||||
project.setAppBlueprint("{\"frontendMenus\":[{\"code\":\"my_borrows\",\"requiresLogin\":true,\"dataScope\":\"CURRENT_USER\"}],\"adminMenus\":[]}");
|
||||
@@ -168,6 +289,9 @@ public class AiGenerateServiceImplTest
|
||||
assertTrue(prompt.contains("CURRENT_USER"));
|
||||
assertTrue(prompt.contains("Do not generate menu or permission tables"));
|
||||
assertTrue(prompt.contains("user_id"));
|
||||
assertFalse(prompt.contains("\"businessActions\""));
|
||||
assertFalse(prompt.contains("conditionFields"));
|
||||
assertFalse(prompt.contains("Business actions must align"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -280,14 +404,14 @@ public class AiGenerateServiceImplTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseParsesNormalizesAndPersistsBusinessActionDraft()
|
||||
public void generateBusinessBlueprintParsesNormalizesAndPersistsBusinessActionDraft()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedLibraryTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithBusinessActions());
|
||||
|
||||
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
|
||||
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
|
||||
List<BusinessActionDesign> actions = response.getBusinessActions();
|
||||
assertEquals(1, actions.size());
|
||||
BusinessActionDesign action = actions.get(0);
|
||||
assertEquals("borrow_book", action.getCode());
|
||||
@@ -310,14 +434,15 @@ public class AiGenerateServiceImplTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseDropsMissingBusinessActionRequestFields()
|
||||
public void generateBusinessBlueprintDropsMissingBusinessActionRequestFields()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedFineTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessActionRequestField());
|
||||
|
||||
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
|
||||
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
|
||||
List<String> requestFields = response.getBusinessActions().get(0).getRequestFields();
|
||||
List<String> requestFields = actions.get(0).getRequestFields();
|
||||
assertEquals(1, requestFields.size());
|
||||
assertEquals("reader_id", requestFields.get(0));
|
||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||
@@ -326,14 +451,15 @@ public class AiGenerateServiceImplTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseRepairsMissingBusinessEffectTargetField()
|
||||
public void generateBusinessBlueprintRepairsMissingBusinessEffectTargetField()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedEffectRepairTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessEffectTargetField());
|
||||
|
||||
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
|
||||
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
|
||||
BusinessActionEffectDesign effect = response.getBusinessActions().get(0).getEffects().get(0);
|
||||
BusinessActionEffectDesign effect = actions.get(0).getEffects().get(0);
|
||||
assertEquals("stock", effect.getTargetField());
|
||||
assertEquals("stock - 1", effect.getValue());
|
||||
|
||||
@@ -344,14 +470,15 @@ public class AiGenerateServiceImplTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDatabaseRejectsBusinessActionReferencingMissingTable()
|
||||
public void generateBusinessBlueprintRejectsBusinessActionReferencingMissingTable()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedBookInfoOnly();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessActionTable());
|
||||
|
||||
try
|
||||
{
|
||||
service.generateDatabase(7L, 10L, request());
|
||||
service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
fail("Expected invalid business action table");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
@@ -363,6 +490,42 @@ public class AiGenerateServiceImplTest
|
||||
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateBusinessBlueprintRejectsRuleCheckWithMissingTargetField()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedLibraryTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(ruleCheckWithMissingTargetFieldResponse());
|
||||
|
||||
try
|
||||
{
|
||||
service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
fail("Expected missing rule target field to be rejected");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals("Business rule target field is required: NUMBER_GTE", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateBusinessBlueprintRejectsEffectValueWithUnknownParam()
|
||||
{
|
||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||
mockSavedLibraryTables();
|
||||
when(deepSeekClient.chat(anyString())).thenReturn(effectValueWithUnknownParamResponse());
|
||||
|
||||
try
|
||||
{
|
||||
service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
|
||||
fail("Expected unknown value placeholder to be rejected");
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
assertEquals("Business effect value references missing field: missing_reader_id", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generationRecordInsertUsesRequiresNewTransaction() throws Exception
|
||||
{
|
||||
@@ -416,6 +579,86 @@ public class AiGenerateServiceImplTest
|
||||
return request;
|
||||
}
|
||||
|
||||
private GenerateBusinessBlueprintRequest businessBlueprintRequest()
|
||||
{
|
||||
GenerateBusinessBlueprintRequest request = new GenerateBusinessBlueprintRequest();
|
||||
request.setProjectName("Library");
|
||||
request.setProjectDesc("Readers borrow and return books");
|
||||
request.setIndustryTemplate("library");
|
||||
request.setExtraRequirements("Need borrow and return workflows, not CRUD");
|
||||
return request;
|
||||
}
|
||||
|
||||
private FrontProjectTable savedTable(Long tableId, String tableName, String tableComment)
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
table.setTableId(tableId);
|
||||
table.setProjectId(10L);
|
||||
table.setTableName(tableName);
|
||||
table.setTableComment(tableComment);
|
||||
return table;
|
||||
}
|
||||
|
||||
private FrontProjectColumn savedColumn(String columnName, String columnType, String columnComment)
|
||||
{
|
||||
FrontProjectColumn column = new FrontProjectColumn();
|
||||
column.setProjectId(10L);
|
||||
column.setColumnName(columnName);
|
||||
column.setColumnType(columnType);
|
||||
column.setColumnComment(columnComment);
|
||||
return column;
|
||||
}
|
||||
|
||||
private void mockSavedLibraryTables()
|
||||
{
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
|
||||
savedTable(101L, "book_info", "Book info"),
|
||||
savedTable(102L, "reader_info", "Reader info"),
|
||||
savedTable(103L, "book_borrow", "Book borrow")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(101L)).thenReturn(Arrays.asList(
|
||||
savedColumn("book_id", "bigint(20)", "Book ID"),
|
||||
savedColumn("stock", "int(11)", "Stock")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(102L)).thenReturn(Arrays.asList(
|
||||
savedColumn("reader_id", "bigint(20)", "Reader ID"),
|
||||
savedColumn("status", "varchar(20)", "Status")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(103L)).thenReturn(Arrays.asList(
|
||||
savedColumn("borrow_id", "bigint(20)", "Borrow ID"),
|
||||
savedColumn("book_id", "bigint(20)", "Book ID"),
|
||||
savedColumn("reader_id", "bigint(20)", "Reader ID"),
|
||||
savedColumn("borrow_status", "varchar(20)", "Borrow status")));
|
||||
}
|
||||
|
||||
private void mockSavedFineTables()
|
||||
{
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
|
||||
savedTable(201L, "fine_record", "Fine record")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(201L)).thenReturn(Arrays.asList(
|
||||
savedColumn("id", "bigint(20)", "ID"),
|
||||
savedColumn("reader_id", "bigint(20)", "Reader ID"),
|
||||
savedColumn("fine_amount", "decimal(10,2)", "Fine amount")));
|
||||
}
|
||||
|
||||
private void mockSavedEffectRepairTables()
|
||||
{
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
|
||||
savedTable(301L, "book_info", "Book info"),
|
||||
savedTable(302L, "borrow_record", "Borrow record")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(301L)).thenReturn(Arrays.asList(
|
||||
savedColumn("book_id", "bigint(20)", "Book ID"),
|
||||
savedColumn("stock", "int(11)", "Stock")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(302L)).thenReturn(Arrays.asList(
|
||||
savedColumn("borrow_id", "bigint(20)", "Borrow ID"),
|
||||
savedColumn("book_id", "bigint(20)", "Book ID")));
|
||||
}
|
||||
|
||||
private void mockSavedBookInfoOnly()
|
||||
{
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
|
||||
savedTable(401L, "book_info", "Book info")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(401L)).thenReturn(Arrays.asList(
|
||||
savedColumn("book_id", "bigint(20)", "Book ID")));
|
||||
}
|
||||
|
||||
private String repeated(String value, int count)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
@@ -644,6 +887,73 @@ public class AiGenerateServiceImplTest
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String businessBlueprintOnlyResponse()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"businessActions\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"code\": \"Borrow Book\",\n"
|
||||
+ " \"name\": \"Borrow Book\",\n"
|
||||
+ " \"ownerTable\": \"book_borrow\",\n"
|
||||
+ " \"method\": \"post\",\n"
|
||||
+ " \"path\": \"/library/borrow/borrow\",\n"
|
||||
+ " \"transaction\": true,\n"
|
||||
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||
+ " \"rules\": [\"Book stock must be greater than zero\"],\n"
|
||||
+ " \"effects\": [\n"
|
||||
+ " {\"type\": \"update_field\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"conditionFields\": [\"book_id\"], \"description\": \"Decrease stock\"}\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String businessBlueprintWithCrudActionsResponse()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"businessActions\": [\n"
|
||||
+ " {\"code\": \"create_book\", \"name\": \"Create Book\", \"ownerTable\": \"book_info\", \"method\": \"POST\", \"path\": \"/book/create\", \"requestFields\": [\"book_id\"]},\n"
|
||||
+ " {\n"
|
||||
+ " \"code\": \"borrow_book\",\n"
|
||||
+ " \"name\": \"Borrow Book\",\n"
|
||||
+ " \"ownerTable\": \"book_borrow\",\n"
|
||||
+ " \"method\": \"POST\",\n"
|
||||
+ " \"path\": \"/library/borrow/borrow\",\n"
|
||||
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||
+ " \"effects\": [\n"
|
||||
+ " {\"type\": \"UPDATE_FIELD\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"conditionFields\": [\"book_id\"], \"description\": \"Decrease stock\"}\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String executableBusinessBlueprintResponse()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"businessActions\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"code\": \"Borrow Book\",\n"
|
||||
+ " \"name\": \"Borrow Book\",\n"
|
||||
+ " \"ownerTable\": \"book_borrow\",\n"
|
||||
+ " \"method\": \"post\",\n"
|
||||
+ " \"path\": \"/library/borrow/borrow\",\n"
|
||||
+ " \"transaction\": true,\n"
|
||||
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||
+ " \"rules\": [\"Book must exist\", \"Stock must be enough\"],\n"
|
||||
+ " \"ruleChecks\": [\n"
|
||||
+ " {\"type\": \"exists\", \"targetTable\": \"book_info\", \"conditionFields\": [\"book_id\"], \"message\": \"Book not found\"},\n"
|
||||
+ " {\"type\": \"number_gte\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"compareValue\": \"1\", \"conditionFields\": [\"book_id\"], \"message\": \"Stock is not enough\"}\n"
|
||||
+ " ],\n"
|
||||
+ " \"effects\": [\n"
|
||||
+ " {\"type\": \"insert_row\", \"targetTable\": \"book_borrow\", \"values\": {\"book_id\": \"${param.book_id}\", \"reader_id\": \"${param.reader_id}\", \"borrow_status\": \"borrowed\"}, \"description\": \"Create borrow record\"},\n"
|
||||
+ " {\"type\": \"decrease_number\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"amount\": \"1\", \"minValue\": \"0\", \"conditionFields\": [\"book_id\"], \"message\": \"Stock decrease failed\", \"description\": \"Decrease stock\"}\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String responseWithMissingBusinessActionRequestField()
|
||||
{
|
||||
return "{\n"
|
||||
@@ -694,4 +1004,42 @@ public class AiGenerateServiceImplTest
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String ruleCheckWithMissingTargetFieldResponse()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"businessActions\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"code\": \"borrow_book\",\n"
|
||||
+ " \"name\": \"Borrow Book\",\n"
|
||||
+ " \"ownerTable\": \"book_borrow\",\n"
|
||||
+ " \"method\": \"POST\",\n"
|
||||
+ " \"path\": \"/library/borrow/borrow\",\n"
|
||||
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||
+ " \"ruleChecks\": [\n"
|
||||
+ " {\"type\": \"NUMBER_GTE\", \"targetTable\": \"book_info\", \"targetField\": \"missing_stock\", \"compareValue\": \"1\", \"conditionFields\": [\"book_id\"], \"message\": \"Stock is not enough\"}\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String effectValueWithUnknownParamResponse()
|
||||
{
|
||||
return "{\n"
|
||||
+ " \"businessActions\": [\n"
|
||||
+ " {\n"
|
||||
+ " \"code\": \"borrow_book\",\n"
|
||||
+ " \"name\": \"Borrow Book\",\n"
|
||||
+ " \"ownerTable\": \"book_borrow\",\n"
|
||||
+ " \"method\": \"POST\",\n"
|
||||
+ " \"path\": \"/library/borrow/borrow\",\n"
|
||||
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||
+ " \"effects\": [\n"
|
||||
+ " {\"type\": \"INSERT_ROW\", \"targetTable\": \"book_borrow\", \"values\": {\"book_id\": \"${param.book_id}\", \"reader_id\": \"${param.missing_reader_id}\"}, \"description\": \"Create borrow record\"}\n"
|
||||
+ " ]\n"
|
||||
+ " }\n"
|
||||
+ " ]\n"
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.domain.GenProject;
|
||||
import com.ruoyi.generator.domain.GenTable;
|
||||
import com.ruoyi.generator.domain.SysProjectModule;
|
||||
import com.ruoyi.generator.domain.front.FrontProject;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
||||
@@ -33,6 +35,8 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -144,6 +148,7 @@ public class FrontProjectPreviewServiceImplTest
|
||||
public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
|
||||
{
|
||||
FrontProject project = frontProjectWithOneTable();
|
||||
project.setFrontendEnabled("1");
|
||||
project.setPreviewStatus("1");
|
||||
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
|
||||
when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
|
||||
@@ -161,6 +166,64 @@ public class FrontProjectPreviewServiceImplTest
|
||||
assertTrue(entries.contains("admin-web/demo/admin.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsFrontendStructureWhenProjectDisablesFrontend()
|
||||
{
|
||||
FrontProject project = frontProjectWithOneTable();
|
||||
project.setFrontendEnabled("0");
|
||||
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
|
||||
|
||||
ServiceException exception = expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
previewService.getStructure(100L, 200L, "frontend");
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals("该项目未启用前台前端", exception.getMessage());
|
||||
verify(genProjectService, never()).getProjectStructure(any(GenProject.class), eq("frontend"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadAllSkipsFrontendWhenProjectDisablesFrontend() throws Exception
|
||||
{
|
||||
FrontProject project = frontProjectWithOneTable();
|
||||
project.setFrontendEnabled("0");
|
||||
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
|
||||
when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
|
||||
.thenReturn(zipWithEntry("demo/backend.txt", "backend"));
|
||||
when(genProjectService.downloadStructure(any(GenProject.class), eq("admin_frontend")))
|
||||
.thenReturn(zipWithEntry("demo/admin.txt", "admin"));
|
||||
|
||||
byte[] data = previewService.downloadAll(100L, 200L);
|
||||
|
||||
Set<String> entries = zipEntries(data);
|
||||
assertTrue(entries.contains("backend/demo/backend.txt"));
|
||||
assertTrue(entries.contains("admin-web/demo/admin.txt"));
|
||||
assertFalse(entries.contains("front-web/demo/frontend.txt"));
|
||||
verify(genProjectService, never()).downloadStructure(any(GenProject.class), eq("frontend"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void passesSelectedModulesIntoDraftProject()
|
||||
{
|
||||
FrontProject project = frontProjectWithOneTable();
|
||||
project.setModules(Arrays.asList(projectModule(11L), projectModule(22L)));
|
||||
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
|
||||
when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
|
||||
.thenReturn(Collections.<Map<String, Object>>emptyList());
|
||||
|
||||
previewService.getStructure(100L, 200L, "backend");
|
||||
|
||||
ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
|
||||
verify(genProjectService, times(1)).getProjectStructure(projectCaptor.capture(), eq("backend"));
|
||||
assertEquals(2, projectCaptor.getValue().getModules().size());
|
||||
assertEquals(Long.valueOf(11L), projectCaptor.getValue().getModules().get(0).getModuleId());
|
||||
assertEquals(Long.valueOf(22L), projectCaptor.getValue().getModules().get(1).getModuleId());
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = FrontProjectPreviewServiceImpl.class.getDeclaredField(name);
|
||||
@@ -175,10 +238,20 @@ public class FrontProjectPreviewServiceImplTest
|
||||
project.setProjectName("Demo");
|
||||
project.setProjectFileName("demo");
|
||||
project.setPackageName("com.example.demo");
|
||||
project.setFrontendEnabled("1");
|
||||
project.setTables(Arrays.asList(table()));
|
||||
return project;
|
||||
}
|
||||
|
||||
private SysProjectModule projectModule(Long moduleId)
|
||||
{
|
||||
SysProjectModule module = new SysProjectModule();
|
||||
module.setProjectId(200L);
|
||||
module.setModuleId(moduleId);
|
||||
module.setStatus("0");
|
||||
return module;
|
||||
}
|
||||
|
||||
private FrontProjectTable table()
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
@@ -224,4 +297,22 @@ public class FrontProjectPreviewServiceImplTest
|
||||
zip.close();
|
||||
return entries;
|
||||
}
|
||||
|
||||
private ServiceException expectServiceException(ThrowingRunnable runnable)
|
||||
{
|
||||
try
|
||||
{
|
||||
runnable.run();
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
throw new AssertionError("Expected ServiceException");
|
||||
}
|
||||
|
||||
private interface ThrowingRunnable
|
||||
{
|
||||
void run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.Test;
|
||||
|
||||
public class FrontProjectSchemaScriptTest
|
||||
{
|
||||
private static final String[] FRONT_PROJECT_DRAFT_COLUMNS = {
|
||||
"requirement_keyword",
|
||||
"industry_template",
|
||||
"frontend_enabled",
|
||||
"app_blueprint",
|
||||
"er_diagram",
|
||||
"business_blueprint"
|
||||
};
|
||||
|
||||
@Test
|
||||
public void upgradeScriptBackfillsMappedFrontProjectDraftColumns() throws Exception
|
||||
{
|
||||
Path repoRoot = repoRoot();
|
||||
String mapperXml = read(repoRoot.resolve("ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml"));
|
||||
String upgradeSql = read(repoRoot.resolve("sql/front_project_upgrade.sql")).toLowerCase();
|
||||
|
||||
for (String column : FRONT_PROJECT_DRAFT_COLUMNS)
|
||||
{
|
||||
assertTrue("FrontProjectMapper should map " + column, mapperXml.contains(column));
|
||||
assertTrue("front_project_upgrade.sql should check " + column,
|
||||
upgradeSql.contains("column_name = '" + column + "'"));
|
||||
assertTrue("front_project_upgrade.sql should add " + column,
|
||||
upgradeSql.contains("add column " + column));
|
||||
}
|
||||
assertTrue("front_project_upgrade.sql should create front_project_module",
|
||||
upgradeSql.contains("create table if not exists front_project_module"));
|
||||
assertTrue("front_workbench.sql should create front_project_module",
|
||||
read(repoRoot.resolve("sql/front_workbench.sql")).contains("create table front_project_module"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dbSqlIncludesBusinessBlueprintTemplates() throws Exception
|
||||
{
|
||||
String dbSql = read(repoRoot().resolve("sql/db.sql"));
|
||||
|
||||
assertTrue("db.sql should include business blueprint backend template",
|
||||
dbSql.contains("Business Blueprint Backend"));
|
||||
assertTrue("db.sql should include executable blueprint effects",
|
||||
dbSql.contains("executeUpdateFieldEffect"));
|
||||
assertTrue("db.sql should include frontend menu template",
|
||||
dbSql.contains("frontendMenus.js.vm"));
|
||||
}
|
||||
|
||||
private Path repoRoot()
|
||||
{
|
||||
Path cwd = Paths.get("").toAbsolutePath();
|
||||
if ("ruoyi-generator".equals(cwd.getFileName().toString()))
|
||||
{
|
||||
return cwd.getParent();
|
||||
}
|
||||
return cwd;
|
||||
}
|
||||
|
||||
private String read(Path path) throws Exception
|
||||
{
|
||||
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,13 @@ import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectModuleMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectFileMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
|
||||
import com.ruoyi.generator.service.ISysModuleService;
|
||||
|
||||
public class FrontProjectServiceImplTest
|
||||
{
|
||||
@@ -44,6 +46,10 @@ public class FrontProjectServiceImplTest
|
||||
private FrontProjectFileMapper frontProjectFileMapper;
|
||||
@Mock
|
||||
private FrontProjectGenerationMapper frontProjectGenerationMapper;
|
||||
@Mock
|
||||
private FrontProjectModuleMapper frontProjectModuleMapper;
|
||||
@Mock
|
||||
private ISysModuleService sysModuleService;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
@@ -55,6 +61,8 @@ public class FrontProjectServiceImplTest
|
||||
setField("frontProjectColumnMapper", frontProjectColumnMapper);
|
||||
setField("frontProjectFileMapper", frontProjectFileMapper);
|
||||
setField("frontProjectGenerationMapper", frontProjectGenerationMapper);
|
||||
setField("frontProjectModuleMapper", frontProjectModuleMapper);
|
||||
setField("sysModuleService", sysModuleService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,6 +81,7 @@ public class FrontProjectServiceImplTest
|
||||
assertEquals("1.0.0", project.getVersion());
|
||||
assertEquals("Vue3 + Element Plus", project.getFrontFramework());
|
||||
assertEquals("Spring Boot + MyBatis", project.getBackFramework());
|
||||
assertEquals("1", project.getFrontendEnabled());
|
||||
assertEquals("0", project.getGenerateStatus());
|
||||
assertEquals("0", project.getPreviewStatus());
|
||||
assertEquals("0", project.getStatus());
|
||||
@@ -80,6 +89,32 @@ public class FrontProjectServiceImplTest
|
||||
verify(frontProjectMapper).insertFrontProject(project);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProjectPersistsFrontendSwitchAndSelectedModules()
|
||||
{
|
||||
org.mockito.Mockito.doAnswer(invocation -> {
|
||||
FrontProject inserted = invocation.getArgument(0);
|
||||
inserted.setProjectId(88L);
|
||||
return 1;
|
||||
}).when(frontProjectMapper).insertFrontProject(any(FrontProject.class));
|
||||
FrontProjectCreateRequest request = new FrontProjectCreateRequest();
|
||||
request.setProjectName("客户中心");
|
||||
request.setFrontendEnabled(Boolean.FALSE);
|
||||
request.setModuleIds(Arrays.asList(11L, 22L));
|
||||
|
||||
FrontProject project = service.createProject(7L, request);
|
||||
|
||||
assertEquals("0", project.getFrontendEnabled());
|
||||
verify(frontProjectMapper).insertFrontProject(project);
|
||||
verify(frontProjectModuleMapper).deleteModulesByProjectId(88L);
|
||||
ArgumentCaptor<com.ruoyi.generator.domain.front.FrontProjectModule> moduleCaptor =
|
||||
ArgumentCaptor.forClass(com.ruoyi.generator.domain.front.FrontProjectModule.class);
|
||||
verify(frontProjectModuleMapper, org.mockito.Mockito.times(2)).insertFrontProjectModule(moduleCaptor.capture());
|
||||
assertEquals(Long.valueOf(11L), moduleCaptor.getAllValues().get(0).getModuleId());
|
||||
assertEquals(Long.valueOf(22L), moduleCaptor.getAllValues().get(1).getModuleId());
|
||||
assertEquals("0", moduleCaptor.getAllValues().get(0).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveDatabaseRejectsInvalidTableName()
|
||||
{
|
||||
|
||||
@@ -280,6 +280,9 @@ public interface I${ClassName}Service
|
||||
(910108, 9101, 'serviceImpl.java.vm', NULL, 'serviceImpl.java.vm',
|
||||
'package ${packageName}.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Resource;
|
||||
@@ -374,6 +377,24 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
|
||||
#foreach($field in $action.requestFields)
|
||||
requireBusinessParam(params, "${field}");
|
||||
#end
|
||||
#end
|
||||
#if($action.ruleChecks)
|
||||
#foreach($rule in $action.ruleChecks)
|
||||
#set($conditionFields = $rule.conditionFields)
|
||||
#if($rule.type == "EXISTS")
|
||||
assertExists(params, "${rule.targetTable}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
|
||||
#elseif($rule.type == "NOT_EXISTS")
|
||||
assertNotExists(params, "${rule.targetTable}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
|
||||
#elseif($rule.type == "FIELD_EQUALS")
|
||||
assertFieldEquals(params, "${rule.targetTable}", "${rule.targetField}", "${rule.expectedValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
|
||||
#elseif($rule.type == "FIELD_NOT_EQUALS")
|
||||
assertFieldNotEquals(params, "${rule.targetTable}", "${rule.targetField}", "${rule.expectedValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
|
||||
#elseif($rule.type == "FIELD_IN")
|
||||
assertFieldIn(params, "${rule.targetTable}", "${rule.targetField}", new String[] {#foreach($value in $rule.expectedValues)"${value}"#if($foreach.hasNext), #end#end}, new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
|
||||
#elseif($rule.type == "NUMBER_GTE")
|
||||
assertNumberGte(params, "${rule.targetTable}", "${rule.targetField}", "${rule.compareValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${rule.message}");
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
int rows = 0;
|
||||
#if($action.effects)
|
||||
@@ -384,6 +405,30 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
|
||||
#set($conditionFields = $action.requestFields)
|
||||
#end
|
||||
rows += executeUpdateFieldEffect(params, "${effect.targetTable}", "${effect.targetField}", "${effect.value}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
|
||||
#elseif($effect.type == "INSERT_ROW")
|
||||
#set($effectIndex = $foreach.index)
|
||||
Map<String, Object> insertValues${effectIndex} = new java.util.LinkedHashMap<String, Object>();
|
||||
#foreach($entry in $effect.values.entrySet())
|
||||
insertValues${effectIndex}.put("${entry.key}", resolveBusinessValue(params, "${entry.value}"));
|
||||
#end
|
||||
rows += insertRow("${effect.targetTable}", insertValues${effectIndex});
|
||||
#elseif($effect.type == "UPDATE_FIELDS")
|
||||
#set($conditionFields = $effect.conditionFields)
|
||||
#set($effectIndex = $foreach.index)
|
||||
Map<String, Object> updateValues${effectIndex} = new java.util.LinkedHashMap<String, Object>();
|
||||
#foreach($entry in $effect.values.entrySet())
|
||||
updateValues${effectIndex}.put("${entry.key}", resolveBusinessValue(params, "${entry.value}"));
|
||||
#end
|
||||
rows += updateFields(params, "${effect.targetTable}", updateValues${effectIndex}, new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
|
||||
#elseif($effect.type == "SET_STATUS")
|
||||
#set($conditionFields = $effect.conditionFields)
|
||||
rows += setStatus(params, "${effect.targetTable}", "${effect.targetField}", resolveBusinessValue(params, "${effect.value}"), new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
|
||||
#elseif($effect.type == "INCREASE_NUMBER")
|
||||
#set($conditionFields = $effect.conditionFields)
|
||||
rows += increaseNumberField(params, "${effect.targetTable}", "${effect.targetField}", "${effect.amount}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
|
||||
#elseif($effect.type == "DECREASE_NUMBER")
|
||||
#set($conditionFields = $effect.conditionFields)
|
||||
rows += decreaseNumberField(params, "${effect.targetTable}", "${effect.targetField}", "${effect.amount}", "${effect.minValue}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end}, "${effect.message}");
|
||||
#else
|
||||
// Unsupported effect type ${effect.type}: ${effect.description}
|
||||
#end
|
||||
@@ -400,6 +445,143 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
|
||||
}
|
||||
}
|
||||
|
||||
private void assertExists(Map<String, Object> params, String tableName, String[] conditionFields, String message)
|
||||
{
|
||||
if (countRows(params, tableName, conditionFields) <= 0) {
|
||||
throw new ServiceException(defaultMessage(message, "Business record does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertNotExists(Map<String, Object> params, String tableName, String[] conditionFields, String message)
|
||||
{
|
||||
if (countRows(params, tableName, conditionFields) > 0) {
|
||||
throw new ServiceException(defaultMessage(message, "Business record already exists"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFieldEquals(Map<String, Object> params, String tableName, String targetField, String expectedValue,
|
||||
String[] conditionFields, String message)
|
||||
{
|
||||
Object value = querySingleField(params, tableName, targetField, conditionFields);
|
||||
if (value == null || !String.valueOf(resolveBusinessValue(params, expectedValue)).equals(String.valueOf(value))) {
|
||||
throw new ServiceException(defaultMessage(message, "Business rule check failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFieldNotEquals(Map<String, Object> params, String tableName, String targetField, String expectedValue,
|
||||
String[] conditionFields, String message)
|
||||
{
|
||||
Object value = querySingleField(params, tableName, targetField, conditionFields);
|
||||
if (value != null && String.valueOf(resolveBusinessValue(params, expectedValue)).equals(String.valueOf(value))) {
|
||||
throw new ServiceException(defaultMessage(message, "Business rule check failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFieldIn(Map<String, Object> params, String tableName, String targetField, String[] expectedValues,
|
||||
String[] conditionFields, String message)
|
||||
{
|
||||
Object value = querySingleField(params, tableName, targetField, conditionFields);
|
||||
String current = value == null ? "" : String.valueOf(value);
|
||||
for (String expectedValue : expectedValues) {
|
||||
if (String.valueOf(resolveBusinessValue(params, expectedValue)).equals(current)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new ServiceException(defaultMessage(message, "Business rule check failed"));
|
||||
}
|
||||
|
||||
private void assertNumberGte(Map<String, Object> params, String tableName, String targetField, String compareValue,
|
||||
String[] conditionFields, String message)
|
||||
{
|
||||
Object value = querySingleField(params, tableName, targetField, conditionFields);
|
||||
BigDecimal current = toBigDecimal(value, "Business numeric value is invalid");
|
||||
BigDecimal expected = resolveBusinessNumber(params, compareValue);
|
||||
if (current.compareTo(expected) < 0) {
|
||||
throw new ServiceException(defaultMessage(message, "Business numeric rule check failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private int insertRow(String tableName, Map<String, Object> values)
|
||||
{
|
||||
String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
|
||||
if (values == null || values.isEmpty()) {
|
||||
throw new ServiceException("Business insert values cannot be empty");
|
||||
}
|
||||
StringBuilder fields = new StringBuilder();
|
||||
StringBuilder placeholders = new StringBuilder();
|
||||
List<Object> args = new ArrayList<Object>();
|
||||
for (Map.Entry<String, Object> entry : values.entrySet()) {
|
||||
if (fields.length() > 0) {
|
||||
fields.append(", ");
|
||||
placeholders.append(", ");
|
||||
}
|
||||
fields.append(requireSafeIdentifier(entry.getKey(), "Business effect value field is invalid"));
|
||||
placeholders.append("?");
|
||||
args.add(entry.getValue());
|
||||
}
|
||||
return jdbcTemplate.update("insert into " + safeTableName + " (" + fields + ") values (" + placeholders + ")", args.toArray());
|
||||
}
|
||||
|
||||
private int updateFields(Map<String, Object> params, String tableName, Map<String, Object> values, String[] conditionFields)
|
||||
{
|
||||
String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
|
||||
if (values == null || values.isEmpty()) {
|
||||
throw new ServiceException("Business update values cannot be empty");
|
||||
}
|
||||
StringBuilder sets = new StringBuilder();
|
||||
List<Object> args = new ArrayList<Object>();
|
||||
for (Map.Entry<String, Object> entry : values.entrySet()) {
|
||||
if (sets.length() > 0) {
|
||||
sets.append(", ");
|
||||
}
|
||||
sets.append(requireSafeIdentifier(entry.getKey(), "Business effect value field is invalid")).append(" = ?");
|
||||
args.add(entry.getValue());
|
||||
}
|
||||
String whereClause = buildWhereClause(params, conditionFields);
|
||||
appendConditionValues(args, params, conditionFields);
|
||||
return jdbcTemplate.update("update " + safeTableName + " set " + sets + " where " + whereClause, args.toArray());
|
||||
}
|
||||
|
||||
private int setStatus(Map<String, Object> params, String tableName, String targetField, Object value, String[] conditionFields)
|
||||
{
|
||||
Map<String, Object> values = new java.util.LinkedHashMap<String, Object>();
|
||||
values.put(targetField, value);
|
||||
return updateFields(params, tableName, values, conditionFields);
|
||||
}
|
||||
|
||||
private int increaseNumberField(Map<String, Object> params, String tableName, String targetField, String amountExpression,
|
||||
String[] conditionFields)
|
||||
{
|
||||
String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
|
||||
String safeTargetField = requireSafeIdentifier(targetField, "Business effect target field is invalid");
|
||||
BigDecimal amount = resolveBusinessNumber(params, amountExpression);
|
||||
String whereClause = buildWhereClause(params, conditionFields);
|
||||
List<Object> args = new ArrayList<Object>();
|
||||
args.add(amount);
|
||||
appendConditionValues(args, params, conditionFields);
|
||||
return jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = " + safeTargetField + " + ? where " + whereClause, args.toArray());
|
||||
}
|
||||
|
||||
private int decreaseNumberField(Map<String, Object> params, String tableName, String targetField, String amountExpression,
|
||||
String minValueExpression, String[] conditionFields, String message)
|
||||
{
|
||||
String safeTableName = requireSafeIdentifier(tableName, "Business effect target table is invalid");
|
||||
String safeTargetField = requireSafeIdentifier(targetField, "Business effect target field is invalid");
|
||||
BigDecimal amount = resolveBusinessNumber(params, amountExpression);
|
||||
BigDecimal minValue = resolveBusinessNumber(params, minValueExpression == null || minValueExpression.length() == 0 ? "0" : minValueExpression);
|
||||
String whereClause = buildWhereClause(params, conditionFields);
|
||||
List<Object> args = new ArrayList<Object>();
|
||||
args.add(amount);
|
||||
appendConditionValues(args, params, conditionFields);
|
||||
args.add(amount);
|
||||
args.add(minValue);
|
||||
int rows = jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = " + safeTargetField + " - ? where " + whereClause + " and " + safeTargetField + " - ? >= ?", args.toArray());
|
||||
if (rows == 0) {
|
||||
throw new ServiceException(defaultMessage(message, "Business numeric decrease failed"));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private int executeUpdateFieldEffect(Map<String, Object> params, String tableName, String targetField,
|
||||
String valueExpression, String[] conditionFields)
|
||||
{
|
||||
@@ -438,6 +620,68 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
|
||||
return values;
|
||||
}
|
||||
|
||||
private int countRows(Map<String, Object> params, String tableName, String[] conditionFields)
|
||||
{
|
||||
String safeTableName = requireSafeIdentifier(tableName, "Business rule target table is invalid");
|
||||
String whereClause = buildWhereClause(params, conditionFields);
|
||||
Integer count = jdbcTemplate.queryForObject("select count(1) from " + safeTableName + " where " + whereClause, conditionValues(params, conditionFields), Integer.class);
|
||||
return count == null ? 0 : count.intValue();
|
||||
}
|
||||
|
||||
private Object querySingleField(Map<String, Object> params, String tableName, String targetField, String[] conditionFields)
|
||||
{
|
||||
String safeTableName = requireSafeIdentifier(tableName, "Business rule target table is invalid");
|
||||
String safeTargetField = requireSafeIdentifier(targetField, "Business rule target field is invalid");
|
||||
String whereClause = buildWhereClause(params, conditionFields);
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList("select " + safeTargetField + " from " + safeTableName + " where " + whereClause + " limit 1", conditionValues(params, conditionFields));
|
||||
if (rows.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return rows.get(0).get(safeTargetField);
|
||||
}
|
||||
|
||||
private void appendConditionValues(List<Object> args, Map<String, Object> params, String[] conditionFields)
|
||||
{
|
||||
for (Object value : conditionValues(params, conditionFields)) {
|
||||
args.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private Object resolveBusinessValue(Map<String, Object> params, String valueExpression)
|
||||
{
|
||||
if (valueExpression == null) {
|
||||
return null;
|
||||
}
|
||||
if ("#[[${now}]]#".equals(valueExpression)) {
|
||||
return new Date();
|
||||
}
|
||||
if (valueExpression.startsWith("#[[${param.]]#") && valueExpression.endsWith("}")) {
|
||||
String field = valueExpression.substring(8, valueExpression.length() - 1);
|
||||
requireBusinessParam(params, field);
|
||||
return params.get(field);
|
||||
}
|
||||
return valueExpression;
|
||||
}
|
||||
|
||||
private BigDecimal resolveBusinessNumber(Map<String, Object> params, String valueExpression)
|
||||
{
|
||||
return toBigDecimal(resolveBusinessValue(params, valueExpression), "Business numeric expression is invalid");
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value, String message)
|
||||
{
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value));
|
||||
} catch (RuntimeException e) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private String defaultMessage(String message, String fallback)
|
||||
{
|
||||
return message == null || message.length() == 0 ? fallback : message;
|
||||
}
|
||||
|
||||
private String requireSafeIdentifier(String value, String message)
|
||||
{
|
||||
if (value == null || !value.matches("[A-Za-z][A-Za-z0-9_]*")) {
|
||||
@@ -793,7 +1037,7 @@ export default {
|
||||
submitForm() {
|
||||
const request = this.form.${pkColumn.javaField} ? ${className}Api.update${ClassName}(this.form) : ${className}Api.add${ClassName}(this.form)
|
||||
request.then(() => {
|
||||
this.$modal.msgSuccess("保存成功")
|
||||
this.#[[$modal]]#.msgSuccess("保存成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
@@ -801,14 +1045,14 @@ export default {
|
||||
handleDelete(row) {
|
||||
const id = row.${pkColumn.javaField}
|
||||
${className}Api.del${ClassName}(id).then(() => {
|
||||
this.$modal.msgSuccess("删除成功")
|
||||
this.#[[$modal]]#.msgSuccess("删除成功")
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
handleBusinessAction(row, actionCode) {
|
||||
const action = this.businessActions.find(item => item.code === actionCode)
|
||||
${className}Api[actionCode]({ ...row }).then(() => {
|
||||
this.$modal.msgSuccess((action ? action.name : "业务动作") + "成功")
|
||||
this.#[[$modal]]#.msgSuccess((action ? action.name : "业务动作") + "成功")
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
|
||||
1631
RuoYi-Vue/sql/db.sql
1631
RuoYi-Vue/sql/db.sql
File diff suppressed because it is too large
Load Diff
129
RuoYi-Vue/sql/front_project_upgrade.sql
Normal file
129
RuoYi-Vue/sql/front_project_upgrade.sql
Normal file
@@ -0,0 +1,129 @@
|
||||
-- =================================================
|
||||
-- EasyCode front_project incremental upgrade
|
||||
-- Run once on existing front workbench databases created before the current
|
||||
-- front_project schema. Safe to run repeatedly.
|
||||
-- =================================================
|
||||
|
||||
set @front_project_requirement_keyword_exists := (
|
||||
select count(1)
|
||||
from information_schema.columns
|
||||
where table_schema = database()
|
||||
and table_name = 'front_project'
|
||||
and column_name = 'requirement_keyword'
|
||||
);
|
||||
|
||||
set @front_project_requirement_keyword_sql := if(
|
||||
@front_project_requirement_keyword_exists = 0,
|
||||
'alter table front_project add column requirement_keyword varchar(1000) default '''' comment ''需求关键词'' after project_desc',
|
||||
'select ''front_project.requirement_keyword already exists'''
|
||||
);
|
||||
|
||||
prepare front_project_requirement_keyword_stmt from @front_project_requirement_keyword_sql;
|
||||
execute front_project_requirement_keyword_stmt;
|
||||
deallocate prepare front_project_requirement_keyword_stmt;
|
||||
|
||||
set @front_project_industry_template_exists := (
|
||||
select count(1)
|
||||
from information_schema.columns
|
||||
where table_schema = database()
|
||||
and table_name = 'front_project'
|
||||
and column_name = 'industry_template'
|
||||
);
|
||||
|
||||
set @front_project_industry_template_sql := if(
|
||||
@front_project_industry_template_exists = 0,
|
||||
'alter table front_project add column industry_template varchar(50) default '''' comment ''行业模板'' after requirement_keyword',
|
||||
'select ''front_project.industry_template already exists'''
|
||||
);
|
||||
|
||||
prepare front_project_industry_template_stmt from @front_project_industry_template_sql;
|
||||
execute front_project_industry_template_stmt;
|
||||
deallocate prepare front_project_industry_template_stmt;
|
||||
|
||||
set @front_project_app_blueprint_exists := (
|
||||
select count(1)
|
||||
from information_schema.columns
|
||||
where table_schema = database()
|
||||
and table_name = 'front_project'
|
||||
and column_name = 'app_blueprint'
|
||||
);
|
||||
|
||||
set @front_project_app_blueprint_sql := if(
|
||||
@front_project_app_blueprint_exists = 0,
|
||||
'alter table front_project add column app_blueprint longtext comment ''应用蓝图草稿JSON'' after preview_status',
|
||||
'select ''front_project.app_blueprint already exists'''
|
||||
);
|
||||
|
||||
prepare front_project_app_blueprint_stmt from @front_project_app_blueprint_sql;
|
||||
execute front_project_app_blueprint_stmt;
|
||||
deallocate prepare front_project_app_blueprint_stmt;
|
||||
|
||||
set @front_project_er_diagram_exists := (
|
||||
select count(1)
|
||||
from information_schema.columns
|
||||
where table_schema = database()
|
||||
and table_name = 'front_project'
|
||||
and column_name = 'er_diagram'
|
||||
);
|
||||
|
||||
set @front_project_er_diagram_sql := if(
|
||||
@front_project_er_diagram_exists = 0,
|
||||
'alter table front_project add column er_diagram longtext comment ''ER图草稿JSON'' after app_blueprint',
|
||||
'select ''front_project.er_diagram already exists'''
|
||||
);
|
||||
|
||||
prepare front_project_er_diagram_stmt from @front_project_er_diagram_sql;
|
||||
execute front_project_er_diagram_stmt;
|
||||
deallocate prepare front_project_er_diagram_stmt;
|
||||
|
||||
set @front_project_business_blueprint_exists := (
|
||||
select count(1)
|
||||
from information_schema.columns
|
||||
where table_schema = database()
|
||||
and table_name = 'front_project'
|
||||
and column_name = 'business_blueprint'
|
||||
);
|
||||
|
||||
set @front_project_business_blueprint_sql := if(
|
||||
@front_project_business_blueprint_exists = 0,
|
||||
'alter table front_project add column business_blueprint longtext comment ''业务蓝图草稿JSON'' after er_diagram',
|
||||
'select ''front_project.business_blueprint already exists'''
|
||||
);
|
||||
|
||||
prepare front_project_business_blueprint_stmt from @front_project_business_blueprint_sql;
|
||||
execute front_project_business_blueprint_stmt;
|
||||
deallocate prepare front_project_business_blueprint_stmt;
|
||||
|
||||
set @front_project_frontend_enabled_exists := (
|
||||
select count(1)
|
||||
from information_schema.columns
|
||||
where table_schema = database()
|
||||
and table_name = 'front_project'
|
||||
and column_name = 'frontend_enabled'
|
||||
);
|
||||
|
||||
set @front_project_frontend_enabled_sql := if(
|
||||
@front_project_frontend_enabled_exists = 0,
|
||||
'alter table front_project add column frontend_enabled char(1) default ''1'' comment ''是否生成前台前端(1是 0否)'' after back_framework',
|
||||
'select ''front_project.frontend_enabled already exists'''
|
||||
);
|
||||
|
||||
prepare front_project_frontend_enabled_stmt from @front_project_frontend_enabled_sql;
|
||||
execute front_project_frontend_enabled_stmt;
|
||||
deallocate prepare front_project_frontend_enabled_stmt;
|
||||
|
||||
create table if not exists front_project_module (
|
||||
id bigint(20) not null auto_increment comment 'id',
|
||||
project_id bigint(20) not null comment '前台项目ID',
|
||||
module_id bigint(20) not null comment '功能模块ID',
|
||||
status char(1) default '0' comment '状态(0正常 1停用)',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_time datetime default null comment '更新时间',
|
||||
remark varchar(500) default null comment '备注',
|
||||
primary key (id),
|
||||
unique key uk_front_project_module (project_id, module_id),
|
||||
key idx_front_project_module_project_id (project_id),
|
||||
key idx_front_project_module_module_id (module_id),
|
||||
constraint fk_front_project_module_project foreign key (project_id) references front_project(project_id),
|
||||
constraint fk_front_project_module_module foreign key (module_id) references sys_module(module_id)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='前台项目功能模块关系';
|
||||
@@ -7,6 +7,7 @@ drop table if exists front_project_file;
|
||||
drop table if exists front_project_generation;
|
||||
drop table if exists front_project_column;
|
||||
drop table if exists front_project_table;
|
||||
drop table if exists front_project_module;
|
||||
drop table if exists front_project;
|
||||
drop table if exists front_user;
|
||||
|
||||
@@ -41,6 +42,7 @@ create table front_project (
|
||||
industry_template varchar(50) default '' comment '行业模板',
|
||||
front_framework varchar(50) default 'Vue3 + Element Plus' comment '前台框架',
|
||||
back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架',
|
||||
frontend_enabled char(1) default '1' comment '是否生成前台前端(1是 0否)',
|
||||
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
||||
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
||||
app_blueprint longtext comment '应用蓝图草稿JSON',
|
||||
@@ -55,6 +57,22 @@ create table front_project (
|
||||
constraint fk_front_project_user foreign key (user_id) references front_user(user_id)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='前台项目草稿表';
|
||||
|
||||
create table front_project_module (
|
||||
id bigint(20) not null auto_increment comment 'id',
|
||||
project_id bigint(20) not null comment '前台项目ID',
|
||||
module_id bigint(20) not null comment '功能模块ID',
|
||||
status char(1) default '0' comment '状态(0正常 1停用)',
|
||||
create_time datetime default null comment '创建时间',
|
||||
update_time datetime default null comment '更新时间',
|
||||
remark varchar(500) default null comment '备注',
|
||||
primary key (id),
|
||||
unique key uk_front_project_module (project_id, module_id),
|
||||
key idx_front_project_module_project_id (project_id),
|
||||
key idx_front_project_module_module_id (module_id),
|
||||
constraint fk_front_project_module_project foreign key (project_id) references front_project(project_id),
|
||||
constraint fk_front_project_module_module foreign key (module_id) references sys_module(module_id)
|
||||
) engine=innodb auto_increment=100 default charset=utf8mb4 comment='前台项目功能模块关系';
|
||||
|
||||
create table front_project_table (
|
||||
table_id bigint(20) not null auto_increment comment '表设计ID',
|
||||
project_id bigint(20) not null comment '项目ID',
|
||||
|
||||
Reference in New Issue
Block a user