Plan unified Qing business action dialog
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user