Plan unified Qing business action dialog

This commit is contained in:
王鹏
2026-06-18 17:56:46 +08:00
parent 60fca3a102
commit e01938fcdc

View File

@@ -0,0 +1,772 @@
# 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<String, String> values = new LinkedHashMap<String, String>();
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<String> currentUserFields;
private transient List<BusinessActionInputFieldDesign> inputFields;
public List<String> getCurrentUserFields() { return currentUserFields; }
public void setCurrentUserFields(List<String> currentUserFields) { this.currentUserFields = currentUserFields; }
public List<BusinessActionInputFieldDesign> getInputFields() { return inputFields; }
public void setInputFields(List<BusinessActionInputFieldDesign> 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<GenTable> tables)
{
if (action == null) {
return;
}
LinkedHashSet<String> trusted = collectCurrentUserFields(action);
action.setCurrentUserFields(new ArrayList<String>(trusted));
List<BusinessActionInputFieldDesign> inputs = new ArrayList<BusinessActionInputFieldDesign>();
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<String, Object> actionParams = prepareBusinessActionParams(
params,
request,
new String[] {#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end});
```
Change the helper to:
```java
private Map<String, Object> prepareBusinessActionParams(Map<String, Object> params,
HttpServletRequest request,
String[] currentUserFields)
{
Map<String, Object> actionParams = params == null
? new LinkedHashMap<String, Object>()
: new LinkedHashMap<String, Object>(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("<el-dialog"));
assertTrue(page.contains("submitBusinessAction"));
assertTrue(page.contains("openBusinessActionDialog"));
assertTrue(page.contains("currentUserFields"));
assertTrue(page.contains("inputFields"));
assertTrue(page.contains("v-for=\"field in businessActionInputFields\""));
assertTrue(page.contains("field.htmlType === 'datetime'"));
assertTrue(page.contains("field.htmlType === 'textarea'"));
assertTrue(page.contains("getBusinessActionFieldOptions(field)"));
assertFalse(page.contains("this.$prompt"));
assertFalse(page.contains("collectBusinessActionPayload"));
```
Also assert the generated action map serializes:
```javascript
currentUserFields: ["user_id"],
inputFields: [
{ field: "due_date", label: "应还日期", htmlType: "datetime", dictType: "" }
]
```
- [ ] **Step 2: Run the template test and verify RED**
Run:
```powershell
mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest test
```
Expected: the new dialog markers are absent and `$prompt` is still present.
- [ ] **Step 3: Add one reusable Element UI dialog**
Place one dialog next to the existing CRUD dialog:
```vue
<el-dialog
:title="businessActionDialogTitle"
:visible.sync="businessActionDialogVisible"
width="520px"
append-to-body
:close-on-click-modal="false"
@closed="resetBusinessActionDialog">
<p v-if="activeBusinessAction && activeBusinessAction.confirmMessage"
class="business-action-hint">
{{ activeBusinessAction.confirmMessage }}
</p>
<el-form ref="businessActionForm" :model="businessActionForm" label-width="110px">
<el-form-item
v-for="field in businessActionInputFields"
:key="field.field"
:label="field.label"
:prop="field.field"
:rules="[{ required: true, message: field.label + '不能为空', trigger: field.htmlType === 'select' ? 'change' : 'blur' }]">
<el-date-picker
v-if="field.htmlType === 'datetime'"
v-model="businessActionForm[field.field]"
type="date"
value-format="yyyy-MM-dd"
:placeholder="'请选择' + field.label" />
<el-select
v-else-if="field.htmlType === 'select' && field.dictType"
v-model="businessActionForm[field.field]"
clearable
:placeholder="'请选择' + field.label">
<el-option
v-for="option in getBusinessActionFieldOptions(field)"
:key="option.value"
:label="option.label"
:value="option.value" />
</el-select>
<el-input
v-else-if="field.htmlType === 'textarea'"
v-model="businessActionForm[field.field]"
type="textarea"
:rows="3"
:placeholder="'请输入' + field.label" />
<el-input
v-else
v-model="businessActionForm[field.field]"
:placeholder="'请输入' + field.label" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="businessActionDialogVisible = false" :disabled="businessActionSubmitting">取消</el-button>
<el-button type="primary" :loading="businessActionSubmitting" @click="submitBusinessAction">
{{ activeBusinessAction && activeBusinessAction.buttonLabel ? activeBusinessAction.buttonLabel : '确认执行' }}
</el-button>
</span>
</el-dialog>
```
- [ ] **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.