回退 f1f79df:抓取线上前端源码证实数组下标确为方括号 fields[0]
上一版 f1f79df 把 xsgzFlatten 的数组下标从 fields[0] 改成 fields.0,
依据是函数注释/提交说明/「needCurlyBraces 语义」三处都写点号。
本次从 sdau 线上静态资源(无需登录)拿到真实前端源码,证明该判断错误。
证据(xsgz.sdau.edu.cn/assets/,免登录可取):
index-B6fhXjS6.js 10.7MB 主包,含 1354 个 chunk 清单
studentForm-Dx5Fpjfs.js 序列化与提交实现
LeaveApplyDrawer-BeaVSGRQ.js 请假提交调用点
前端真实链路(LeaveApplyDrawer 的 P()):
c = ee(表单值, 字段定义)
L = ae(c, 字段定义) // fields 数组
T = {projectId, requestType:"LEAVE", startTime, endTime,
fields:L, address, longitude, latitude}
f = le(T, ext) // le → Ne → v → pe,转 FormData
await te(f) // POST application_submit (multipart)
键名规则(studentForm-Dx5Fpjfs.js 原文):
function pe(e,t,a){return e? a? `${e}[${t}]` : `${e}.${t}` : t}
v(): if(Array.isArray(s)) s.forEach((l,p)=>{
const c=`${i}[${p}]`; // 数组下标永远方括号,与 a 无关
... v(l,r,c,o)
})
Ne(e, t={}, a=false) → v(..., a=false)
即:对象键用点号,数组下标硬编码方括号。实际提交形如
fields[0].fieldId / fields[0].value。原实现正确,f1f79df 改错了。
另外查明:注释里长期引用的 toFormatFormData / needCurlyBraces
在整个 bundle 中并不存在(grep 计数为 0),是早期臆造的符号,
现已在注释中改为真实函数名 v()/pe()/Ne() 并附调用链。
改动:
- xsgzFlatten 数组分支还原为 "%s[%d]",注释给出源码依据;
- XsgzSubmitApplication 注释改为真实前端链路与规则;
- TestXsgzFlatten 期望还原为 fields[0].*;
- 契约测试期望同步为 fields[0].*,并加断言:括号需配对、
不得出现 fields[0][fieldId] 这类二维下标形态。
This commit is contained in:
@@ -273,11 +273,19 @@ func (s *XsgzSession) XsgzSubmitCancel(ctx context.Context, p XsgzSubmitPayload)
|
|||||||
return s.XsgzSubmitApplication(ctx, p)
|
return s.XsgzSubmitApplication(ctx, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// xsgzFlatten 按前端 toFormatFormData(needCurlyBraces=false) 的规则摊平嵌套结构:
|
// xsgzFlatten 复刻前端 studentForm-Dx5Fpjfs.js 的 FormData 序列化规则:
|
||||||
// - 键路径用点号连接(fields.0.value);
|
//
|
||||||
// - nil 与空字符串跳过;
|
// function pe(e,t,a){return e?a?`${e}[${t}]`:`${e}.${t}`:t}
|
||||||
// - 数组按下标展开(fields.0 / fields.1);
|
// function v(e,t,a,o=false){...}
|
||||||
// - map 递归展开;标量直接输出(bool 用 true/false)。
|
//
|
||||||
|
// 规则:
|
||||||
|
// - 对象键用点号连接(address / fields[0].fieldId);
|
||||||
|
// - 数组下标固定用方括号(fields[0]、fields[1]),与参数 o 无关;
|
||||||
|
// - undefined / null 与空字符串跳过;
|
||||||
|
// - map 递归展开;标量直接输出(bool 用 true/false,Date 转 ISO)。
|
||||||
|
//
|
||||||
|
// 注:早期注释写的 toFormatFormData(needCurlyBraces=false) 是臆造的符号,
|
||||||
|
// 该 bundle 中并不存在(已由线上源码核实)。
|
||||||
func xsgzFlatten(prefix string, v interface{}, out *[][2]string) {
|
func xsgzFlatten(prefix string, v interface{}, out *[][2]string) {
|
||||||
switch val := v.(type) {
|
switch val := v.(type) {
|
||||||
case nil:
|
case nil:
|
||||||
@@ -291,12 +299,15 @@ func xsgzFlatten(prefix string, v interface{}, out *[][2]string) {
|
|||||||
xsgzFlatten(key, child, out)
|
xsgzFlatten(key, child, out)
|
||||||
}
|
}
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
// 下标也用点号连接(fields.0 / fields.1),与前端
|
// 数组下标用方括号,对象键用点号——这是前端源码的硬编码行为:
|
||||||
// toFormatFormData(needCurlyBraces=false) 一致。
|
// studentForm-Dx5Fpjfs.js:
|
||||||
// 注意:此处曾误用方括号 "%s[%d]",与同函数注释及提交说明矛盾,
|
// function pe(e,t,a){return e?a?`${e}[${t}]`:`${e}.${t}`:t}
|
||||||
// 会让学校端收到 fields[0].value 而取不到 fields.0.value。
|
// v(): s.forEach((l,p)=>{const c=`${i}[${p}]`; ... })
|
||||||
|
// 调用方 LeaveApplyDrawer: le(T, D.value) → Ne(e,t={},a=false) → v(...,a=false)
|
||||||
|
// 即使 a=false(对象键走点号),数组下标依然固定为 [n]。
|
||||||
|
// 实际提交形如:fields[0].fieldId / fields[0].value / fields[1].value
|
||||||
for i, child := range val {
|
for i, child := range val {
|
||||||
key := fmt.Sprintf("%s.%d", prefix, i)
|
key := fmt.Sprintf("%s[%d]", prefix, i)
|
||||||
xsgzFlatten(key, child, out)
|
xsgzFlatten(key, child, out)
|
||||||
}
|
}
|
||||||
case string:
|
case string:
|
||||||
@@ -313,11 +324,17 @@ func xsgzFlatten(prefix string, v interface{}, out *[][2]string) {
|
|||||||
|
|
||||||
// XsgzSubmitApplication 统一的申请提交(请假/销假/续假共用)。
|
// XsgzSubmitApplication 统一的申请提交(请假/销假/续假共用)。
|
||||||
//
|
//
|
||||||
// 序列化规则严格对齐前端 toFormatFormData(T, needCurlyBraces=false):
|
// 序列化规则对齐前端 studentForm-Dx5Fpjfs.js 的 v()/pe():
|
||||||
// - 嵌套/数组用**点号**展开(fields.0.value),不用方括号;
|
// - 对象键用点号展开(address、fields[0].fieldId);
|
||||||
// - 值为 null 或空字符串时**跳过**(不提交该键);
|
// - 数组下标用方括号(fields[0].value),见 xsgzFlatten 注释;
|
||||||
// - 数组按下标逐个展开;
|
// - 值为 null 或空字符串时跳过(不提交该键);
|
||||||
// - 其余值为对象时递归展开。
|
// - 其余对象递归展开。
|
||||||
|
//
|
||||||
|
// 前端对应链路:LeaveApplyDrawer 的 P()
|
||||||
|
//
|
||||||
|
// T = {projectId, requestType, startTime, endTime, fields, address, longitude, latitude}
|
||||||
|
// f = Ne(T, ext) → v(...) → FormData
|
||||||
|
// POST /leave/student_action/application_submit (multipart)
|
||||||
func (s *XsgzSession) XsgzSubmitApplication(ctx context.Context, p XsgzSubmitPayload) ([]byte, error) {
|
func (s *XsgzSession) XsgzSubmitApplication(ctx context.Context, p XsgzSubmitPayload) ([]byte, error) {
|
||||||
if p.ProjectID == "" {
|
if p.ProjectID == "" {
|
||||||
return nil, fmt.Errorf("缺少请假类型(projectId)")
|
return nil, fmt.Errorf("缺少请假类型(projectId)")
|
||||||
|
|||||||
@@ -13,7 +13,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 端到端契约回归:用假学工服务器接收真实请求,断言我们发出的 multipart 表单
|
// 端到端契约回归:用假学工服务器接收真实请求,断言我们发出的 multipart 表单
|
||||||
// 与前端 toFormatFormData + application_submit 的行为一致。
|
// 与前端 studentForm-Dx5Fpjfs.js 的 v()/pe() 序列化 + application_submit 行为一致。
|
||||||
|
//
|
||||||
|
// 前端真实链路(LeaveApplyDrawer-BeaVSGRQ.js 的 P()):
|
||||||
|
//
|
||||||
|
// c = ee(l.value, r.value) // 表单值
|
||||||
|
// L = ae(c, r.value) // fields 数组
|
||||||
|
// T = {projectId, requestType:"LEAVE", startTime, endTime, fields:L, address, longitude, latitude}
|
||||||
|
// f = le(T, D.value) // → Ne→v→pe,转 FormData
|
||||||
|
// await te(f) // POST application_submit (multipart)
|
||||||
//
|
//
|
||||||
// 真实学校接口无法在未登录场景下验证,但请求体的**形状**是纯客户端产物,
|
// 真实学校接口无法在未登录场景下验证,但请求体的**形状**是纯客户端产物,
|
||||||
// 可以用本地假服务器精确断言,覆盖「参数名、点号摊平、空值跳过、数组下标」。
|
// 可以用本地假服务器精确断言,覆盖「参数名、点号摊平、空值跳过、数组下标」。
|
||||||
@@ -148,28 +156,33 @@ func TestXsgzSubmitLeaveMultipartContract(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fields 必须是点号摊平的下标键(不是方括号,也不是 JSON 串)
|
// fields 必须是「数组下标方括号 + 对象键点号」:
|
||||||
if got := cap.form["fields.0.fieldId"]; got != "f1" {
|
// studentForm-Dx5Fpjfs.js: const c = `${i}[${p}]`(数组下标硬编码方括号)
|
||||||
t.Fatalf("fields.0.fieldId = %q, want f1(点号摊平)", got)
|
// pe(e,t,a) → a? `${e}[${t}]` : `${e}.${t}`
|
||||||
|
if got := cap.form["fields[0].fieldId"]; got != "f1" {
|
||||||
|
t.Fatalf("fields[0].fieldId = %q, want f1", got)
|
||||||
}
|
}
|
||||||
if got := cap.form["fields.0.value"]; got != "身体不适" {
|
if got := cap.form["fields[0].value"]; got != "身体不适" {
|
||||||
t.Fatalf("fields.0.value = %q", got)
|
t.Fatalf("fields[0].value = %q", got)
|
||||||
}
|
}
|
||||||
if got := cap.form["fields.1.value"]; got != "回家休息" {
|
if got := cap.form["fields[1].value"]; got != "回家休息" {
|
||||||
t.Fatalf("fields.1.value = %q", got)
|
t.Fatalf("fields[1].value = %q", got)
|
||||||
}
|
}
|
||||||
// 空值字段的 value 不应出现
|
// 空值字段的 value 不应出现
|
||||||
if _, ok := cap.form["fields.2.value"]; ok {
|
if _, ok := cap.form["fields[2].value"]; ok {
|
||||||
t.Fatal("空字符串 value 应被跳过,但出现了 fields.2.value")
|
t.Fatal("空字符串 value 应被跳过,但出现了 fields[2].value")
|
||||||
}
|
}
|
||||||
// 但该字段的非空属性仍应提交
|
// 但该字段的非空属性仍应提交
|
||||||
if got := cap.form["fields.2.fieldId"]; got != "f3" {
|
if got := cap.form["fields[2].fieldId"]; got != "f3" {
|
||||||
t.Fatalf("fields.2.fieldId = %q", got)
|
t.Fatalf("fields[2].fieldId = %q", got)
|
||||||
}
|
}
|
||||||
// 不应出现方括号形式
|
// 数组下标之后必须紧跟点号再接对象键,不能出现 fields[0][fieldId] 之类
|
||||||
for k := range cap.form {
|
for k := range cap.form {
|
||||||
if strings.Contains(k, "[") {
|
if strings.Count(k, "[") != strings.Count(k, "]") {
|
||||||
t.Fatalf("不应出现方括号键(前端用点号): %s", k)
|
t.Fatalf("键 %q 括号不配对", k)
|
||||||
|
}
|
||||||
|
if strings.Contains(k, "][") && !strings.HasPrefix(k, "fields[") {
|
||||||
|
t.Fatalf("键 %q 形如二维数组下标,前端不会这样提交", k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 请假请求不应带销假专有字段
|
// 请假请求不应带销假专有字段
|
||||||
@@ -224,8 +237,8 @@ func TestXsgzSubmitCancelMultipartContract(t *testing.T) {
|
|||||||
if _, ok := cap.form["startTime"]; ok {
|
if _, ok := cap.form["startTime"]; ok {
|
||||||
t.Fatal("销假未提供 startTime 时不应提交该键")
|
t.Fatal("销假未提供 startTime 时不应提交该键")
|
||||||
}
|
}
|
||||||
if got := cap.form["fields.0.value"]; got != "已返校" {
|
if got := cap.form["fields[0].value"]; got != "已返校" {
|
||||||
t.Fatalf("fields.0.value = %q", got)
|
t.Fatalf("fields[0].value = %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package cpdaily
|
package cpdaily
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 参考向量由真实前端 security.js(RSAUtils.encryptedString)在 Node 中生成,
|
// 参考向量由真实前端 security.js(RSAUtils.encryptedString)在 Node 中生成,
|
||||||
// 用于确保 Go 实现与浏览器行为逐字节一致。
|
// 用于确保 Go 实现与浏览器行为逐字节一致。
|
||||||
@@ -101,8 +98,8 @@ func TestIsXsgzSchool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// xsgzFlatten 必须与前端 toFormatFormData(needCurlyBraces=false) 规则一致:
|
// xsgzFlatten 必须与前端 studentForm-Dx5Fpjfs.js 的 v()/pe() 规则一致:
|
||||||
// 点号展开、跳过空值、数组按下标。
|
// 对象键用点号、数组下标用方括号、跳过空值。
|
||||||
func TestXsgzFlatten(t *testing.T) {
|
func TestXsgzFlatten(t *testing.T) {
|
||||||
in := map[string]interface{}{
|
in := map[string]interface{}{
|
||||||
"projectId": "uuid-1",
|
"projectId": "uuid-1",
|
||||||
@@ -125,14 +122,14 @@ func TestXsgzFlatten(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
want := map[string]string{
|
want := map[string]string{
|
||||||
"projectId": "uuid-1",
|
"projectId": "uuid-1",
|
||||||
"requestType": "CANCELLATION",
|
"requestType": "CANCELLATION",
|
||||||
"longitude": "115.8",
|
"longitude": "115.8",
|
||||||
"inArea": "true",
|
"inArea": "true",
|
||||||
"fields.0.fieldId": "f1",
|
"fields[0].fieldId": "f1",
|
||||||
"fields.0.value": "v1",
|
"fields[0].value": "v1",
|
||||||
"fields.1.fieldId": "f2",
|
"fields[1].fieldId": "f2",
|
||||||
"fields.1.value": "v2",
|
"fields[1].value": "v2",
|
||||||
}
|
}
|
||||||
for k, v := range want {
|
for k, v := range want {
|
||||||
if got[k] != v {
|
if got[k] != v {
|
||||||
@@ -140,15 +137,9 @@ func TestXsgzFlatten(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 空值不应出现
|
// 空值不应出现
|
||||||
for _, bad := range []string{"address", "empty", "fields.0.blank"} {
|
for _, bad := range []string{"address", "empty", "fields[0].blank"} {
|
||||||
if _, ok := got[bad]; ok {
|
if _, ok := got[bad]; ok {
|
||||||
t.Fatalf("空值键 %q 不应被提交", bad)
|
t.Fatalf("空值键 %q 不应被提交", bad)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 数组下标必须是点号,不能是方括号(needCurlyBraces=false 的语义)
|
|
||||||
for k := range got {
|
|
||||||
if strings.ContainsAny(k, "[]") {
|
|
||||||
t.Fatalf("键 %q 使用了方括号,前端用点号连接", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user