# Qing Unified Business Action Dialog 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:** Replace chained business-action prompts with one generated dynamic form, automatically resolve record/page parameters, and inject authenticated-user parameters on the backend for every Qing business button. **Architecture:** Add transient render metadata to each `BusinessActionDesign`: trusted current-user fields and user-editable input-field definitions. Both Qing generation paths enrich actions from the saved database schema. The Vue template resolves available values first, opens one Element UI dialog for unresolved editable fields, and submits once; the controller overwrites trusted user fields from the authenticated portal user before the service validates `requestFields`. **Tech Stack:** Java 8, Spring Boot 2.5, Apache Velocity, Vue 2, Element UI, JUnit 4, Mockito, Maven. --- ## File Map - Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionInputFieldDesign.java` - Render-only metadata for one dynamic business-action form field. - Create `ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java` - Infers authenticated-user fields and database-backed input metadata. - Create `ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java` - Unit tests inference and schema metadata lookup independently of templates. - Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java` - Stores transient `currentUserFields` and `inputFields`. - Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java` - Enriches normal Qing backend/frontend actions and upgrades stale installed templates. - Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java` - Enriches business actions rendered through the business-block path. - Modify `ruoyi-generator/src/main/resources/qing/controller.java.vm` - Overwrites trusted request fields using the authenticated portal user. - Modify `ruoyi-generator/src/main/resources/qing/index.vue.vm` - Replaces chained `$prompt` calls with one reusable dynamic dialog. - Modify `ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java` - Covers generated dialog behavior and trusted backend injection. - Modify `ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java` - Covers stale-template fallback and normal preview metadata. - Modify `ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java` - Covers metadata in the secondary generation path. - Modify `sql/qing_templates.sql` - Synchronizes bundled controller and frontend-index template records. - Modify `sql/db.sql` - Synchronizes the same runnable-template seed records. Because the worktree already contains unrelated edits in several target files, inspect each path before staging. Commit only task-owned hunks; if a clean partial stage cannot be produced safely, leave the implementation uncommitted and report that explicitly. ### Task 1: Derive Render Metadata for Every Business Action **Files:** - Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionInputFieldDesign.java` - Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java` - Create: `ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java` - Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java` - [ ] **Step 1: Write the failing metadata test** Create `BusinessActionRenderSupportTest` with a business action containing `book_id`, `user_id`, `due_date`, and `reason`. Give it an `INSERT_ROW` effect whose `user_id` value is `${current_user.id}` and schema columns with comments/control types. ```java @Test public void enrichesTrustedUserFieldsAndEditableInputMetadata() { BusinessActionDesign action = new BusinessActionDesign(); action.setRequestFields(Arrays.asList("book_id", "user_id", "due_date", "reason")); BusinessActionEffectDesign effect = new BusinessActionEffectDesign(); effect.setType("INSERT_ROW"); Map values = new LinkedHashMap(); values.put("user_id", "${current_user.id}"); values.put("book_id", "${param.book_id}"); effect.setValues(values); action.setEffects(Arrays.asList(effect)); GenTable book = table("book"); book.setColumns(Arrays.asList( column("book_id", "bookId", "图书", "input", ""), column("due_date", "dueDate", "应还日期", "datetime", ""), column("reason", "reason", "申请说明", "textarea", ""))); BusinessActionRenderSupport.enrich(action, Arrays.asList(book)); assertEquals(Arrays.asList("user_id"), action.getCurrentUserFields()); assertEquals(Arrays.asList("book_id", "due_date", "reason"), action.getInputFields().stream() .map(BusinessActionInputFieldDesign::getField) .collect(Collectors.toList())); assertEquals("应还日期", action.getInputFields().get(1).getLabel()); assertEquals("datetime", action.getInputFields().get(1).getHtmlType()); assertEquals("textarea", action.getInputFields().get(2).getHtmlType()); } ``` Add a second test proving `${current_user.id}` used as an effect `value`, rule `expectedValue`, or rule `compareValue` marks the corresponding `targetField` as trusted. - [ ] **Step 2: Run the test and verify RED** Run: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessActionRenderSupportTest test ``` Expected: compilation fails because `BusinessActionInputFieldDesign`, `BusinessActionRenderSupport`, and the new getters do not exist. - [ ] **Step 3: Add the render-only input DTO** Implement: ```java package com.ruoyi.generator.domain.front.dto; public class BusinessActionInputFieldDesign { private String field; private String label; private String htmlType; private String dictType; public String getField() { return field; } public void setField(String field) { this.field = field; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public String getHtmlType() { return htmlType; } public void setHtmlType(String htmlType) { this.htmlType = htmlType; } public String getDictType() { return dictType; } public void setDictType(String dictType) { this.dictType = dictType; } } ``` Add to `BusinessActionDesign`: ```java private transient List currentUserFields; private transient List inputFields; public List getCurrentUserFields() { return currentUserFields; } public void setCurrentUserFields(List currentUserFields) { this.currentUserFields = currentUserFields; } public List getInputFields() { return inputFields; } public void setInputFields(List inputFields) { this.inputFields = inputFields; } ``` - [ ] **Step 4: Implement deterministic inference** Implement `BusinessActionRenderSupport.enrich(action, tables)` so it: 1. Uses a `LinkedHashSet` to preserve request-field order. 2. Marks map keys whose effect value is exactly `${current_user.id}`. 3. Marks `targetField` when an effect `value`, rule `expectedValue`, rule `compareValue`, or any rule `expectedValues` entry is `${current_user.id}`. 4. Builds `inputFields` from `requestFields - currentUserFields`. 5. Resolves schema metadata by exact `columnName` or `javaField`, preferring the action owner table and then other project tables. 6. Falls back to a humanized field name and `input` control when no schema column matches. Core methods: ```java public static void enrich(BusinessActionDesign action, List tables) { if (action == null) { return; } LinkedHashSet trusted = collectCurrentUserFields(action); action.setCurrentUserFields(new ArrayList(trusted)); List inputs = new ArrayList(); for (String field : safeList(action.getRequestFields())) { if (!trusted.contains(field)) { inputs.add(toInputField(field, action.getOwnerTable(), tables)); } } action.setInputFields(inputs); } ``` The fallback label must use `field.replace('_', ' ')`; the template can display it unchanged without embedding action-specific labels. - [ ] **Step 5: Run the metadata tests and verify GREEN** Run: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessActionRenderSupportTest test ``` Expected: all metadata tests pass. - [ ] **Step 6: Commit the isolated new files if staging is clean** ```powershell git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionInputFieldDesign.java ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java git add -p -- ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java git commit -m "Add Qing business action render metadata" ``` Before committing, inspect `git diff --cached` and omit the commit if it includes pre-existing unrelated hunks. ### Task 2: Feed Metadata into Both Qing Generation Paths **Files:** - Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java` - Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java` - Modify: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java` - Modify: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java` - [ ] **Step 1: Add failing preview-generation assertions** In `GenProjectServiceImplTest`, create a `borrow_book` action with: ```java action.setRequestFields(Arrays.asList("book_id", "user_id", "due_date")); effectValues.put("user_id", "${current_user.id}"); ``` Render `frontend-index.vue.vm` and assert: ```java assertTrue(content.contains("currentUserFields: [\"user_id\"]")); assertTrue(content.contains("field: \"due_date\"")); assertTrue(content.contains("label: \"应还日期\"")); assertTrue(content.contains("htmlType: \"datetime\"")); ``` Add equivalent assertions to `BusinessBlockGenerationServiceTest` so that rendering through `putPageDesignBusinessActions` receives the same metadata. - [ ] **Step 2: Run both tests and verify RED** Run: ```powershell mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest,BusinessBlockGenerationServiceTest test ``` Expected: metadata strings are absent. - [ ] **Step 3: Enrich backend and page actions** In `GenProjectServiceImpl.putTableBusinessActions`, enrich every filtered table action before placing it in Velocity: ```java for (BusinessActionDesign action : tableBusinessActions) { BusinessActionRenderSupport.enrich(action, project.getTables()); } ``` In `applyBusinessButtonConfig`, enrich the copied action after applying visual configuration: ```java BusinessActionRenderSupport.enrich(action, projectTables); ``` Pass `project.getTables()` through `filterPageDesignBusinessActions` and `applyBusinessButtonConfig` rather than looking up global state. Apply the same explicit table-list parameter in `BusinessBlockGenerationService`. Do not duplicate inference logic there. When copying actions, also copy the transient metadata if it is already present: ```java copy.setCurrentUserFields(source.getCurrentUserFields()); copy.setInputFields(source.getInputFields()); ``` - [ ] **Step 4: Run both tests and verify GREEN** Run: ```powershell mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest,BusinessBlockGenerationServiceTest test ``` Expected: both test classes pass. - [ ] **Step 5: Review the service diffs before staging** Run: ```powershell git diff -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java ``` Stage only metadata-enrichment hunks; do not absorb existing worktree changes. ### Task 3: Make Authenticated User Fields Server-Trusted **Files:** - Modify: `ruoyi-generator/src/main/resources/qing/controller.java.vm` - Modify: `ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java` - Modify: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java` - [ ] **Step 1: Write the failing controller-template assertions** Set `action.currentUserFields` to `["user_id"]` in the existing backend-action rendering test, then assert: ```java assertTrue(controller.contains( "prepareBusinessActionParams(params, request, new String[] {\"user_id\"})")); assertTrue(controller.contains( "actionParams.put(field, currentUser.getUserId())")); ``` Add a stale controller-template test using content that contains the old two-argument `prepareBusinessActionParams` but not `currentUserFields`. Assert preview fallback emits the new trusted-field loop. - [ ] **Step 2: Run the tests and verify RED** Run: ```powershell mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest,GenProjectServiceImplTest test ``` Expected: trusted-field assertions fail. - [ ] **Step 3: Pass action-specific trusted fields to the controller helper** Render each action endpoint call as: ```java Map actionParams = prepareBusinessActionParams( params, request, new String[] {#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end}); ``` Change the helper to: ```java private Map prepareBusinessActionParams(Map params, HttpServletRequest request, String[] currentUserFields) { Map actionParams = params == null ? new LinkedHashMap() : new LinkedHashMap(params); PortalUser currentUser = tokenStore.getPortalUser(resolveToken(request)); if (currentUser == null) { throw new IllegalArgumentException("请先登录"); } actionParams.put("__currentUserId", currentUser.getUserId()); if (currentUserFields != null) { for (String field : currentUserFields) { if (StringUtils.isNotBlank(field)) { actionParams.put(field, currentUser.getUserId()); } } } return actionParams; } ``` This order intentionally overwrites any client-supplied `user_id`. - [ ] **Step 4: Update stale-template detection** Require the new marker in `shouldUseBundledBackendControllerTemplate`: ```java || !templateContent.contains("currentUserFields") || !templateContent.contains("actionParams.put(field, currentUser.getUserId())") ``` - [ ] **Step 5: Run the tests and verify GREEN** Run: ```powershell mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest,GenProjectServiceImplTest test ``` Expected: controller rendering and stale-template fallback tests pass. ### Task 4: Replace Chained Prompts with One Dynamic Dialog **Files:** - Modify: `ruoyi-generator/src/main/resources/qing/index.vue.vm` - Modify: `ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java` - Modify: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java` - [ ] **Step 1: Replace the old prompt test with failing dialog expectations** Change `qingFrontendCollectsMissingBusinessActionFields` to assert: ```java assertTrue(page.contains("businessActionDialogVisible")); assertTrue(page.contains("businessActionInputFields")); assertTrue(page.contains("

{{ activeBusinessAction.confirmMessage }}

取消 {{ activeBusinessAction && activeBusinessAction.buttonLabel ? activeBusinessAction.buttonLabel : '确认执行' }} ``` - [ ] **Step 4: Serialize render metadata and add dialog state** For each `businessActionMap` item add: ```javascript buttonLabel: "${action.buttonLabel}", currentUserFields: [#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end], inputFields: [ #foreach($field in $action.inputFields) { field: "${field.field}", label: "${field.label}", htmlType: "${field.htmlType}", dictType: "${field.dictType}" }#if($foreach.hasNext),#end #end ] ``` Add component state: ```javascript businessActionDialogVisible: false, businessActionSubmitting: false, activeBusinessAction: null, businessActionBasePayload: {}, businessActionForm: {}, businessActionInputFields: [] ``` Add computed title: ```javascript businessActionDialogTitle() { return this.activeBusinessAction ? (this.activeBusinessAction.buttonLabel || this.activeBusinessAction.name || "业务操作") : "业务操作" } ``` - [ ] **Step 5: Replace the chained collection flow** Use one routing method: ```javascript handleBusinessAction(code, row) { if (this.handleConfiguredRoute("business:" + code, row) || this.handleConfiguredRoute("toolbar:" + code, row) || this.handleConfiguredRoute("table:" + code, row)) { return } const action = this.businessActionMap[code] if (!action || !action.path) { this.$message.warning("动作未配置") return } const payload = this.buildBusinessActionPayload(action, row) const missingFields = (action.inputFields || []).filter(field => !this.hasBusinessActionValue(payload[field.field])) if (missingFields.length) { this.openBusinessActionDialog(action, payload, missingFields) return } if (action.confirmMessage) { this.$confirm(action.confirmMessage, "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }).then(() => this.executeBusinessAction(action, payload)).catch(() => {}) return } this.executeBusinessAction(action, payload) } ``` Implement dialog methods: ```javascript openBusinessActionDialog(action, payload, fields) { this.activeBusinessAction = action this.businessActionBasePayload = Object.assign({}, payload) this.businessActionInputFields = fields this.businessActionForm = fields.reduce((form, field) => { form[field.field] = payload[field.field] === undefined ? "" : payload[field.field] return form }, {}) this.businessActionDialogVisible = true this.$nextTick(() => this.$refs.businessActionForm && this.$refs.businessActionForm.clearValidate()) }, submitBusinessAction() { this.$refs.businessActionForm.validate(valid => { if (!valid || !this.activeBusinessAction) { return } const payload = Object.assign({}, this.businessActionBasePayload, this.businessActionForm) this.businessActionSubmitting = true this.executeBusinessAction(this.activeBusinessAction, payload) .then(() => { this.businessActionDialogVisible = false }) .finally(() => { this.businessActionSubmitting = false }) }) }, resetBusinessActionDialog() { if (this.businessActionSubmitting) { return } this.activeBusinessAction = null this.businessActionBasePayload = {} this.businessActionForm = {} this.businessActionInputFields = [] }, hasBusinessActionValue(value) { return value !== undefined && value !== null && String(value).trim() !== "" }, getBusinessActionFieldOptions(field) { if (!field || !field.dictType) { return [] } return this._dict.getDictDataByType(field.dictType).map(item => ({ label: item.name, value: item.value })) } ``` `buildBusinessActionPayload` must iterate `action.inputFields` plus `requestFields`, but must skip `action.currentUserFields` so trusted identity is never read from local storage or sent by the client. - [ ] **Step 6: Keep dialog values on backend failure** Make `executeBusinessAction(action, payload)` return a Promise. Resolve only for `code === "200"` and reject with an `Error` for a business failure: ```javascript executeBusinessAction(action, payload) { const method = String(action.method || "POST").toLowerCase() const request = method === "get" ? this.request.get(action.path, { params: payload }) : this.request[method] ? this.request[method](action.path, payload) : this.request.post(action.path, payload) return request.then(res => { if (res.code !== "200") { throw new Error(res.msg || "操作失败") } this.$message.success(action.successMessage || "操作成功") this.refreshBusinessActionTarget(action.refreshTarget) return res }).catch(error => { const data = error && error.response ? error.response.data : null this.$message.error((data && data.msg) || (error && error.message) || "操作失败") return Promise.reject(error) }) } ``` The dialog closes only in `submitBusinessAction().then(...)`; failures therefore preserve entered values. - [ ] **Step 7: Update stale frontend-template detection** Replace the obsolete `collectBusinessActionPayload` marker with: ```java || !templateContent.contains("businessActionDialogVisible") || !templateContent.contains("submitBusinessAction") || !templateContent.contains("currentUserFields") || templateContent.contains("this.$prompt") ``` Update the stale-template test to assert the bundled output contains the dialog and does not contain `$prompt`. - [ ] **Step 8: Run template and fallback tests** Run: ```powershell mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest,GenProjectServiceImplTest test ``` Expected: all tests pass; generated frontend contains one dynamic dialog and no chained prompt. ### Task 5: Synchronize Runnable SQL Template Seeds **Files:** - Modify: `sql/qing_templates.sql` - Modify: `sql/db.sql` - Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java` - [ ] **Step 1: Run the seed consistency test and verify RED** Run: ```powershell mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest#qingDatabaseSeedsContainCurrentBusinessActionTemplates test ``` Expected: failure because bundled `controller.java.vm` and `index.vue.vm` no longer match the SQL copies. - [ ] **Step 2: Mechanically replace the two SQL records** For both SQL files, replace: - `controller.java.vm` with escaped content from `ruoyi-generator/src/main/resources/qing/controller.java.vm` - `frontend-index.vue.vm` with escaped content from `ruoyi-generator/src/main/resources/qing/index.vue.vm` Preserve the existing record IDs, template IDs, file names, file paths, author, and `sysdate()` suffix. Escape every single quote in template content as `''` and normalize line endings to LF before insertion. Do not hand-edit the embedded template bodies; use a deterministic bulk replacement because the test compares the complete escaped resource. - [ ] **Step 3: Run the exact seed test and verify GREEN** Run: ```powershell mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest#qingDatabaseSeedsContainCurrentBusinessActionTemplates test ``` Expected: PASS. - [ ] **Step 4: Run all targeted generator tests** Run: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessActionRenderSupportTest,QingTemplateSupportTest,GenProjectServiceImplTest,BusinessBlockGenerationServiceTest test ``` Expected: BUILD SUCCESS. ### Task 6: Build and Browser-Verify Generated Business Buttons **Files:** - No additional source files unless verification exposes a defect. - [ ] **Step 1: Run the full generator module** Run: ```powershell mvn -pl ruoyi-generator test ``` Expected: BUILD SUCCESS with no new test failures. - [ ] **Step 2: Build EasyCode** Run from `easycode-web`: ```powershell npm run build ``` Expected: Vite build completes successfully. - [ ] **Step 3: Regenerate/restart the current runnable preview** Use the existing run-preview flow for the project backing `http://127.0.0.1:57320`. Wait until the backend and frontend preview status reports ready, then reload `/books/1` in the in-app browser. - [ ] **Step 4: Verify the borrowing interaction** In the in-app browser: 1. Click `借书`. 2. Confirm there is at most one business dialog. 3. Confirm there is no `user id` field. 4. Confirm the current book identifier is not requested. 5. If `due_date` is unresolved, confirm it appears as a date picker in the same dialog. 6. Cancel and verify no request is sent. 7. Reopen, submit once, and verify exactly one business-action request. 8. Confirm success/error feedback comes from the configured or backend message. - [ ] **Step 5: Verify a second multi-parameter business action** Open another generated action such as reservation, return, approval, or rejection: - all unresolved editable fields appear together; - row/detail values are not requested again; - the authenticated user is not editable; - failure leaves the dialog and entered values available for retry; - success applies the configured `refreshTarget`. - [ ] **Step 6: Final diff and completion audit** Run: ```powershell git diff --check git status --short ``` Confirm: - no `$prompt` remains in the Qing frontend template or SQL seed copies; - no user-specific action names are hard-coded into runtime behavior; - controller injection overwrites trusted fields; - both generation paths enrich metadata; - resource templates and SQL seed copies match; - unrelated worktree changes remain untouched.