28 KiB
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
currentUserFieldsandinputFields.
- Stores transient
- 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
$promptcalls with one reusable dynamic dialog.
- Replaces chained
- 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.
@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:
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:
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:
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:
- Uses a
LinkedHashSetto preserve request-field order. - Marks map keys whose effect value is exactly
${current_user.id}. - Marks
targetFieldwhen an effectvalue, ruleexpectedValue, rulecompareValue, or any ruleexpectedValuesentry is${current_user.id}. - Builds
inputFieldsfromrequestFields - currentUserFields. - Resolves schema metadata by exact
columnNameorjavaField, preferring the action owner table and then other project tables. - Falls back to a humanized field name and
inputcontrol when no schema column matches.
Core methods:
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:
mvn -pl ruoyi-generator -Dtest=BusinessActionRenderSupportTest test
Expected: all metadata tests pass.
- Step 6: Commit the isolated new files if staging is clean
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:
action.setRequestFields(Arrays.asList("book_id", "user_id", "due_date"));
effectValues.put("user_id", "${current_user.id}");
Render frontend-index.vue.vm and assert:
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:
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:
for (BusinessActionDesign action : tableBusinessActions) {
BusinessActionRenderSupport.enrich(action, project.getTables());
}
In applyBusinessButtonConfig, enrich the copied action after applying visual configuration:
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:
copy.setCurrentUserFields(source.getCurrentUserFields());
copy.setInputFields(source.getInputFields());
- Step 4: Run both tests and verify GREEN
Run:
mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest,BusinessBlockGenerationServiceTest test
Expected: both test classes pass.
- Step 5: Review the service diffs before staging
Run:
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:
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:
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:
Map<String, Object> actionParams = prepareBusinessActionParams(
params,
request,
new String[] {#foreach($field in $action.currentUserFields)"${field}"#if($foreach.hasNext), #end#end});
Change the helper to:
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:
|| !templateContent.contains("currentUserFields")
|| !templateContent.contains("actionParams.put(field, currentUser.getUserId())")
- Step 5: Run the tests and verify GREEN
Run:
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:
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:
currentUserFields: ["user_id"],
inputFields: [
{ field: "due_date", label: "应还日期", htmlType: "datetime", dictType: "" }
]
- Step 2: Run the template test and verify RED
Run:
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:
<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:
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:
businessActionDialogVisible: false,
businessActionSubmitting: false,
activeBusinessAction: null,
businessActionBasePayload: {},
businessActionForm: {},
businessActionInputFields: []
Add computed title:
businessActionDialogTitle() {
return this.activeBusinessAction
? (this.activeBusinessAction.buttonLabel || this.activeBusinessAction.name || "业务操作")
: "业务操作"
}
- Step 5: Replace the chained collection flow
Use one routing method:
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:
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:
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:
|| !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:
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:
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.vmwith escaped content fromruoyi-generator/src/main/resources/qing/controller.java.vmfrontend-index.vue.vmwith escaped content fromruoyi-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:
mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest#qingDatabaseSeedsContainCurrentBusinessActionTemplates test
Expected: PASS.
- Step 4: Run all targeted generator tests
Run:
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:
mvn -pl ruoyi-generator test
Expected: BUILD SUCCESS with no new test failures.
- Step 2: Build EasyCode
Run from easycode-web:
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:
- Click
借书. - Confirm there is at most one business dialog.
- Confirm there is no
user idfield. - Confirm the current book identifier is not requested.
- If
due_dateis unresolved, confirm it appears as a date picker in the same dialog. - Cancel and verify no request is sent.
- Reopen, submit once, and verify exactly one business-action request.
- 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:
git diff --check
git status --short
Confirm:
- no
$promptremains 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.