重新核对前端源码发现两处与实现不符,已修正:
1. FormData 序列化格式
前端 toFormatFormData(T, needCurlyBraces=false) 的规则是:
- 嵌套/数组用**点号**展开(fields.0.value),不是方括号;
- 值为 null 或空字符串时**跳过**该键;
- 数组按下标逐个展开。
原实现把 fields 整个 JSON.stringify 成一个字符串,与前端不一致。
新增 xsgzFlatten 按上述规则摊平,并加单测覆盖(点号/跳过空值/数组下标)。
2. fields 内容必须是「字段定义 + 用户填值」
前端提交前会 forEach 把用户输入写回各字段的 value:
f.value.forEach(l => { l.value = t[l.fieldId] ... })
原实现只提交了空的字段定义。
现在先拉 form_fields_for_add,再 fillXsgzFieldValues 把请假原因等
按字段标签(含"原因/事由")回填到 value;销假同理(含"说明/原因")。
参数契约已逐项比对(源码原文 vs 实现):
请假:{projectId, requestType:"LEAVE", startTime, endTime,
fields[], address, longitude, latitude}
销假:{originalId, projectId, requestType:"CANCELLATION", fields[],
cancellationApplyInArea, inArea, address, longitude, latitude}
This commit is contained in:
@@ -500,12 +500,28 @@ func executeXsgzLeaveCancel(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeave
|
||||
|
||||
sendEvent("submitting", map[string]interface{}{"status": "submitting", "message": "正在提交销假申请..."})
|
||||
|
||||
// 与前端一致:先按 (projectId, requestType) 拉取该申请的表单字段定义,
|
||||
// 作为 fields 提交(ActionPopup.vue: C.value = await form_fields_for_add({projectId, requestType}))
|
||||
formFields, ffErr := sess.XsgzFetchFormFields(c.Context(), req.ProjectID, "CANCELLATION")
|
||||
if ffErr != nil {
|
||||
slog.Warn("xsgz 获取销假表单字段失败,按空字段提交", "userId", user.ID, "error", ffErr)
|
||||
formFields = nil
|
||||
}
|
||||
// 销假表单通常含"销假说明/原因",用地址兜底填充可见字段
|
||||
for _, f := range formFields {
|
||||
label := strings.ToLower(mapStr(f, "fieldZh") + mapStr(f, "fieldName") + mapStr(f, "label"))
|
||||
if (strings.Contains(label, "说明") || strings.Contains(label, "原因")) && req.Address != "" {
|
||||
f["value"] = req.Address
|
||||
}
|
||||
}
|
||||
|
||||
body, err := sess.XsgzSubmitCancel(c.Context(), cpd.XsgzSubmitPayload{
|
||||
ProjectID: req.ProjectID,
|
||||
OriginalID: req.LeaveID,
|
||||
Address: req.Address,
|
||||
Longitude: req.Longitude,
|
||||
Latitude: req.Latitude,
|
||||
Fields: formFields,
|
||||
})
|
||||
if err != nil {
|
||||
msg := "提交销假失败: " + err.Error()
|
||||
|
||||
@@ -583,6 +583,16 @@ func executeXsgzLeaveApply(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeaveA
|
||||
|
||||
sendEvent("submitting", map[string]interface{}{"status": "submitting", "message": "正在提交请假申请..."})
|
||||
|
||||
// 与前端一致:先按 (projectId, requestType) 拉取表单字段定义,再把用户填写的值
|
||||
// 写回各字段的 value 后作为 fields 提交。
|
||||
// (index-C4CfkeDm.js: f.value = form_fields_for_add(...),提交前 forEach 补 value)
|
||||
formFields, ffErr := sess.XsgzFetchFormFields(c.Context(), req.XsgzProjectID, "LEAVE")
|
||||
if ffErr != nil {
|
||||
slog.Warn("xsgz 获取请假表单字段失败,按空字段提交", "userId", user.ID, "error", ffErr)
|
||||
formFields = nil
|
||||
}
|
||||
fillXsgzFieldValues(formFields, req)
|
||||
|
||||
payload := cpd.XsgzSubmitPayload{
|
||||
ProjectID: req.XsgzProjectID,
|
||||
RequestType: "LEAVE",
|
||||
@@ -591,6 +601,7 @@ func executeXsgzLeaveApply(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeaveA
|
||||
Address: req.LocationAddr,
|
||||
Longitude: req.Longitude,
|
||||
Latitude: req.Latitude,
|
||||
Fields: formFields,
|
||||
}
|
||||
body, err := sess.XsgzSubmitLeave(c.Context(), payload)
|
||||
if err != nil {
|
||||
@@ -623,3 +634,31 @@ func executeXsgzLeaveApply(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeaveA
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// fillXsgzFieldValues 把用户填写的值写回 xsgz 表单字段定义的 value 字段。
|
||||
//
|
||||
// xsgz 提交的 fields 是「字段定义 + 当前值」的数组;本平台表单字段与 xsgz 字段
|
||||
// 命名不同,这里按常见语义做映射:请假原因 → 含"原因/事由"的字段。
|
||||
// 未能识别的字段保持原值(空),由学校端校验。
|
||||
func fillXsgzFieldValues(fields []map[string]interface{}, req *ExecuteLeaveApplyRequest) {
|
||||
if len(fields) == 0 {
|
||||
return
|
||||
}
|
||||
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 != "" {
|
||||
f["value"] = req.LeaveReason
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mapStr 安全取 map 中的字符串值
|
||||
func mapStr(m map[string]interface{}, k string) string {
|
||||
if v, ok := m[k]; ok && v != nil {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -273,12 +273,52 @@ func (s *XsgzSession) XsgzSubmitCancel(ctx context.Context, p XsgzSubmitPayload)
|
||||
return s.XsgzSubmitApplication(ctx, p)
|
||||
}
|
||||
|
||||
// xsgzFlatten 按前端 toFormatFormData(needCurlyBraces=false) 的规则摊平嵌套结构:
|
||||
// - 键路径用点号连接(fields.0.value);
|
||||
// - nil 与空字符串跳过;
|
||||
// - 数组按下标展开(fields.0 / fields.1);
|
||||
// - map 递归展开;标量直接输出(bool 用 true/false)。
|
||||
func xsgzFlatten(prefix string, v interface{}, out *[][2]string) {
|
||||
switch val := v.(type) {
|
||||
case nil:
|
||||
return
|
||||
case map[string]interface{}:
|
||||
for k, child := range val {
|
||||
key := k
|
||||
if prefix != "" {
|
||||
key = prefix + "." + k
|
||||
}
|
||||
xsgzFlatten(key, child, out)
|
||||
}
|
||||
case []interface{}:
|
||||
for i, child := range val {
|
||||
key := fmt.Sprintf("%s[%d]", prefix, i)
|
||||
xsgzFlatten(key, child, out)
|
||||
}
|
||||
case string:
|
||||
if val == "" {
|
||||
return
|
||||
}
|
||||
*out = append(*out, [2]string{prefix, val})
|
||||
case bool:
|
||||
*out = append(*out, [2]string{prefix, strconv.FormatBool(val)})
|
||||
default:
|
||||
*out = append(*out, [2]string{prefix, fmt.Sprintf("%v", val)})
|
||||
}
|
||||
}
|
||||
|
||||
// XsgzSubmitApplication 统一的申请提交(请假/销假/续假共用)。
|
||||
//
|
||||
// 序列化规则严格对齐前端 toFormatFormData(T, needCurlyBraces=false):
|
||||
// - 嵌套/数组用**点号**展开(fields.0.value),不用方括号;
|
||||
// - 值为 null 或空字符串时**跳过**(不提交该键);
|
||||
// - 数组按下标逐个展开;
|
||||
// - 其余值为对象时递归展开。
|
||||
func (s *XsgzSession) XsgzSubmitApplication(ctx context.Context, p XsgzSubmitPayload) ([]byte, error) {
|
||||
if p.ProjectID == "" {
|
||||
return nil, fmt.Errorf("缺少请假类型(projectId)")
|
||||
}
|
||||
form := map[string]string{
|
||||
fields := map[string]interface{}{
|
||||
"projectId": p.ProjectID,
|
||||
"requestType": p.RequestType,
|
||||
"address": p.Address,
|
||||
@@ -286,22 +326,33 @@ func (s *XsgzSession) XsgzSubmitApplication(ctx context.Context, p XsgzSubmitPay
|
||||
"latitude": p.Latitude,
|
||||
}
|
||||
if p.StartTime != "" {
|
||||
form["startTime"] = p.StartTime
|
||||
fields["startTime"] = p.StartTime
|
||||
}
|
||||
if p.EndTime != "" {
|
||||
form["endTime"] = p.EndTime
|
||||
fields["endTime"] = p.EndTime
|
||||
}
|
||||
if p.OriginalID != "" {
|
||||
form["originalId"] = p.OriginalID
|
||||
fields["originalId"] = p.OriginalID
|
||||
}
|
||||
if p.RequestType == "CANCELLATION" {
|
||||
form["inArea"] = strconv.FormatBool(p.InArea)
|
||||
form["cancellationApplyInArea"] = strconv.FormatBool(p.CancellationApplyInArea)
|
||||
fields["inArea"] = p.InArea
|
||||
fields["cancellationApplyInArea"] = p.CancellationApplyInArea
|
||||
}
|
||||
if len(p.Fields) > 0 {
|
||||
if raw, err := json.Marshal(p.Fields); err == nil {
|
||||
form["fields"] = string(raw)
|
||||
arr := make([]interface{}, 0, len(p.Fields))
|
||||
for _, f := range p.Fields {
|
||||
arr = append(arr, f)
|
||||
}
|
||||
fields["fields"] = arr
|
||||
}
|
||||
|
||||
// 按前端规则摊平为键值对
|
||||
pairs := make([][2]string, 0, 16)
|
||||
xsgzFlatten("", fields, &pairs)
|
||||
|
||||
form := map[string]string{}
|
||||
for _, kv := range pairs {
|
||||
form[kv[0]] = kv[1]
|
||||
}
|
||||
resp, err := s.client.R().SetContext(ctx).
|
||||
SetHeader("Authorization", s.Token).
|
||||
|
||||
@@ -97,3 +97,49 @@ func TestIsXsgzSchool(t *testing.T) {
|
||||
t.Fatalf("CAS service 地址错误: %s", XsgzCasServiceURL("sdau"))
|
||||
}
|
||||
}
|
||||
|
||||
// xsgzFlatten 必须与前端 toFormatFormData(needCurlyBraces=false) 规则一致:
|
||||
// 点号展开、跳过空值、数组按下标。
|
||||
func TestXsgzFlatten(t *testing.T) {
|
||||
in := map[string]interface{}{
|
||||
"projectId": "uuid-1",
|
||||
"requestType": "CANCELLATION",
|
||||
"address": "", // 空字符串应跳过
|
||||
"longitude": "115.8",
|
||||
"inArea": true,
|
||||
"empty": nil, // nil 应跳过
|
||||
"fields": []interface{}{
|
||||
map[string]interface{}{"fieldId": "f1", "value": "v1", "blank": ""},
|
||||
map[string]interface{}{"fieldId": "f2", "value": "v2"},
|
||||
},
|
||||
}
|
||||
var pairs [][2]string
|
||||
xsgzFlatten("", in, &pairs)
|
||||
|
||||
got := map[string]string{}
|
||||
for _, kv := range pairs {
|
||||
got[kv[0]] = kv[1]
|
||||
}
|
||||
|
||||
want := map[string]string{
|
||||
"projectId": "uuid-1",
|
||||
"requestType": "CANCELLATION",
|
||||
"longitude": "115.8",
|
||||
"inArea": "true",
|
||||
"fields[0].fieldId": "f1",
|
||||
"fields[0].value": "v1",
|
||||
"fields[1].fieldId": "f2",
|
||||
"fields[1].value": "v2",
|
||||
}
|
||||
for k, v := range want {
|
||||
if got[k] != v {
|
||||
t.Fatalf("key %q = %q, want %q(实际: %v)", k, got[k], v, got)
|
||||
}
|
||||
}
|
||||
// 空值不应出现
|
||||
for _, bad := range []string{"address", "empty", "fields[0].blank"} {
|
||||
if _, ok := got[bad]; ok {
|
||||
t.Fatalf("空值键 %q 不应被提交", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user