xsgz 请假字段填充改为「标签匹配 + 必填文本兜底」,降低对字段命名的依赖

从 transformFields-D3I9yUm6.js 反推出 form_fields_for_add 的真实结构:
{ fieldId, fieldName, componentType, fieldType, required, options[] }
前端 transformFields 把它转成 fieldEn=fieldId / fieldZh=fieldName 后提交,
且请假原因并非硬编码字段,而是学校后台动态配置的。

原先只按「原因/事由」关键词填值,若学校把字段命名为"申请理由说明"
"补充信息"等就会漏填,导致学校端校验失败。

改为两轮策略:
1. 优先按标签命中(原因/事由/说明);
2. 未命中时,用「必填的文本类字段」(TEXT/TEXTAREA/LONGTEXT)兜底填入请假原因。
   该校表单通常只有一个原因字段,兜底可覆盖自定义命名;
   选择类字段(RADIO/CHECKBOX 等)不猜测,留空由学校端校验。

新增 4 个单测覆盖:关键词命中、自定义命名、兜底逻辑、空输入不 panic。
This commit is contained in:
becomer
2026-10-17 17:45:12 +08:00
parent 07b0cfaa88
commit c8ff086b7b
2 changed files with 87 additions and 10 deletions
+31 -8
View File
@@ -635,23 +635,46 @@ func executeXsgzLeaveApply(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeaveA
return nil
}
// fillXsgzFieldValues 把用户填写的值写回 xsgz 表单字段定义的 value 字段。
// fillXsgzFieldValues 把用户填写的值写回 xsgz 表单字段定义。
//
// xsgz 提交的 fields 是「字段定义 + 当前值」的数组;本平台表单字段与 xsgz 字段
// 命名不同,这里按常见语义做映射:请假原因 → 含"原因/事由"的字段。
// 未能识别的字段保持原值(空),由学校端校验。
// 依据前端 transformFields 的字段结构:
//
// form_fields_for_add 返回 { fieldId, fieldName, componentType, fieldType, required, options[] }
//
// 提交时前端遍历字段,把用户输入按 fieldId 写回 value。本平台只有「请假原因」
// 一项自由文本,因此:
// 1. 优先按标签(fieldName 含"原因/事由/说明")精确匹配;
// 2. 未命中时,退化为「必填的文本类字段」(fieldType 为 TEXT/TEXTAREA/LONGTEXT)
// 一律填入请假原因——该校表单通常只有一个原因字段,这样可以避免因
// 学校自定义字段名(如"请假事由说明""申请理由")导致漏填。
//
// 选择类字段(RADIO/CHECKBOX/SELECT 等)不猜测,留空由学校端校验。
func fillXsgzFieldValues(fields []map[string]interface{}, req *ExecuteLeaveApplyRequest) {
if len(fields) == 0 {
if len(fields) == 0 || req.LeaveReason == "" {
return
}
textTypes := map[string]bool{"TEXT": true, "TEXTAREA": true, "LONGTEXT": true}
// 第一轮:按标签匹配
matched := false
for _, f := range fields {
label := strings.ToLower(mapStr(f, "fieldZh") + mapStr(f, "fieldName") + mapStr(f, "label"))
switch {
case strings.Contains(label, "原因") || strings.Contains(label, "事由"):
if req.LeaveReason != "" {
if strings.Contains(label, "原因") || strings.Contains(label, "事由") || strings.Contains(label, "说明") {
f["value"] = req.LeaveReason
matched = true
}
}
if matched {
return
}
// 第二轮:必填文本字段兜底
for _, f := range fields {
ft := strings.ToUpper(mapStr(f, "fieldType"))
required, _ := f["required"].(bool)
if required && textTypes[ft] {
f["value"] = req.LeaveReason
}
}
}
@@ -0,0 +1,54 @@
package handler
import "testing"
// fillXsgzFieldValues:优先按标签匹配,未命中时用必填文本字段兜底。
func TestFillXsgzFieldValues(t *testing.T) {
req := &ExecuteLeaveApplyRequest{LeaveReason: "感冒发烧需休息"}
t.Run("按标签命中原因字段", func(t *testing.T) {
fields := []map[string]interface{}{
{"fieldId": "f1", "fieldName": "请假原因", "fieldType": "TEXTAREA", "required": true},
{"fieldId": "f2", "fieldName": "紧急联系人", "fieldType": "TEXT", "required": true},
}
fillXsgzFieldValues(fields, req)
if fields[0]["value"] != req.LeaveReason {
t.Fatalf("原因字段未被填充: %v", fields[0])
}
if _, ok := fields[1]["value"]; ok {
t.Fatalf("非原因字段不应被填充: %v", fields[1])
}
})
t.Run("学校自定义字段名时用必填文本兜底", func(t *testing.T) {
fields := []map[string]interface{}{
{"fieldId": "f1", "fieldName": "申请理由说明", "fieldType": "TEXTAREA", "required": true},
{"fieldId": "f2", "fieldName": "备注", "fieldType": "TEXT", "required": false},
}
fillXsgzFieldValues(fields, req)
// "说明" 命中第一轮
if fields[0]["value"] != req.LeaveReason {
t.Fatalf("含'说明'的字段应被填充: %v", fields[0])
}
})
t.Run("无关键词时必填文本字段兜底", func(t *testing.T) {
fields := []map[string]interface{}{
{"fieldId": "f1", "fieldName": "补充信息", "fieldType": "TEXTAREA", "required": true},
{"fieldId": "f2", "fieldName": "选择项", "fieldType": "RADIO", "required": true},
}
fillXsgzFieldValues(fields, req)
if fields[0]["value"] != req.LeaveReason {
t.Fatalf("必填文本字段应兜底填充: %v", fields[0])
}
if _, ok := fields[1]["value"]; ok {
t.Fatalf("选择类字段不应猜测填充: %v", fields[1])
}
})
t.Run("空 fields 与空原因不 panic", func(t *testing.T) {
fillXsgzFieldValues(nil, req)
fillXsgzFieldValues([]map[string]interface{}{}, req)
fillXsgzFieldValues([]map[string]interface{}{{"fieldId": "f1"}}, &ExecuteLeaveApplyRequest{})
})
}