diff --git a/RuoYi-Vue/README.md b/RuoYi-Vue/README.md index 591e61b..835b2b8 100644 --- a/RuoYi-Vue/README.md +++ b/RuoYi-Vue/README.md @@ -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. 配置后端 diff --git a/RuoYi-Vue/docs/superpowers/plans/2026-05-24-executable-business-blueprint.md b/RuoYi-Vue/docs/superpowers/plans/2026-05-24-executable-business-blueprint.md new file mode 100644 index 0000000..1707905 --- /dev/null +++ b/RuoYi-Vue/docs/superpowers/plans/2026-05-24-executable-business-blueprint.md @@ -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. diff --git a/RuoYi-Vue/docs/superpowers/specs/2026-05-24-executable-business-blueprint-design.md b/RuoYi-Vue/docs/superpowers/specs/2026-05-24-executable-business-blueprint-design.md new file mode 100644 index 0000000..1f648f3 --- /dev/null +++ b/RuoYi-Vue/docs/superpowers/specs/2026-05-24-executable-business-blueprint-design.md @@ -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` 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. diff --git a/RuoYi-Vue/easycode-web/src/api/project.js b/RuoYi-Vue/easycode-web/src/api/project.js index 7dd10ff..b6adb89 100644 --- a/RuoYi-Vue/easycode-web/src/api/project.js +++ b/RuoYi-Vue/easycode-web/src/api/project.js @@ -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`, diff --git a/RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue b/RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue new file mode 100644 index 0000000..57f3925 --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js b/RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js new file mode 100644 index 0000000..d9f60b4 --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js @@ -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 } + ] + } +} diff --git a/RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs b/RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs new file mode 100644 index 0000000..50d9c7a --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs @@ -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') +}) diff --git a/RuoYi-Vue/easycode-web/src/views/GenerateView.vue b/RuoYi-Vue/easycode-web/src/views/GenerateView.vue index be838db..f6540cf 100644 --- a/RuoYi-Vue/easycode-web/src/views/GenerateView.vue +++ b/RuoYi-Vue/easycode-web/src/views/GenerateView.vue @@ -4,7 +4,7 @@

项目生成工作台

-

先生成数据库结构,确认后再进入项目预览。

+

先生成应用蓝图,再生成数据库结构,保存修改后生成业务蓝图。

@@ -33,6 +33,39 @@ placeholder="例如字段偏好、模块边界、业务规则" /> + + + + + + + + + + + + + + 保存表结构 + + 生成业务蓝图 + -
+

应用蓝图

+ + 摘要 + 模块图 + JSON + 角色:{{ appBlueprint.roles.length }} 前台菜单:{{ appBlueprint.frontendMenus.length }} 后台菜单:{{ appBlueprint.adminMenus.length }}
-
-
+
+

角色

@@ -97,12 +143,14 @@
- -
- 应用调整 -
+ +
-
@@ -148,25 +196,25 @@ 入参:{{ action.requestFields.join('、') }}
-
+
规则
    -
  • {{ rule }}
  • +
  • + {{ rule }} +
影响
    -
  • - {{ effect.description || effect.type }} - / {{ effect.targetTable }} - .{{ effect.targetField }} +
  • + {{ formatEffect(effect) }}
- +
@@ -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; } diff --git a/RuoYi-Vue/easycode-web/src/views/PreviewView.vue b/RuoYi-Vue/easycode-web/src/views/PreviewView.vue index a23467e..bbdca43 100644 --- a/RuoYi-Vue/easycode-web/src/views/PreviewView.vue +++ b/RuoYi-Vue/easycode-web/src/views/PreviewView.vue @@ -10,9 +10,12 @@
- - - +
@@ -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 { diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java index 704dcf7..22b301b 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java @@ -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)); } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java index 21cdc8e..13be57b 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java @@ -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 businessActions; + private transient List modules; + private List 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 getModules() { + return modules; + } + + public void setModules(List modules) { + this.modules = modules; + } + public Boolean getFrontDraft() { return frontDraft; } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java index 272572b..ff94bb2 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java @@ -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 tables; + private List moduleIds; + private List 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 getTables() { return tables; } public void setTables(List tables) { this.tables = tables; } + public List getModuleIds() { return moduleIds; } + public void setModuleIds(List moduleIds) { this.moduleIds = moduleIds; } + public List getModules() { return modules; } + public void setModules(List modules) { this.modules = modules; } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProjectModule.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProjectModule.java new file mode 100644 index 0000000..53875fe --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProjectModule.java @@ -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; } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java index 4ceae1e..8cf0d18 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java @@ -12,6 +12,7 @@ public class BusinessActionDesign private Boolean transaction; private List requestFields; private List rules; + private List ruleChecks; private List effects; public String getCode() { return code; } @@ -30,6 +31,8 @@ public class BusinessActionDesign public void setRequestFields(List requestFields) { this.requestFields = requestFields; } public List getRules() { return rules; } public void setRules(List rules) { this.rules = rules; } + public List getRuleChecks() { return ruleChecks; } + public void setRuleChecks(List ruleChecks) { this.ruleChecks = ruleChecks; } public List getEffects() { return effects; } public void setEffects(List effects) { this.effects = effects; } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java index e1f2d40..540f5fd 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java @@ -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 values; + private String amount; + private String minValue; private List 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 getValues() { return values; } + public void setValues(Map 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 getConditionFields() { return conditionFields; } public void setConditionFields(List 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; } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionRuleDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionRuleDesign.java new file mode 100644 index 0000000..4b6054b --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionRuleDesign.java @@ -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 expectedValues; + private String compareValue; + private List 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 getExpectedValues() { return expectedValues; } + public void setExpectedValues(List expectedValues) { this.expectedValues = expectedValues; } + public String getCompareValue() { return compareValue; } + public void setCompareValue(String compareValue) { this.compareValue = compareValue; } + public List getConditionFields() { return conditionFields; } + public void setConditionFields(List conditionFields) { this.conditionFields = conditionFields; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java index 16cefe4..9a91a58 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java @@ -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 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 getModuleIds() { return moduleIds; } + public void setModuleIds(List moduleIds) { this.moduleIds = moduleIds; } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java index 41740eb..375a476 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java @@ -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 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 getModuleIds() { return moduleIds; } + public void setModuleIds(List moduleIds) { this.moduleIds = moduleIds; } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java new file mode 100644 index 0000000..5e2b30a --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java @@ -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; } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front/FrontProjectModuleMapper.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front/FrontProjectModuleMapper.java new file mode 100644 index 0000000..42d4b5e --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front/FrontProjectModuleMapper.java @@ -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 selectModulesByProjectId(Long projectId); + + public List selectSysProjectModulesByProjectId(Long projectId); + + public int insertFrontProjectModule(FrontProjectModule frontProjectModule); + + public int deleteModulesByProjectId(Long projectId); +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java index a3a1421..10b1df1 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java @@ -106,12 +106,17 @@ public class GenProjectServiceImpl implements IGenProjectService { List structureNodes = selectStructureNodes(project, projectTemplate); Map sourceNodeMap = new HashMap(); - Map> nodeMap = new LinkedHashMap>(); for (SysProjectStructure node : structureNodes) { sourceNodeMap.put(node.getNodeId(), node); + } + + Map> nodeMap = new LinkedHashMap>(); + Map> mergedFolderMap = new LinkedHashMap>(); + for (SysProjectStructure node : structureNodes) { if (isTableScopedNode(node)) { for (GenTable table : projectTables) { Map 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 getProjectModules(GenProject project) { if (project.isFrontDraft()) { - return new ArrayList(); + return project.getModules() == null ? new ArrayList() : 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 mergeTableScopedFolderNode(Map> mergedFolderMap, + SysProjectStructure node, GenProject project, GenTable table, + Map sourceNodeMap, + Map nodeData) { + if (!"folder".equals(node.getNodeType()) || nodeData == null) { + return nodeData; + } + String mergeKey = getTableScopedFolderMergeKey(node, project, table, sourceNodeMap); + Map 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 sourceNodeMap) { + List pathParts = new ArrayList(); + 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> roots, Map nodeData, SysProjectStructure node, Map> nodeMap, Map 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> children = (List>) parentNode.get("children"); if (children != null) { - children.add(nodeData); + addNodeIfAbsent(children, nodeData); } } + private void addNodeIfAbsent(List> nodes, Map nodeData) { + for (Map node : nodes) { + if (node == nodeData) { + return; + } + } + nodes.add(nodeData); + } + private boolean matchesTemplateFile(TemplateFile templateFile, String templateFileName) { if (templateFile == null || StringUtils.isEmpty(templateFileName)) { return false; diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java index 69dee98..a2d9784 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java @@ -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 tableNames = collectTableNames(response.getTables()); Set 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 tableNames, Set 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 tableNames, Set 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 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 columnNames) + { + if (effect.getValues() == null) + { + return; + } + for (Map.Entry 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 tableNames, String messagePrefix) + { + if (StringUtils.isBlank(targetTable) || !tableNames.contains(targetTable)) + { + throw new ServiceException(messagePrefix + StringUtils.defaultString(targetTable)); + } + } + + private void validateConditionFields(List conditionFields, Set 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 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 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 collectTableNames(List tables) { Set tableNames = new HashSet(); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java index 21a4982..c9f14b2 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java @@ -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()); 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 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()); + } + 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 actionCodes = new HashSet(); Set columnNames = collectColumnNames(response); Map> columnNamesByTable = collectColumnNamesByTable(response); + List normalizedActions = new ArrayList(); 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 usedCodes) @@ -663,6 +819,57 @@ public class AiGenerateServiceImpl implements IAiGenerateService return normalizedValues; } + private void normalizeBusinessRuleChecks(BusinessActionDesign action, Set columnNames, Map> columnNamesByTable) + { + if (action.getRuleChecks() == null) + { + action.setRuleChecks(new ArrayList()); + return; + } + List normalizedRules = new ArrayList(); + 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> columnNamesByTable, Set columnNames) + { + if (StringUtils.isBlank(targetField)) + { + return ""; + } + Set 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 columnNames, Map> 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 normalizeEffectValues(Map values, String targetTable, + Map> columnNamesByTable, Set columnNames) + { + Map normalizedValues = new LinkedHashMap(); + if (values == null) + { + return normalizedValues; + } + Set tableColumnNames = columnNamesByTable == null ? null : columnNamesByTable.get(targetTable); + int valueIndex = 1; + for (Map.Entry 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> columnNamesByTable, Set columnNames) { if (StringUtils.isBlank(targetField)) @@ -1052,6 +1287,43 @@ public class AiGenerateServiceImpl implements IAiGenerateService } } + private DatabaseDesignResponse loadSavedDatabaseDesign(Long projectId) + { + List 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 tables = new ArrayList(); + for (FrontProjectTable savedTable : savedTables) + { + if (savedTable == null) + { + continue; + } + DatabaseTableDesign table = toDatabaseTableDesign(savedTable); + List savedColumns = frontProjectColumnMapper.selectColumnsByTableId(savedTable.getTableId()); + List columns = new ArrayList(); + 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()); + 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 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() : 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(); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java index 8b5542b..a4e6812 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java @@ -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); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java index 290eb70..a881a11 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java @@ -40,6 +40,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi public List> 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)); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java index 8ece6e5..6925344 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java @@ -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 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 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 modules = frontProjectModuleMapper.selectSysProjectModulesByProjectId(project.getProjectId()); + project.setModules(modules); + List moduleIds = new ArrayList(); + 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 moduleIds) + { + if (projectId == null) + { + return; + } + frontProjectModuleMapper.deleteModulesByProjectId(projectId); + if (moduleIds == null || moduleIds.isEmpty()) + { + return; + } + Set uniqueModuleIds = new LinkedHashSet(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()) diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java index 397beb6..3c9aabe 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java @@ -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 generateBusinessBlueprint(Long userId, Long projectId, GenerateBusinessBlueprintRequest request); } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectService.java index 618fbeb..635c91f 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectService.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectService.java @@ -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 listAvailableModules(); } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml b/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml index 2b77b57..34f94e9 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml @@ -16,6 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -28,7 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - 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 + 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 + + + + + + insert into front_project_module + + project_id, + module_id, + status, + create_time, + update_time, + remark, + + + #{projectId}, + #{moduleId}, + #{status}, + #{createTime}, + #{updateTime}, + #{remark}, + + + + + delete from front_project_module where project_id = #{projectId} + + diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/generator/SysModuleMapper.xml b/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/generator/SysModuleMapper.xml index 3ff48f1..6b19d5c 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/generator/SysModuleMapper.xml +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/generator/SysModuleMapper.xml @@ -29,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and module_alias = #{moduleAlias} and module_desc = #{moduleDesc} and status = #{status} + and del_flag = #{delFlag} @@ -92,4 +93,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{moduleId} - \ No newline at end of file + diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java index 9c589df..5de5c56 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java @@ -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> structure = service.getProjectStructure(project, "admin_frontend"); + + Map src = child(structure.get(0), 0); + Map api = child(src, 0); + assertEquals(1, children(api).size()); + Map 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 views = child(src, 1); + assertEquals(1, children(views).size()); + Map 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 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 child(Map node, int index) { - return ((List>) node.get("children")).get(index); + return children(node).get(index); + } + + @SuppressWarnings("unchecked") + private List> children(Map node) { + return (List>) 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); + } } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java index 21a7e41..e27bdc1 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest()); + + assertEquals(1, actions.size()); + assertEquals("borrow_book", actions.get(0).getCode()); + ArgumentCaptor 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 actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest()); - List 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 actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest()); - List requestFields = response.getBusinessActions().get(0).getRequestFields(); + List requestFields = actions.get(0).getRequestFields(); assertEquals(1, requestFields.size()); assertEquals("reader_id", requestFields.get(0)); ArgumentCaptor 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 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" + + "}"; + } } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java index c3aa8a0..cb1b3fb 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java @@ -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 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.>emptyList()); + + previewService.getStructure(100L, 200L, "backend"); + + ArgumentCaptor 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(); + } } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectSchemaScriptTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectSchemaScriptTest.java new file mode 100644 index 0000000..9db42a7 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectSchemaScriptTest.java @@ -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); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java index 76d35e3..6512a3b 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java @@ -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 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() { diff --git a/RuoYi-Vue/sql/business_blueprint_templates.sql b/RuoYi-Vue/sql/business_blueprint_templates.sql index b8e47b0..5a01ff2 100644 --- a/RuoYi-Vue/sql/business_blueprint_templates.sql +++ b/RuoYi-Vue/sql/business_blueprint_templates.sql @@ -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 insertValues${effectIndex} = new java.util.LinkedHashMap(); +#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 updateValues${effectIndex} = new java.util.LinkedHashMap(); +#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 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 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 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 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 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 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 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 args = new ArrayList(); + for (Map.Entry 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 params, String tableName, Map 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 args = new ArrayList(); + for (Map.Entry 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 params, String tableName, String targetField, Object value, String[] conditionFields) + { + Map values = new java.util.LinkedHashMap(); + values.put(targetField, value); + return updateFields(params, tableName, values, conditionFields); + } + + private int increaseNumberField(Map 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 args = new ArrayList(); + args.add(amount); + appendConditionValues(args, params, conditionFields); + return jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = " + safeTargetField + " + ? where " + whereClause, args.toArray()); + } + + private int decreaseNumberField(Map 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 args = new ArrayList(); + 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 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 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 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> 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 args, Map params, String[] conditionFields) + { + for (Object value : conditionValues(params, conditionFields)) { + args.add(value); + } + } + + private Object resolveBusinessValue(Map 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 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() }) }, diff --git a/RuoYi-Vue/sql/db.sql b/RuoYi-Vue/sql/db.sql index 4880a9e..6fa8829 100644 --- a/RuoYi-Vue/sql/db.sql +++ b/RuoYi-Vue/sql/db.sql @@ -1483,3 +1483,1634 @@ values '["Spring Cloud","Nacos","Gateway"]', '["网关服务","认证中心","系统服务","日志中心","文件服务"]', '["微服务基础架构","权限平台","多服务治理"]', '', '', '1.0.0', '', 6, '0', '0', 'admin', sysdate(), 'admin', sysdate(), null); SET FOREIGN_KEY_CHECKS = 1; + +-- EasyCode business blueprint templates +-- Business blueprint Velocity templates +-- Import this script after sql/db.sql. +-- Template ids: +-- 9101 backend: RuoYi Spring Boot + MyBatis +-- 9102 admin_frontend: Vue2 + Element UI for RuoYi admin +-- 9103 frontend: Vue3 + Element Plus portal + +SET FOREIGN_KEY_CHECKS = 0; + +DELETE FROM sys_project_template WHERE template_id IN (9101, 9102, 9103); +DELETE FROM sys_project_structure WHERE template_id IN (9101, 9102, 9103) OR node_id BETWEEN 910100 AND 910399; +DELETE FROM sys_template_file WHERE template_id IN (9101, 9102, 9103) OR template_file_id BETWEEN 910100 AND 910399; +DELETE FROM sys_template WHERE template_id IN (9101, 9102, 9103); + +INSERT INTO sys_template (template_id, template_name, template_path, template_desc, template_type, template_status, create_by, create_time, remark) VALUES +(9101, 'Business Blueprint Backend', '/blueprint/backend', 'RuoYi backend with CRUD and executable blueprint effects', 'backend', 0, 'admin', sysdate(), 'business blueprint'), +(9102, 'Business Blueprint Admin Frontend', '/blueprint/admin_frontend', 'Vue2 Element UI admin pages with blueprint action buttons', 'admin_frontend', 0, 'admin', sysdate(), 'business blueprint'), +(9103, 'Business Blueprint Portal Frontend', '/blueprint/frontend', 'Vue3 Element Plus portal pages with blueprint action buttons', 'frontend', 0, 'admin', sysdate(), 'business blueprint'); + +-- Backend template files +INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES +(910101, 9101, 'pom.xml.vm', NULL, 'pom.xml.vm', +' + 4.0.0 + ${packageName} + ${projectFileName}-backend + 1.0.0 + ${projectName} + Generated backend with business blueprint actions + + + com.ruoyi + ruoyi + 3.8.8 + + + + + com.ruoyi + ruoyi-common + + + com.ruoyi + ruoyi-framework + + + +', 'admin', sysdate()), +(910102, 9101, 'application.yml.vm', NULL, 'application.yml.vm', +'server: + port: 8080 + +spring: + application: + name: ${projectFileName}-backend +', 'admin', sysdate()), +(910103, 9101, 'README.md.vm', NULL, 'README.md.vm', +'# ${projectName} Backend + +Generated from EasyCode business blueprint templates. + +Create table SQL: + +```sql +${createTableSql} +``` + +# Business Actions + +#if($hasBusinessActions) +#foreach($action in $businessActions) +- ${action.name} `${action.code}` `${action.method} ${action.path}` owner `${action.ownerTable}` +#end +#else +No business actions in blueprint. +#end +', 'admin', sysdate()), +(910104, 9101, 'domain.java.vm', NULL, 'domain.java.vm', +'package ${packageName}.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + * ${functionName} object ${tableName} + * + * @author ${author} + * @date ${datetime} + */ +public class ${ClassName} extends BaseEntity +{ + private static final long serialVersionUID = 1L; + +#foreach($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + /** ${column.columnComment} */ +#if($column.javaType == "Date") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") +#end +#if($column.isList == "1") + @Excel(name = "${column.columnComment}") +#end + private ${column.javaType} ${column.javaField}; + +#end +#end +#foreach($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + public void set${column.capJavaField}(${column.javaType} ${column.javaField}) + { + this.${column.javaField} = ${column.javaField}; + } + + public ${column.javaType} get${column.capJavaField}() + { + return ${column.javaField}; + } + +#end +#end + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) +#foreach($column in $columns) +#if(!$table.isSuperColumn($column.javaField)) + .append("${column.javaField}", get${column.capJavaField}()) +#end +#end + .toString(); + } +} +', 'admin', sysdate()), +(910105, 9101, 'mapper.java.vm', NULL, 'mapper.java.vm', +'package ${packageName}.mapper; + +import java.util.List; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName} Mapper + */ +public interface ${ClassName}Mapper +{ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + public int insert${ClassName}(${ClassName} ${className}); + + public int update${ClassName}(${ClassName} ${className}); + + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); +} +', 'admin', sysdate()), +(910106, 9101, 'mapper.xml.vm', NULL, 'mapper.xml.vm', +' + + + + +#foreach($column in $columns) +#if($column.isPk == "1") + +#else + +#end +#end + + + + select +#foreach($column in $columns) + ${column.columnName}#if($foreach.hasNext),#end +#end + from ${tableName} + + + + + + + + insert into ${tableName} + +#foreach($column in $columns) +#if($column.isInsert == "1" && $column.isIncrement != "1") + ${column.columnName}, +#end +#end + + +#foreach($column in $columns) +#if($column.isInsert == "1" && $column.isIncrement != "1") + #{${column.javaField}}, +#end +#end + + + + + update ${tableName} + +#foreach($column in $columns) +#if($column.isEdit == "1" && $column.isPk != "1") + ${column.columnName} = #{${column.javaField}}, +#end +#end + + where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}} + + + + delete from ${tableName} where ${pkColumn.columnName} in + + #{${pkColumn.javaField}} + + + +', 'admin', sysdate()), +(910107, 9101, 'service.java.vm', NULL, 'service.java.vm', +'package ${packageName}.service; + +import java.util.List; +import java.util.Map; +import ${packageName}.domain.${ClassName}; + +/** + * ${functionName} Service + */ +public interface I${ClassName}Service +{ + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}); + + public int insert${ClassName}(${ClassName} ${className}); + + public int update${ClassName}(${ClassName} ${className}); + + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s); + + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}); + +#if($hasTableBusinessActions) +#foreach($action in $tableBusinessActions) + public int ${action.code}(Map params); + +#end +#end +} +', 'admin', sysdate()), +(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; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.common.exception.ServiceException; +import ${packageName}.domain.${ClassName}; +import ${packageName}.mapper.${ClassName}Mapper; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName} Service implementation + */ +@Service +public class ${ClassName}ServiceImpl implements I${ClassName}Service +{ + @Resource + private ${ClassName}Mapper ${className}Mapper; +#if($hasTableBusinessActions) + @Resource + private JdbcTemplate jdbcTemplate; +#end + + @Override + public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + + @Override + public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) + { + return ${className}Mapper.select${ClassName}List(${className}); + } + + @Override + public int insert${ClassName}(${ClassName} ${className}) + { + return ${className}Mapper.insert${ClassName}(${className}); + } + + @Override + public int update${ClassName}(${ClassName} ${className}) + { + return ${className}Mapper.update${ClassName}(${className}); + } + + @Override + public int delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s); + } + + @Override + public int delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField}) + { + return ${className}Mapper.delete${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField}); + } + +#if($hasTableBusinessActions) +#foreach($action in $tableBusinessActions) + /** + * ${action.name} +#if($action.requestFields) + * Request fields: +#foreach($field in $action.requestFields) + * - ${field} +#end +#end +#if($action.rules) + * Rules: +#foreach($rule in $action.rules) + * - ${rule} +#end +#end +#if($action.effects) + * Effects: +#foreach($effect in $action.effects) + * - ${effect.type} ${effect.targetTable}.${effect.targetField} ${effect.value} ${effect.description} +#end +#end + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int ${action.code}(Map params) + { + if (params == null) { + throw new ServiceException("业务动作参数不能为空"); + } +#if($action.requestFields) +#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) +#foreach($effect in $action.effects) +#if($effect.type == "UPDATE_FIELD") +#set($conditionFields = $effect.conditionFields) +#if(!$conditionFields || $conditionFields.size() == 0) +#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 insertValues${effectIndex} = new java.util.LinkedHashMap(); +#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 updateValues${effectIndex} = new java.util.LinkedHashMap(); +#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 +#end +#end + return rows; + } + +#end + private void requireBusinessParam(Map params, String field) + { + if (!params.containsKey(field) || params.get(field) == null) { + throw new ServiceException("缺少业务动作参数:" + field); + } + } + + private void assertExists(Map 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 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 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 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 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 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 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 args = new ArrayList(); + for (Map.Entry 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 params, String tableName, Map 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 args = new ArrayList(); + for (Map.Entry 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 params, String tableName, String targetField, Object value, String[] conditionFields) + { + Map values = new java.util.LinkedHashMap(); + values.put(targetField, value); + return updateFields(params, tableName, values, conditionFields); + } + + private int increaseNumberField(Map 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 args = new ArrayList(); + args.add(amount); + appendConditionValues(args, params, conditionFields); + return jdbcTemplate.update("update " + safeTableName + " set " + safeTargetField + " = " + safeTargetField + " + ? where " + whereClause, args.toArray()); + } + + private int decreaseNumberField(Map 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 args = new ArrayList(); + 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 params, String tableName, String targetField, + String valueExpression, String[] conditionFields) + { + String safeTableName = requireSafeIdentifier(tableName, "业务影响目标表不合法"); + String safeTargetField = requireSafeIdentifier(targetField, "业务影响目标字段不合法"); + String safeValueExpression = requireSafeExpression(valueExpression); + String whereClause = buildWhereClause(params, conditionFields); + Object[] values = conditionValues(params, conditionFields); + String sql = "update " + safeTableName + " set " + safeTargetField + " = " + safeValueExpression + " where " + whereClause; + return jdbcTemplate.update(sql, values); + } + + private String buildWhereClause(Map params, String[] conditionFields) + { + if (conditionFields == null || conditionFields.length == 0) { + throw new ServiceException("业务影响缺少条件字段"); + } + StringBuilder where = new StringBuilder(); + for (String field : conditionFields) { + String safeField = requireSafeIdentifier(field, "业务影响条件字段不合法"); + requireBusinessParam(params, safeField); + if (where.length() > 0) { + where.append(" and "); + } + where.append(safeField).append(" = ?"); + } + return where.toString(); + } + + private Object[] conditionValues(Map params, String[] conditionFields) + { + Object[] values = new Object[conditionFields.length]; + for (int i = 0; i < conditionFields.length; i++) { + values[i] = params.get(conditionFields[i]); + } + return values; + } + + private int countRows(Map 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 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> 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 args, Map params, String[] conditionFields) + { + for (Object value : conditionValues(params, conditionFields)) { + args.add(value); + } + } + + private Object resolveBusinessValue(Map 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 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_]*")) { + throw new ServiceException(message); + } + return value; + } + + private String requireSafeExpression(String valueExpression) + { + if (valueExpression == null || !valueExpression.matches("[A-Za-z0-9_ +*/().-]{1,200}") + || valueExpression.contains(";") || valueExpression.contains("--")) { + throw new ServiceException("业务影响字段表达式不合法"); + } + return valueExpression; + } +#end +} +', 'admin', sysdate()), +(910109, 9101, 'controller.java.vm', NULL, 'controller.java.vm', +'package ${packageName}.controller; + +import java.util.List; +import java.util.Map; +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import ${packageName}.domain.${ClassName}; +import ${packageName}.service.I${ClassName}Service; + +/** + * ${functionName} Controller + */ +@RestController +@RequestMapping("/${moduleName}/${businessName}") +public class ${ClassName}Controller extends BaseController +{ + @Resource + private I${ClassName}Service ${className}Service; + + @GetMapping("/list") + public TableDataInfo list(${ClassName} ${className}) + { + startPage(); + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + return getDataTable(list); + } + + @PostMapping("/export") + public void export(HttpServletResponse response, ${ClassName} ${className}) + { + List<${ClassName}> list = ${className}Service.select${ClassName}List(${className}); + ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}.class); + util.exportExcel(response, list, "${functionName}"); + } + + @GetMapping(value = "/{${pkColumn.javaField}}") + public AjaxResult getInfo(@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) + { + return success(${className}Service.select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaField})); + } + + @Log(title = "${functionName}", businessType = BusinessType.INSERT) + @PostMapping + public AjaxResult add(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.insert${ClassName}(${className})); + } + + @Log(title = "${functionName}", businessType = BusinessType.UPDATE) + @PutMapping + public AjaxResult edit(@RequestBody ${ClassName} ${className}) + { + return toAjax(${className}Service.update${ClassName}(${className})); + } + + @Log(title = "${functionName}", businessType = BusinessType.DELETE) + @DeleteMapping("/{${pkColumn.javaField}s}") + public AjaxResult remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) + { + return toAjax(${className}Service.delete${ClassName}By${pkColumn.capJavaField}s(${pkColumn.javaField}s)); + } + +#if($hasTableBusinessActions) +#foreach($action in $tableBusinessActions) + @Log(title = "${action.name}", businessType = BusinessType.OTHER) +#if($action.method == "GET") + @GetMapping("${action.path}") + public AjaxResult ${action.code}(@RequestParam Map params) + { + return toAjax(${className}Service.${action.code}(params)); + } +#elseif($action.method == "PUT") + @PutMapping("${action.path}") + public AjaxResult ${action.code}(@RequestBody Map params) + { + return toAjax(${className}Service.${action.code}(params)); + } +#elseif($action.method == "DELETE") + @DeleteMapping("${action.path}") + public AjaxResult ${action.code}(@RequestBody(required = false) Map params) + { + return toAjax(${className}Service.${action.code}(params)); + } +#else + @PostMapping("${action.path}") + public AjaxResult ${action.code}(@RequestBody Map params) + { + return toAjax(${className}Service.${action.code}(params)); + } +#end + +#end +#end +} +', 'admin', sysdate()); + +-- Admin frontend template files +INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES +(910201, 9102, 'package.json.vm', NULL, 'package.json.vm', +'{ + "name": "${projectFileName}-admin", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vue-cli-service serve", + "build": "vue-cli-service build" + }, + "dependencies": { + "element-ui": "2.15.14", + "vue": "2.6.14", + "vue-router": "3.6.5", + "vuex": "3.6.2" + } +} +', 'admin', sysdate()), +(910202, 9102, 'api.js.vm', NULL, 'api.js.vm', +'import request from "@/utils/request" + +const baseUrl = "/${moduleName}/${businessName}" + +export function list${ClassName}(query) { + return request({ + url: baseUrl + "/list", + method: "get", + params: query + }) +} + +export function get${ClassName}(${pkColumn.javaField}) { + return request({ + url: baseUrl + "/" + ${pkColumn.javaField}, + method: "get" + }) +} + +export function add${ClassName}(data) { + return request({ + url: baseUrl, + method: "post", + data + }) +} + +export function update${ClassName}(data) { + return request({ + url: baseUrl, + method: "put", + data + }) +} + +export function del${ClassName}(${pkColumn.javaField}) { + return request({ + url: baseUrl + "/" + ${pkColumn.javaField}, + method: "delete" + }) +} + +#if($hasTableBusinessActions) +#foreach($action in $tableBusinessActions) +export function ${action.code}(data) { + return request({ + url: baseUrl + "${action.path}", + method: "${action.method.toLowerCase()}", +#if($action.method == "GET") + params: data +#else + data +#end + }) +} + +#end +#end +', 'admin', sysdate()), +(910203, 9102, 'index.vue.vm', NULL, 'index.vue.vm', +' + + +', 'admin', sysdate()); + +INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES +(910204, 9102, 'adminMenus.js.vm', NULL, 'adminMenus.js.vm', +'export const appRoles = [ +#foreach($role in $appRoles) + { code: "${role.code}", name: "${role.name}", description: "${role.description}" }#if($foreach.hasNext),#end +#end +] + +export const adminMenus = [ +#foreach($menu in $adminMenus) + { + code: "${menu.code}", + name: "${menu.name}", + path: "${menu.path}", + requiresLogin: ${menu.requiresLogin}, + visibleRoles: [ +#foreach($role in $menu.visibleRoles) + "${role}"#if($foreach.hasNext),#end +#end + ], + dataScope: "${menu.dataScope}" + }#if($foreach.hasNext),#end +#end +] + +export function requiresLogin(menu) { + return Boolean(menu && menu.requiresLogin) +} + +export function canShowMenu(menu, currentUser = {}) { + if (!menu) { + return false + } + if (menu.requiresLogin && !currentUser.id && !currentUser.userId) { + return false + } + const userRoles = Array.isArray(currentUser.roles) ? currentUser.roles : [] + if (!Array.isArray(menu.visibleRoles) || menu.visibleRoles.length === 0) { + return true + } + return menu.visibleRoles.some(role => userRoles.includes(role)) +} +', 'admin', sysdate()); + +INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES +(910205, 9102, 'adminPermission.js.vm', NULL, 'adminPermission.js.vm', +'import { adminMenus, canShowMenu, requiresLogin } from "./config/adminMenus" + +function defaultCurrentUser() { + try { + return JSON.parse(localStorage.getItem("currentUser") || "{}") + } catch (error) { + return {} + } +} + +function isLoggedIn(user) { + return Boolean(user && (user.id || user.userId)) +} + +export function installAdminGuard(router, getCurrentUser = defaultCurrentUser) { + router.beforeEach((to, from, next) => { + const menu = adminMenus.find(item => item.path === to.path) + const user = getCurrentUser() || {} + if (menu && !canShowMenu(menu, user)) { + next(false) + return + } + if (menu && requiresLogin(menu) && !isLoggedIn(user)) { + next("/login") + return + } + next() + }) +} +', 'admin', sysdate()); + +-- Portal frontend template files +INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES +(910301, 9103, 'package.json.vm', NULL, 'package.json.vm', +'{ + "name": "${projectFileName}-portal", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build" + }, + "dependencies": { + "@vitejs/plugin-vue": "5.0.5", + "axios": "1.7.2", + "element-plus": "2.7.6", + "vite": "5.3.1", + "vue": "3.4.29", + "vue-router": "4.4.0" + }, + "devDependencies": {} +} +', 'admin', sysdate()), +(910302, 9103, 'index.html.vm', NULL, 'index.html.vm', +' + + + + + ${projectName} + + +
+ + + +', 'admin', sysdate()), +(910303, 9103, 'main.js.vm', NULL, 'main.js.vm', +'import { createApp } from "vue" +import ElementPlus from "element-plus" +import "element-plus/dist/index.css" +import App from "./App.vue" +import router from "./router" + +createApp(App).use(router).use(ElementPlus).mount("#app") +', 'admin', sysdate()), +(910304, 9103, 'App.vue.vm', NULL, 'App.vue.vm', +' + + + + +', 'admin', sysdate()), +(910305, 9103, 'router.js.vm', NULL, 'router.js.vm', +'import { createRouter, createWebHistory } from "vue-router" +import { canShowMenu, frontendMenus, requiresLogin } from "../config/frontendMenus" +#foreach($table in $tables) +import ${table.className}Index from "../views/${table.businessName}/index.vue" +import ${table.className}Detail from "../views/${table.businessName}/detail.vue" +#end + +const routes = [ +#if($hasTables) +#set($firstTable = $tables.get(0)) + { + path: "/", + redirect: "/${firstTable.businessName}" + }, +#else + { + path: "/", + component: { template: "
" } + } +#end +#foreach($table in $tables) + { + path: "/${table.businessName}", + component: ${table.className}Index + }, + { + path: "/${table.businessName}/:id", + component: ${table.className}Detail + }#if($foreach.hasNext),#end +#end +] + +function currentUser() { + try { + return JSON.parse(localStorage.getItem("currentUser") || "{}") + } catch (error) { + return {} + } +} + +function isLoggedIn(user) { + return Boolean(user && (user.id || user.userId)) +} + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +router.beforeEach((to, from, next) => { + const menu = frontendMenus.find(item => item.path === to.path) + const user = currentUser() + if (menu && !canShowMenu(menu, user)) { + next(false) + return + } + if (menu && requiresLogin(menu) && !isLoggedIn(user)) { + next("/login") + return + } + next() +}) + +export default router +', 'admin', sysdate()), +(910306, 9103, 'request.js.vm', NULL, 'request.js.vm', +'import axios from "axios" +import { ElMessage } from "element-plus" + +const request = axios.create({ + baseURL: import.meta.env.VITE_APP_BASE_API || "", + timeout: 15000 +}) + +request.interceptors.response.use( + response => response.data, + error => { + ElMessage.error(error.response?.data?.msg || error.message || "请求失败") + return Promise.reject(error) + } +) + +export default request +', 'admin', sysdate()), +(910307, 9103, 'api.js.vm', NULL, 'api.js.vm', +'import request from "./request" + +const baseUrl = "/${moduleName}/${businessName}" + +export function list${ClassName}(query) { + return request({ + url: baseUrl + "/list", + method: "get", + params: query + }) +} + +export function get${ClassName}(${pkColumn.javaField}) { + return request({ + url: baseUrl + "/" + ${pkColumn.javaField}, + method: "get" + }) +} + +#if($hasTableBusinessActions) +#foreach($action in $tableBusinessActions) +export function ${action.code}(data) { + return request({ + url: baseUrl + "${action.path}", + method: "${action.method.toLowerCase()}", +#if($action.method == "GET") + params: data +#else + data +#end + }) +} + +#end +#end +', 'admin', sysdate()), +(910308, 9103, 'index.vue.vm', NULL, 'index.vue.vm', +' + + + + +', 'admin', sysdate()), +(910309, 9103, 'detail.vue.vm', NULL, 'detail.vue.vm', +' + + + + +', 'admin', sysdate()); + +INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES +(910310, 9103, 'frontendMenus.js.vm', NULL, 'frontendMenus.js.vm', +'export const appRoles = [ +#foreach($role in $appRoles) + { code: "${role.code}", name: "${role.name}", description: "${role.description}" }#if($foreach.hasNext),#end +#end +] + +export const frontendMenus = [ +#foreach($menu in $frontendMenus) + { + code: "${menu.code}", + name: "${menu.name}", + path: "${menu.path}", + requiresLogin: ${menu.requiresLogin}, + visibleRoles: [ +#foreach($role in $menu.visibleRoles) + "${role}"#if($foreach.hasNext),#end +#end + ], + dataScope: "${menu.dataScope}" + }#if($foreach.hasNext),#end +#end +] + +export function requiresLogin(menu) { + return Boolean(menu && menu.requiresLogin) +} + +export function canShowMenu(menu, currentUser = {}) { + if (!menu) { + return false + } + if (menu.requiresLogin && !currentUser.id && !currentUser.userId) { + return false + } + const userRoles = Array.isArray(currentUser.roles) ? currentUser.roles : [] + if (!Array.isArray(menu.visibleRoles) || menu.visibleRoles.length === 0) { + return true + } + return menu.visibleRoles.some(role => userRoles.includes(role)) +} +', 'admin', sysdate()); + +-- Backend project structure +INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES +(910100, 0, '{projectName}-backend', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910101, 910100, 'pom.xml', 'file', NULL, 9101, -1, 'pom.xml.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910102, 910100, 'README.md', 'file', NULL, 9101, -1, 'README.md.vm', 2, '0', 'admin', sysdate(), '', NULL), +(910103, 910100, 'src', 'folder', NULL, 9101, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL), +(910104, 910103, 'main', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910105, 910104, 'java', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910106, 910105, '{packageName}', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910107, 910106, 'domain', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910108, 910106, 'mapper', 'folder', NULL, 9101, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL), +(910109, 910106, 'service', 'folder', NULL, 9101, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL), +(910110, 910109, 'impl', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910111, 910106, 'controller', 'folder', NULL, 9101, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL), +(910112, 910104, 'resources', 'folder', NULL, 9101, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL), +(910113, 910112, 'mapper', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910114, 910113, '{moduleName}', 'folder', NULL, 9101, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910115, 910112, 'application.yml', 'file', NULL, 9101, -1, 'application.yml.vm', 2, '0', 'admin', sysdate(), '', NULL), +(910116, 910107, '{ClassName}.java', 'file', NULL, 9101, 0, 'domain.java.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910117, 910108, '{ClassName}Mapper.java', 'file', NULL, 9101, 0, 'mapper.java.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910118, 910109, 'I{ClassName}Service.java', 'file', NULL, 9101, 0, 'service.java.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910119, 910110, '{ClassName}ServiceImpl.java', 'file', NULL, 9101, 0, 'serviceImpl.java.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910120, 910111, '{ClassName}Controller.java', 'file', NULL, 9101, 0, 'controller.java.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910121, 910114, '{ClassName}Mapper.xml', 'file', NULL, 9101, 0, 'mapper.xml.vm', 1, '0', 'admin', sysdate(), '', NULL); + +-- Admin frontend project structure +INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES +(910200, 0, '{projectName}-admin', 'folder', NULL, 9102, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910201, 910200, 'package.json', 'file', NULL, 9102, -1, 'package.json.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910202, 910200, 'src', 'folder', NULL, 9102, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL), +(910203, 910202, 'api', 'folder', NULL, 9102, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910204, 910203, '{moduleName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910205, 910204, '{businessName}.js', 'file', NULL, 9102, 0, 'api.js.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910206, 910202, 'views', 'folder', NULL, 9102, NULL, NULL, 2, '0', 'admin', sysdate(), '', NULL), +(910207, 910206, '{moduleName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910208, 910207, '{businessName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910209, 910208, 'index.vue', 'file', NULL, 9102, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL); + +INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES +(910210, 910202, 'config', 'folder', NULL, 9102, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL), +(910211, 910210, 'adminMenus.js', 'file', NULL, 9102, -1, 'adminMenus.js.vm', 1, '0', 'admin', sysdate(), '', NULL); + +INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES +(910212, 910202, 'permission.js', 'file', NULL, 9102, -1, 'adminPermission.js.vm', 4, '0', 'admin', sysdate(), '', NULL); + +-- Portal frontend project structure +INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES +(910300, 0, '{projectName}-portal', 'folder', NULL, 9103, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910301, 910300, 'package.json', 'file', NULL, 9103, -1, 'package.json.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910302, 910300, 'index.html', 'file', NULL, 9103, -1, 'index.html.vm', 2, '0', 'admin', sysdate(), '', NULL), +(910303, 910300, 'src', 'folder', NULL, 9103, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL), +(910304, 910303, 'main.js', 'file', NULL, 9103, -1, 'main.js.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910305, 910303, 'App.vue', 'file', NULL, 9103, -1, 'App.vue.vm', 2, '0', 'admin', sysdate(), '', NULL), +(910306, 910303, 'router', 'folder', NULL, 9103, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL), +(910307, 910306, 'index.js', 'file', NULL, 9103, -1, 'router.js.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910308, 910303, 'api', 'folder', NULL, 9103, NULL, NULL, 4, '0', 'admin', sysdate(), '', NULL), +(910309, 910308, 'request.js', 'file', NULL, 9103, -1, 'request.js.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910310, 910308, '{businessName}.js', 'file', NULL, 9103, 0, 'api.js.vm', 2, '0', 'admin', sysdate(), '', NULL), +(910311, 910303, 'views', 'folder', NULL, 9103, NULL, NULL, 5, '0', 'admin', sysdate(), '', NULL), +(910312, 910311, '{businessName}', 'folder', NULL, 9103, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL), +(910313, 910312, 'index.vue', 'file', NULL, 9103, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL), +(910314, 910312, 'detail.vue', 'file', NULL, 9103, 0, 'detail.vue.vm', 2, '0', 'admin', sysdate(), '', NULL); + +INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES +(910315, 910303, 'config', 'folder', NULL, 9103, NULL, NULL, 6, '0', 'admin', sysdate(), '', NULL), +(910316, 910315, 'frontendMenus.js', 'file', NULL, 9103, -1, 'frontendMenus.js.vm', 1, '0', 'admin', sysdate(), '', NULL); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/RuoYi-Vue/sql/front_project_upgrade.sql b/RuoYi-Vue/sql/front_project_upgrade.sql new file mode 100644 index 0000000..23a1bea --- /dev/null +++ b/RuoYi-Vue/sql/front_project_upgrade.sql @@ -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='前台项目功能模块关系'; diff --git a/RuoYi-Vue/sql/front_workbench.sql b/RuoYi-Vue/sql/front_workbench.sql index 86b8cc8..e7e188a 100644 --- a/RuoYi-Vue/sql/front_workbench.sql +++ b/RuoYi-Vue/sql/front_workbench.sql @@ -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',