OA 系統核心本質:表單定義 → 數據存儲 → 流程審批 → 統計查詢。傳統開發模式下,每個新模組 (人事檔案、請假單、採購單) 都要寫 Controller、Service、DAO、JSP、JS、SQL,6 成時間耗在 CRUD 樣板碼。本文闡述基於 XML 元數據 + Java 動態執行 的表單引擎架構,實現「配置代替編碼」,由實施顧問拖拉拽即可上線新模組。
┌─────────────────────────────────────┐
│ 視圖層: 動態表單渲染器 (Vue/React) │ ← 解析 FormSchema JSON 生成 UI
├─────────────────────────────────────┤
│ 引擎層: FormEngine (核心業務邏輯) │ ← 生命週期管理、數據綁定、校驗、流程編排
├─────────────────────────────────────┤
│ 元數據層: XML/JSON Schema 存儲 │ ← 版本化、可視化設計器產出
├─────────────────────────────────────┤
│ 數據層: 動態表結構管理 (DDL 執行器) │ ← 運行時建表/加欄位/建索引
└─────────────────────────────────────┘
<form id="HR_EMPLOYEE" name="員工檔案" version="3" table="t_hr_employee">
<!-- 1. 基礎屬性 -->
<permissions type="RBAC" roles="HR_ADMIN,HR_USER"/>
<attachments enabled="true" maxSize="10MB"/>
<editor type="richtext" field="remark"/>
<workflow processKey="employee_onboard"/>
<!-- 2. 字段定義 (拖拉拽生成) -->
<fields>
<field id="emp_no" name="工號" type="STRING" length="20"
required="true" unique="true"
ui="input" placeholder="自動生成或手工輸入"/>
<field id="name" name="姓名" type="STRING" length="50" required="true" ui="input"/>
<field id="gender" name="性別" type="ENUM" enumValues="M:男,F:女" ui="radio"/>
<field id="entry_date" name="入職日期" type="DATE" ui="datepicker"/>
<field id="salary" name="薪資" type="DECIMAL" precision="10" scale="2"
permission="SENSITIVE" ui="number"/> <!-- 權限控制到字段 -->
<field id="resume" name="簡歷附件" type="FILE" ui="uploader"/>
</fields>
<!-- 3. 查詢/統計配置 -->
<queries>
<query id="dept_stat" name="部門人數統計"
sql="SELECT dept_id, COUNT(*) FROM t_hr_employee GROUP BY dept_id"/>
<query id="salary_export" name="薪資導出" export="excel"
fields="emp_no,name,salary" permission="FINANCE"/>
</queries>
<!-- 4. 高級擴展 (給資深用戶) -->
<advanced>
<customTable name="t_hr_employee_ext" /> <!-- 指定物理表名 -->
<customColumn field="emp_no" column="EMP_NO" type="VARCHAR2(20)"/>
</advanced>
</form>
@Component
public class FormSchemaRepository {
private final Map<String, FormSchema> cache = new ConcurrentHashMap<>();
private final JAXBContext jaxbContext;
public FormSchema getSchema(String formId) {
return cache.computeIfAbsent(formId, this::loadFromXml);
}
private FormSchema loadFromXml(String formId) {
// 從資料庫/文件系統讀取 XML,JAXB 反序列化為 FormSchema POJO
// 校驗 XSD、建立字段索引 Map、預編譯校驗規則/腳本
}
}
@Service
@Transactional
public class DynamicSchemaService {
@Autowired private JdbcTemplate jdbcTemplate;
@Autowired private DatabaseMetaData metaData;
public void syncTable(FormSchema schema) {
String table = schema.getTable();
if (!tableExists(table)) {
createTable(schema);
} else {
alterTableAddColumns(schema); // 僅新增欄位,不刪改歷史數據
}
createIndexes(schema); // 唯一約束、查詢索引
}
private void createTable(FormSchema schema) {
StringBuilder ddl = new StringBuilder("CREATE TABLE ").append(schema.getTable()).append(" (");
ddl.append("id BIGINT PRIMARY KEY AUTO_INCREMENT, ");
ddl.append("created_at DATETIME DEFAULT CURRENT_TIMESTAMP, ");
ddl.append("updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, ");
for (FieldDef f : schema.getFields()) {
ddl.append(buildColumnDef(f)).append(", ");
}
ddl.setLength(ddl.length() - 2); // 移除最後逗號
ddl.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
jdbcTemplate.execute(ddl.toString());
}
private String buildColumnDef(FieldDef f) {
// 根據 type/length/precision/required 映射 SQL 類型
// STRING -> VARCHAR(n), DECIMAL -> DECIMAL(p,s), DATE -> DATETIME, ENUM -> VARCHAR(20) + CHECK
}
}
安全警示:生產環境嚴禁自動 DROP COLUMN / ALTER COLUMN TYPE,僅允許 ADD COLUMN / ADD INDEX。結構變更需走變更單、備份、灰度驗證。
@Repository
public class DynamicFormDao {
@Autowired private JdbcTemplate jdbcTemplate;
@Autowired private FormSchemaRepository schemaRepo;
public Long insert(String formId, Map<String, Object> data) {
FormSchema schema = schemaRepo.getSchema(formId);
// 1. 數據校驗 (必填、類型、長度、唯一、腳本)
// 2. 構建動態 SQL
String sql = buildInsertSql(schema, data.keySet());
// 3. 執行並返回生成主鍵
KeyHolder kh = new GeneratedKeyHolder();
jdbcTemplate.update(con -> {
PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
int i = 1;
for (String field : data.keySet()) ps.setObject(i++, convertValue(data.get(field), schema.getField(field)));
return ps;
}, kh);
return kh.getKey().longValue();
}
public Page<Map<String, Object>> query(String formId, QueryRequest req) {
// 動態構建 WHERE/ORDER BY/LIMIT,支援多表 JOIN (關聯配置)
// 結果自動映射為 Map<fieldId, value>,前端直接渲染
}
}
<workflow processKey="..."/>。status=APPROVED)。| 維度 | 傳統開發 | 表單引擎模式 |
|---|---|---|
| 新模組上線時間 | 3-10 天 (開發+測試+部署) | 0.5-2 天 (配置+驗證) |
| 代碼量 | 每模組 500-2000 行 | 引擎核心 3000 行,模組 0 行代碼 |
| 維護成本 | 分散在各模組,改動風險大 | 集中在引擎核心,統一升級修復 |
| 靈活性 | 完全可控,可寫任意複雜邏輯 | 受限於引擎能力邊界,複雜非標邏輯需擴展點/腳本 |
| 適用場景 | 核心業務、高度定制、高效能 | 標準化表單、行政審批、數據采集、報表查詢、低代碼平台 |
架構師建議:核心交易/計費/庫存等高併發強一致性業務,堅持手寫代碼;行政、人事、財務報銷、資產管理、客戶資料等標準化表單業務,果斷上引擎。引擎核心團隊 2-3 人支撐全公司 50+ 表單模組,ROI 極高。