becomer
|
458e13b807
|
CAS 回调遇「参数解码错误」自动重取盐重试,覆盖 DK 轮换窗口
ci/woodpecker/tag/build Pipeline was successful
多个无凭证实验已确认:
- /api/sec_js 的 DK 盐每次请求都不同,且与会话无关(同 cookie 两次也不同);
- 服务端只能从密文里还原 DK,不依赖任何会话状态;
- 用当次 sec_js 加密的密文,服务端必定能解开(返回「用户名或密码错误」而非解码错误);
- 无/无效 CASTGC 时 CAS 直接返回 200+登录页(Set-Cookie 清除 CASTGC),
根本不会给出 ticket——所以「参数解码错误」发生时 ticket 是有效的,
失败只可能发生在 /cas/callback 的解密环节。
由此推论:取盐与提交之间若服务端轮换了 DK,就会解密失败。
官方前端对该错误的提示正是「刷新页面后尝试」,说明官方也视其为可重试的
瞬时问题,而非致命错误。
改动:
- 新增 isXsgzDecodeError:仅当响应体含「参数解码错误」时为真
(凭证错误、ticket 无效等不触发重试);
- 命中时重新拉取 sec_js、重新加密、重新提交一次。
安全性:解码失败意味着服务端没解出内容,ticket 未被消费,
因此重试不会浪费这张一次性 ticket。
日志:重试时记录 dkChanged(两次盐是否不同),
可据此确认线上是否真的发生了轮换,为后续是否加重试次数提供依据。
v1.2.163
|
2026-10-18 17:18:14 +08:00 |
|
becomer
|
05d9ef26b1
|
续期后加真实可用性校验,避免「假成功」导致无限重试
ci/woodpecker/tag/build Pipeline was successful
162 日志(8884 容器,v1.2.157)实证:
16:25:58 INFO 刷新cookie: 已用长期凭证续期成功 userId=1279
16:25:58 ERROR 获取表单失败 taskId=337 error="...cpdaily_cookie_expired"
续期报告成功,紧接着业务调用仍过期。
根因:refreshCookieByLongTermToken 判定成功的唯一标准是
「结果里存在 MOD_ 前缀的 cookie」。但续期走的
/imUserToken/getImUserToken 接口只校验 sessionToken 头,
**不校验 CAS 会话是否有效**;当 CASTGC/sessionToken 本身已过期时,
它仍可能返回 errCode=0,并把 jar 里那批无效 cookie 全数带回。
于是本次续期「成功」,调用方立刻重试又过期,用户只看到内部错误码。
改动:
- 新增 cpd.VerifyCookieUsable:用业务同款最小请求(拉当天任务列表)
做只读探测。三种任务类型任一能正常返回即视为有效;返回
ErrCookieExpired 才判定凭证不可用;网络等其它错误不归咎于凭证。
该探测无任何写副作用。
- refreshCookieByLongTermToken 在回写 cookie 前先跑校验,
不通过则按续期失败返回,让上层走「请重新登录」引导,
而不是发出一轮注定失败的重试。
配合上一提交(07cf121)的两处升级与 userFacingError 兜底,
现在链路上任一环节的登录态失效都会转成明确的重登引导,
不再把 cpdaily_cookie_expired 暴露给用户。
v1.2.162
|
2026-10-18 16:38:55 +08:00 |
|
becomer
|
07cf121aac
|
修 cookie 自动续期后仍失效时把内部错误码暴露给用户
现象(用户反馈):
获取签到表单失败: 获取当日任务列表失败: 解析任务列表响应失败: cpdaily_cookie_expired
期望:能自动续期就续期;续不了就明确引导重新登录(前端据此跳登录页)。
实际:既没提示重新登录,还把内部错误码 cpdaily_cookie_expired 直接显示出来。
根因:自动续期成功、但用新 cookie 重试仍判登录态失效时,
重新拿到的错误(仍是 ErrCookieExpired)被原样返回。
而 needReLoginSignal() 只认 ErrPasswordChanged / ErrNeedReLogin,
不认识 ErrCookieExpired,于是:
- errMsg 保持 "前缀 + cpdaily_cookie_expired";
- needReLogin=false,前端不会引导重新登录。
这属于「续期拿到了凭证,但凭证其实无效」的情形(长期凭证过期、
学校侧强制下线等),语义上等同于必须重新登录。
改动:
1. withCookieRefresh:续期后重试仍报登录态失效 → 升级为 ErrNeedReLogin,
这是所有 withCookieRefresh 调用方的统一入口,一处修全覆盖;
2. 签到执行路径:续期后重试仍过期同样升级,并加注释说明;
3. 新增 userFacingError(prefix, err):输出前统一兜底,
任何残留的登录态失效语义都转成「登录凭证已过期,请重新登录」,
普通错误保留原文。即使某条链路漏做升级,也不会再泄露内部错误码;
4. 「获取签到表单」与「签到提交」两处失败文案改用 userFacingError,
needReLogin 一并由它给出(前端据此跳登录页)。
测试:原始 ErrCookieExpired、多层 %w 包装、ErrNeedReLogin、
JSON 文本「未登录」均转成引导重登文案且不含内部错误码;
普通错误原样保留;nil 不触发重登。
|
2026-10-18 16:36:31 +08:00 |
|
becomer
|
8b647fc31a
|
CAS 链路加诊断日志,便于一次请求定位「参数解码错误」
ci/woodpecker/tag/build Pipeline was successful
上一轮用公开接口做对照实验,确认了服务端响应语义:
- 正确加密但 ticket 无效 → 「用户名或密码错误」(解密成功)
- 裸 ticket / 乱码密文 → 「参数解码错误」(解密失败)
- 空 ticket → 500
同时已排除代码侧原因(Go 与站点 security.js 产物逐字节一致、
chunkSize=126 一致、sec_js 禁缓存、算法自 v1.2.157 未变)。
但「参数解码错误」仍缺一处可见性:CAS 换票拿到的 ticket 本身是什么形态、
加密后几块、服务端原始响应是什么,线上日志都没有。没有这些信息,
下一次复现只能继续猜。
新增(均不含凭证本体):
- CAS 换票成功:ticketLen / hasSTPrefix / hasSpace / service
- 加密完成:ticketLen / dkLen / encLen / blockCount
(blockCount>1 才可能出现多块空格,届时 body 是否被 URL 编码才有区别;
这条日志能一次性回答该问题)
- 登录被拒:httpStatus / message / bodySnippet(截断 300 rune)
配套 truncateForLog:按 rune 截断,不切断多字节字符。
测试覆盖短串不截断、超长加标记、中文不产生乱码、边界值。
v1.2.160
v1.2.161
|
2026-10-17 23:11:37 +08:00 |
|
becomer
|
392c90ec11
|
更正 CAS 注释的推断错误,并兼容 ticket 的三种参数名
上一提交 e38fd89 把「参数解码错误」归因于 url.QueryEscape 把密文空格
变成 '+'。本次用站点真实的 security.js 在 Node 里跑通加密链路后,
该归因被证伪:
- RSAUtils.chunkSize = 2 * biHighIndex(m) = 126(字符),
与 Go 的 k-2 = 126 一致;
- 实测真实密钥下,典型 CAS ticket(数十字符)明文约 77 < 126,
产物是**单块 256 hex、不含空格**,QueryEscape 与否完全等价;
- 用同一 DK/M/E 对拍,Go 与前端 security.js 的输出**逐字节一致**,
说明加密实现本身没有问题。
因此「参数解码错误」更可能是 ticket 本身已失效/被消费
(CASTGC 过期、同一 ticket 重复使用),而非编码差异。
注释已改为陈述实测事实,不再保留错误推断。
保留的直接拼接本身无害且更贴近前端原文(避免将来 ticket 变长
产生多块时引入空格→'+' 的偏差),故不回退。
顺带补上真实存在的兼容差异:前端 OAuthCallback 从 query 里按
ticket / service_ticket / ST 三个名字依次取,我们原先只认 ticket。
|
2026-10-17 23:08:54 +08:00 |
|
becomer
|
e38fd89425
|
修销假两个线上报错:projectId 为空、CAS 参数解码错误
线上(162,v1.2.160,userId=1490)实测两类报错:
提交销假失败: 缺少请假类型(projectId)
学工系统登录失败: 登录失败: 参数解码错误,刷新页面后尝试
且伴随 WARN「获取销假表单字段失败: 未找到数据」——因为 projectId 为空,
form_fields_for_add 查不到任何东西。
1) projectId 为空
projectId 是销假提交必传项,但前端销假请求体未必带(TypeWid 在列表里,
不一定回传到执行接口)。原实现直接拿 req.ProjectID 用,为空即报错。
修复:
- 为空时按 leaveId 调 leave_detail 回查补齐,补到后记 INFO;
- 回查仍拿不到则明确报「无法确定该请假记录所属的请假类型,
请返回列表刷新后重试」,而不是发出注定失败的提交。
- 同时修 XsgzFetchLeaveDetail 的解析:前端取的是
`c.value?.project?.id || c.value?.projectId`,项目 id 可能嵌在
project 对象里;原先只读顶层 projectId,现按「顶层优先、
回落 project.id」处理,项目名同样回落到 project.text。
2) CAS 参数解码错误
前端 getOAuthToken 的 body 是 `ticket=${s}` 直接拼接,**不做 URL 编码**。
我们原先用 url.QueryEscape,而密文由多块小写 hex 以**空格**连接
(实测长 ticket 分 2 块),QueryEscape 会把空格变成 '+';
服务端按原始 body 解析时还原不回空格,逐块解密失败,
正好对应「参数解码错误,刷新页面后尝试」。
修复:改为与前端一致的直接拼接。
测试:
- 回查补齐 projectId 的两条路径(嵌套 project.id、顶层优先);
- CAS body 保留原始空格、不得出现 '+';
- 反例固化 QueryEscape 会破坏 ticket,说明为何不能用它。
|
2026-10-17 23:05:30 +08:00 |
|
becomer
|
9bf2d46f84
|
修人脸识别排队被误判为失败:识别中应继续等待
问题(162 服务器日志实证):
ERROR 人脸识别被系统驳回,判定签到失败 userId=1043 message=识别中,预计等待4位
ERROR 人脸识别被系统驳回,判定签到失败 userId=1043 message=识别中,预计等待8位
「识别中,预计等待N位」是服务端识别队列的进度提示,不是失败。
但 evalFaceIdentify 里只要 recognitionProgress 非空就判 Rejected+Settled,
于是提交后的复核立刻判定签到失败,用户看到报错,实际签到可能仍在排队后成功。
根因:
if res.Progress != "" { res.Rejected = true; res.Settled = true; return }
修复:
1. 新增 faceProgressIsPending / faceProgressIsTerminalFailure 两个判定:
- 含「识别中/排队/处理中/等待」等 → 进行中,继续等待;
- 含「未通过/失败/驳回/人脸不匹配/未检测到人脸」等 → 终止失败;
- 两者皆无的未知文案 → 保守按进行中处理,不误杀(超时后仍走原有
「未出结果按成功」兜底);
- 判定失败前先排除排队文案,避免「识别中」被误伤。
2. handler 轮询策略调整:原先固定 4 轮(0/1/2/3 秒)就收手,排队靠后
必然等不到结果。现在前 4 轮快速取结果,之后每 3 秒一轮,最长再等
90 秒;期间把最新排队提示透传给前端(queued=true),用户能看见进度。
测试:用日志里的真实文案断言不判驳回;同时保留 FRS.0036 等真实失败
文案必须判驳回;progressCode=2 无文案时仍判驳回;未知文案不误杀。
|
2026-10-17 21:37:07 +08:00 |
|
becomer
|
152878cc7c
|
销假填充与 normalize 打架:天气字段不再被地址污染
自审本轮改动时发现的不一致:销假路径先按标签用地址兜底填充,
再调用 normalizeXsgzFields,而后者对 regions 是「清空 value」。
两者叠加导致:
- 若字段名含"原因/说明"且是 regions 类型,地址先被写入、又被清空,
等于白填,且语义上互相矛盾;
- 上传类字段同样可能先被写入地址,再被 delete(value)。
请假路径的 fillXsgzFieldValues 已跳过这两类,销假这段循环没跟上。
改动:销假填充循环加 xsgzNonTextFillable 判断,与请假路径保持一致。
测试:一个同时含 regions / imageUpload / TEXT 的销假表单,
断言文本字段得到地址、地区字段为空、上传字段无 value。
|
2026-10-17 18:26:27 +08:00 |
|
becomer
|
b90a1dcd8f
|
xsgz 纯原始值数组改为 join(","),并引入对拍测试防止再次误读前端规则
写了一个忠实转写前端 v()/pe() 的参考实现做对拍,立刻发现一处真实偏差:
前端: tags = "a,b,c" (单个键)
Go: tags[0]=a tags[1]=b tags[2]=c (三个索引键)
前端 v() 对数组是两条分支:
if (s.some(l => l && typeof l == "object")) → 按下标展开 `${i}[${p}]`
else → const l = s.map(p => p ?? "").join(","); append(i, l)
我们只有前一条分支。学校端按 tags 读字符串时拿不到值。
此前几轮反复在「方括号 vs 点号」上判断失误,根因是仅凭注释与内部一致性
推断契约。这次改为用可执行的参考实现做对拍,让差异由测试暴露而不是靠人眼。
改动:
- xsgzFlatten 数组分支按是否有对象分流:纯原始值 join(",")(空数组
与全空值不提交),含对象才按下标展开;键名规则本身不变
(对象键点号、数组下标方括号);
- 新增 leave_xsgz_jsref_test.go:把前端 v()/pe() 转写成 jsFlatten,
用真实形态载荷(请假、销假、多字段、嵌套 map、空 fields)对拍,
双向断言「不缺键、不多键、值一致」;
- 记录一处有意差异并说明理由:前端会把 null 提交为空串,我们跳过空值,
对必填校验等价且更干净。
|
2026-10-17 18:25:23 +08:00 |
|
becomer
|
6bc7ab359c
|
xsgz regions 字段不再被文本值污染,且必填时能正确拦下
前端 Ie() 对 regions 组件的处理(studentForm-Dx5Fpjfs.js):
s = (e[fieldId+"_regionsDetail"] ?? e[fieldId+"_detail"])
?.replace(/,/g,",")?.trim() || ""
o.value = 选中值 ? (s ? `${选中值},${s}` : 选中值) : (s || n)
即 value 是「选中值,详情」的拼接串,详情还会把半角逗号换成全角。
本平台不提供地区选择,没有 _regionsDetail/_detail 可拼接,
此前的实现会把「请假原因」当作地区值写进 value,
学校端会收到无法解析的非法地区串。
改动:
- normalizeXsgzFields 对 regions 清空 value(置 "",由 xsgzFlatten 跳过,
不会提交该键),避免残留脏值;
- fillXsgzFieldValues 跳过 regions(新增 xsgzNonTextFillable 统一判定
上传类与地区类),不再把文本写进地区字段。
顺带修掉一个逻辑矛盾:checkXsgzUnfilledRequired 原先对「非文本可填」类型
一律 continue,导致**必填的 regions 字段永远不会被拦下**——
请求会带着空地区发出去,然后被学校端含糊拒绝。
现在只有上传类跳过(它由 uploadFiles 承载,无 value 属正常),
regions 必填无值时正常纳入预检,明确提示用户缺哪个字段。
测试:regions 不被填入请假原因、残留 value 被清空、
必填 regions 触发预检拦下(该用例正是暴露上述矛盾的地方)。
|
2026-10-17 18:24:19 +08:00 |
|
becomer
|
bb4a1d2325
|
xsgz 字段提交按前端 Ie() 语义清洗:上传类去 value、多选 join 逗号
从 studentForm-Dx5Fpjfs.js 的 Ie() 看到字段有三条分支:
regions: value = 拼接地名
imageUpload / fileUpload / smoothSignature: o.uploadFiles = 文件列表;不设 value
其余: o.value = Array.isArray(n) ? n.join(",") : n
我们的实现只有一条路——直接写 value,导致两个偏差:
1. 上传类字段(图片/文件/手写签名)被塞了文本 value,前端根本不会这样提交,
且从未提供 uploadFiles。学校端若要求附件,行为不可预期。
2. 多选类字段若 value 是数组,xsgzFlatten 会展开成 fields[0].value[0]
/ fields[0].value[1],学校端按 value 取字符串时读不到;
前端是 join(",") 成单个字符串。
改动:
- 新增 normalizeXsgzFields,按上述三分支净化字段定义:
上传类删除 value(本平台不代传附件,不臆造 uploadFiles),
数组 value 按前端规则 join(",")(跳过 nil 与空串);
- 请假、销假两条提交路径都接入;
- fillXsgzFieldValues 跳过上传类字段,避免把「请假原因」写进附件字段;
- checkXsgzUnfilledRequired 不把上传类必填字段算作缺失
(它们由 uploadFiles 承载,无 value 属正常),避免误拦。
测试:三种上传类型去 value、多选 join、数组含 nil/空串、普通字段不受影响、
空输入不 panic、填充跳过上传类、预检不误拦上传类。
|
2026-10-17 18:23:06 +08:00 |
|
becomer
|
96de022871
|
xsgz 登录后补拉 /auth/user 拿 roleId,不再只依赖 cas/callback 响应
核对前端源码发现两者的获取路径是分开的:
getOAuthToken: setToken(m.data.data?.access_token) // 只管 token
getUserInfo: POST /auth/user → data.role.roleId // roleId 在这里
axios 拦截器: s.headers.roleId = JSON.parse(getCurrentRole()).roleId
s.headers.Authorization = getToken() // 原文,无 Bearer 前缀
我们原先在同一处只读 cas/callback 的 data.userInfo.roleId。
若该校响应不含该嵌套结构,RoleID 会为空,导致:
- 所有后续请求缺少 roleId 头;
- 学校端可能直接拒绝,且报错信息不会指向真正原因。
改动:
- 登录成功后主动 POST /api/auth/user 取 data.role.roleId(roleName 同步);
- 该调用失败不阻断(slog.Warn 后沿用 cas 响应的值),保持向后兼容;
- 两者都拿不到 roleId 时才明确报错「未能获取角色信息」,
避免带着空 roleId 一路请求到被学校端拒绝;
- realName 缺失时回退 xm(国内学工系统常见字段名)。
顺带核实两处未被验证过的契约,结论是现有实现正确、无需改动:
- Authorization 头是 token 原文,不加 Bearer 前缀;
- RoleID 与前端 getCurrentRole().roleId 语义一致。
测试:/auth/user 的路径与 POST 方法、role/roleName/realName 解析、
realName 缺失回退 xm、非 0 code 必须报错。
|
2026-10-17 18:21:52 +08:00 |
|
becomer
|
7c2b9aa3a7
|
销假范围标记改用 leave_detail 精确查询,避免分页上限导致漏拦截
上一版 recordRequiresInArea 拉 50 条列表再线性查找留两条隐患:
记录超过 50 条时找不到 → 误判「不要求」→ 漏掉前端那道拦截。
核对前端源码后改为按 leaveId 精确查询(LeaveDetailView 的 ve()):
s.listType==="apply" && (d.leaveId = s.data.id, n = await Ft(d))
c.value = { ...n }
"cancellation-apply-in-area": c.value?.cancellationApplyInArea === true
|| c.value?.cancellationApplyInArea === "是"
其中 Ft 即 studentForm 的 he → GET /leave/student_action/leave_detail。
改动:
- 新增 XsgzFetchLeaveDetail(GET leave_detail?leaveId=...,code!=0 报错);
- recordRequiresInArea 改为「详情优先、列表回退、都失败则保守 false」,
既消除分页隐患,也保留对详情接口异常的容错。
测试:详情接口路径与参数名、bool true / 字符串「是」两种标记解析、
非 0 code 必须报错(不能静默当成空详情)。
|
2026-10-17 18:20:28 +08:00 |
|
becomer
|
ab4f29a474
|
销假范围强制校验对齐前端:读 cancellationApplyInArea 决定是否拦截
上一版(355557b)算出 inArea 就上报,但没有判断「该校是否要求在范围内」,
等于把前端的一道拦截丢了。前端源码(LeaveDetailView-BveoXCrD.js):
const v = computed(() => a.actionType==="CANCELAPPLY" && a.cancellationApplyInArea)
if (v.value && !T?.canPosition) { warning("不在销假申请范围内,无法销假"); return }
该标记的来源也一并核实(同文件传入 StudentActionDialog 的 props):
"application-id": c.value?.id || s.data?.id
"project-id": c.value?.project?.id || c.value?.projectId
"cancellation-apply-in-area":
c.value?.cancellationApplyInArea === true
|| c.value?.cancellationApplyInArea === "是"
顺带确认了 originalId 与 projectId 的取值与我们现有实现一致
(ToLeaveItems 的 ID=r.ID、TypeWid=r.ProjectID)。
改动:
- XsgzLeaveRecord 增加 CancellationApplyInArea(用 interface{} 兼容
bool 与 "是" 两种返回),并加 RequiresInArea() 做等价归一化;
- 新增 recordRequiresInArea:后端自行重查记录判定,不复用前端传值
(可篡改、且可能未传);查不到时保守返回 false,不额外拦截;
- 销假流程改为:该校要求范围内且实际不在范围内 → 明确报错并拒绝提交,
与前端一致;未配置范围时 inArea 视为 true。
测试:RequiresInArea 覆盖 bool true/false、"是"/"否"、"true" 字符串、
nil、数字等输入。
|
2026-10-17 18:19:27 +08:00 |
|
becomer
|
355557b6ce
|
销假补上地理范围校验:inArea 此前恒为 false,学校端可能直接拒绝
从 sdau 线上源码(LeaveDetailView-BveoXCrD.js)拿到销假提交的真实实现:
const W = _e(r.value, I.value), // 构造 fields
H = { originalId: a.applicationId, projectId: a.projectId,
requestType: "CANCELLATION", fields: W,
cancellationApplyInArea: a.cancellationApplyInArea,
inArea: T?.canPosition || !1, // ← 客户端地理围栏计算结果
address, longitude, latitude },
z = he(H, M.value), B = await Te(z)
前端还有一道硬拦截:
const v = computed(() => a.actionType==="CANCELAPPLY" && a.cancellationApplyInArea)
if (v.value && !T?.canPosition) { warning("不在销假申请范围内,无法销假"); return }
而我们的实现里 XsgzSubmitPayload.InArea 从未被赋值,销假请求恒传
inArea=false。若该校 cancellationApplyInArea=true,前端不会提交的请求
我们会照发,结果被学校端拒绝,用户只看到含糊的失败。
新增(均对齐前端):
- XsgzFetchLocationRange:GET /api/leave/student_action/location_range
(前端 ve/bt,路径由源码核实)
- InLocationRange:复刻前端 c() 的围栏判断——Circle/poi 比球面距离,
Polygon/Rectangle 走射线法点在多边形内;范围为空时返回 true
(对应前端 !r.value.length 时 return true)
- 销假流程先拉范围再算 inArea;范围查询失败或无有效坐标时按不限制处理
并记日志,不让本校未配置范围的情况把功能卡死
复用 task.go 已有的 haversineMeters(注意其签名是 lat1,lon1,lat2,lon2),
未重复造轮子。
测试覆盖:空范围、圆形半径内外、poi、多边形内外、矩形、多范围任一命中、
退化多边形(<3 点)。
|
2026-10-17 18:18:09 +08:00 |
|
becomer
|
568329890b
|
回退 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] 这类二维下标形态。
|
2026-10-17 18:15:35 +08:00 |
|
becomer
|
f1f79dfb6c
|
修正 xsgz 表单数组下标:fields[0] 改为 fields.0,与 needCurlyBraces=false 语义一致
新增的 httptest 端到端契约测试(真发 multipart,假服务器接收)发现:
xsgzFlatten 对数组用 fmt.Sprintf("%s[%d]"),实际提交键为 fields[0].value,
而同一函数的注释、XsgzSubmitApplication 注释、以及 07b0cfa 的提交说明
三处都写的是点号 fields.0.value。
needCurlyBraces=false 的语义本身就是「不用方括号/花括号」,实现与此矛盾。
resty 对 multipart 键名原样透传(addMultipartFormField →
createMultipartHeader(mf.Param)),方括号完全来自我方代码,不是库行为。
后果:学校端按 fields.0.value 取值时拿不到值,请假原因等字段为空,
大概率被判定为「必填项未填写」而提交失败。
改动:
- xsgzFlatten 数组分支改为 "%s.%d",并在注释里记录这次错误;
- TestXsgzFlatten 的期望值同步修正(原断言固化了错误行为),
并新增断言:所有键不得含 [ 或 ];
- 新增 leave_xsgz_submit_test.go:用 httptest 假服务器捕获真实请求,
端到端断言路径、Authorization/roleId 头、顶层参数名、
fields 点号摊平、空值跳过、请假/销假字段互不串味、
以及缺 projectId/originalId 时本地拦截不发请求。
|
2026-10-17 18:12:53 +08:00 |
|
becomer
|
00bd63cfdf
|
修复销假/请假 SSE 崩溃:stream writer 内误用已回收的 fasthttp RequestCtx
线上 v1.2.158 销假必崩,前端显示「连接失败,请检查网络」,用户误判为网络问题。
根因(162 服务器日志实证):
SetBodyStreamWriter 的回调运行在 fasthttp 的独立写协程中,此时 *RequestCtx
已被归还复用。回调内调用 newXsgzSession(c.Context(), ...) 把该 ctx 交给
resty 的 SetContext,resty 内部走 context.WithDeadline → propagateCancel
→ parent.Done(),而 RequestCtx.Done() 直接解引用 ctx.s(server.go:2726):
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV] fasthttp.(*RequestCtx).Done server.go:2726
此时 s 已为 nil,触发 SIGSEGV。
panic 发生在协程里,fiber 的 recover 中间件捕获不到,直接杀死整个进程,
前端 XHR 连接被掐断 → 只剩「连接失败,请检查网络」这条误导性提示。
修复:
1. 新增 streamCtx():返回 context.WithTimeout(Background, 3min),
脱离请求生命周期,回调内一律使用它(销假与请假两条链路);
2. 两个回调都加 recover 兜底,即便再出现协程内 panic 也只返回
task_error 事件,不会拖垮进程。
回归测试:TestStreamCtxIsUsable 保证 ctx 具备 Done/deadline;
TestRequestCtxDonePanicsAfterReset 用零值 RequestCtx 固化崩溃机制,
解释为什么不能用 c.Context()。
|
2026-10-17 18:07:34 +08:00 |
|
becomer
|
e7c9b06056
|
xsgz 提交前预检必填字段,避免含糊失败
学校请假表单由后台动态配置,可能包含本平台无法提供值的必填字段
(如"是否离校"单选、请假类型细分下拉)。原先这类情况会直接提交,
由学校端返回业务错误,用户看到的提示不明确。
新增 checkXsgzUnfilledRequired:提交前检查所有 required 字段是否已有值
(空串/null 均视为缺失),有缺失则不发请求,直接提示
「该校请假表单还有必填项无法自动填写:X、Y,请前往学校学工系统办理」,
并写入日志。请假与销假两条路径都已接入。
新增 5 个单测覆盖:有值不报、无值报字段名、空串算缺失、
无 fieldName 回退 fieldId、非必填不报。
|
2026-10-17 17:46:49 +08:00 |
|
becomer
|
c8ff086b7b
|
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。
|
2026-10-17 17:45:12 +08:00 |
|
becomer
|
07b0cfaa88
|
校正 sdau 提交参数序列化,严格对齐前端 toFormatFormData
ci/woodpecker/tag/build Pipeline was successful
重新核对前端源码发现两处与实现不符,已修正:
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}
v1.2.158
|
2026-10-17 17:43:31 +08:00 |
|
becomer
|
4a064b53cc
|
sdau 销假接口实现:与请假共用 application_submit,靠 requestType 区分
静态分析定位到请假详情页组件链(views/leave/detail/):
ActionPopup-CRBsBafz.js 中 handleType=CANCELAPPLY 的销假申请逻辑,
其提交函数 re 从 fieldsConfig-BzLAymBp.js 导入,即与请假同一个
application_submit 接口。
销假 payload(requestType=CANCELLATION):
{ originalId: <原假单ID>, projectId, requestType:"CANCELLATION",
fields[], cancellationApplyInArea, inArea, address, longitude, latitude }
改动:
- service:新增 XsgzSubmitCancel / XsgzSubmitApplication(请假/销假/续假共用),
XsgzSubmitPayload 增加 originalId/inArea/cancellationApplyInArea
- service:XsgzLeaveRecord 增加 projectId 字段;ToLeaveItems 把 projectId
写入 TypeWid,供前端销假时回传
- handler:新增 executeXsgzLeaveCancel,SSE 事件流 + 落日志
- handler:ExecuteLeaveRequest 增加 projectId
- 前端:ExecuteLeaveParams 增加 projectId;销假提交时回传 typeWid
另:batch-cancel / batch-cancel-list 经确认是辅导员侧接口,学生销假不走它们。
|
2026-10-17 17:40:16 +08:00 |
|
becomer
|
cb3e0f8917
|
sdau 提交请假接口实现:按前端源码解析的契约接入 application_submit
从主 bundle 路由表 __vite__mapDeps 定位到请假页 chunk 链
(LeaveApply → index-C4CfkeDm.js → fieldsConfig-BzLAymBp.js),
完整解析出提交契约:
POST /api/leave/student_action/application_submit (multipart/form-data)
payload = { projectId, requestType:"LEAVE", startTime, endTime,
fields[], address, longitude, latitude }
改动:
- service:新增 XsgzSubmitLeave(multipart 提交)、XsgzFetchIsRequireLocation
- service:do() 支持 JSON body;新增 XsgzSubmitPayload
- handler:新增 executeXsgzLeaveApply,SSE 事件流与辅导猫链路一致,
并落 cp_leave_logs
- LeaveType / ExecuteLeaveApplyParams 新增 projectId:xsgz 用 UUID 作类型 ID
- 前端提交时带上 projectId
顺带修正 GetPendingLeaves:batch-cancel-list 经实测是**辅导员**接口
(学生调用报「没有辅导员销假权限」),学生端改回 leave_page 按状态标记 CanReport。
注:销假写接口(batch-cancel)学生侧契约仍未确认,暂未接入。
|
2026-10-17 17:34:01 +08:00 |
|
becomer
|
3b46e45eb6
|
sdau 销假/请假改为真实数据,不再只做拦截提示
问题:上一版对 xsgz 学校只返回「请前往学校学工系统办理」的提示,
用户点销假按钮(/leave/pending-list)和请假类型时拿不到任何真实数据。
改动:
- 新增 CasTicketForService:用已有 CASTGC 换取指定 service 的 CAS 票据,
不发起账密登录(handler 侧只有会话、没有明文密码)
- 新增 XsgzSession.ToLeaveItems:把 xsgz 请假记录转换为通用 LeaveItem 契约,
status 为「休假中/逾期未销假」时 CanReport=true
- GetPendingLeaves:xsgz 学校改为真实拉取 /api/leave/record/leave_page
- GetLeaveTypes:xsgz 学校改为真实拉取 optional_project_all
- LeaveType 新增 ProjectID 字段:xsgz 用 UUID 作类型 ID,与旧契约的整数 code 并存
只读链路已实测通过:登录→CASTGC 换票→access_token→请假记录(1条) / 请假项目(1个)。
|
2026-10-17 17:25:21 +08:00 |
|
becomer
|
ef03130980
|
请假/销假全部入口补齐独立学工系统分流,修复点销假仍报 420
ci/woodpecker/tag/build Pipeline was successful
问题:上一版只在 GetLeaveTypes 加了 xsgz/xsfw 分支,其余请假入口仍走辅导猫
旧链路。用户点销假按钮(走 /leave/pending-list)时仍拿到辅导猫的 HTTP 420
「产品已到期」,提示误导且无法自助解决。
改动:新增 rejectExternalLeaveSystem 统一守卫,接入全部 7 个请假相关入口:
- GetLeaveTypes / GetLeaveApplyApprovers / ExecuteLeaveApply(请假侧)
- GetPendingLeaves / GetLeaveApprovers / ExecuteLeave(销假侧)
- GetLeaveExtendPrefill(续假)
命中 xsgz/xsfw 时短路返回明确文案「XX大学 的请假由学校「学工系统」独立提供,
不支持在今日校园内提交,请前往学校学工系统办理」,不再让用户看到 420。
新增单测验证分流判定与文案(纯静态映射,不触发网络)。
v1.2.157
|
2026-10-17 17:12:52 +08:00 |
|
becomer
|
17d3b07c84
|
CAS 登录按页面实际 captchaSwitch 选择验证码形态,不再只依赖学校静态配置
ci/woodpecker/tag/build Pipeline was successful
问题:isSliderCaptchaSchool 一旦命中就永远走滑块分支。但 wisedu CAS 的
captchaSwitch 会随风控状态变化——连续失败后该校可能从滑块(2)切换为
图片验证码(1),此时 openSliderCaptcha 返回空图、滑块必然失败,最终表现为
「登录失败,请检查账号密码」,掩盖了真实原因。
改动:新增 casCaptchaMode 从登录页读取 captchaSwitch(1=图片 2=滑块),
登录时优先按页面实际值分流;页面未声明时才回退到学校级 sliderIds 配置。
图片验证码分支复用已有 captchaOCR。
新增单测覆盖 captchaSwitch=1/2/缺失/未知取值/含空白等情形。
v1.2.156
|
2026-10-17 17:05:18 +08:00 |
|
becomer
|
45212898ae
|
修复 xsfw 学工会话建立:改用 cookie jar 收集全链路 cookie
ci/woodpecker/tag/build Pipeline was successful
原实现只从最终响应 cb.Cookies() 取 cookie,会丢掉重定向链中间 302 下发的
MOD_AUTH_CAS / _WEU,导致后续 modules 请求 403。
- 改用 cookie jar 贯穿「CASTGC 换票 → 跟随回调」两步,最后从 jar 全量取;
- 换票与回调拆成两个 resty client:NoRedirectPolicy 影响 client 级且请求开始后
不生效,同一 client 无法先取 302 再跟随;
- resty 在 NoRedirectPolicy 下遇 302 返回 error,但响应本身有效(含 Location),
改为忽略该 error、只判断 resp == nil。
v1.2.155
|
2026-10-17 17:03:29 +08:00 |
|
becomer
|
f2d89a6d61
|
新增两套独立学工请假系统的 flavor 分流与接口封装
ci/woodpecker/tag/build Pipeline was successful
背景:山东农业大学、云南警官学院的辅导猫请假应用均为 HTTP 420「产品已到期」,
两校实际使用各自学校的学工平台请假,与辅导猫无关,需要按学校单独适配。
1. 新增 LeaveFlavorXsgz / LeaveFlavorXsfw 两个 flavor,leave_flavor.go 按
schoolId 优先分流(静态映射,不参与探测缓存),其他学校仍走原 new/old 流程。
2. leave_xsgz.go —— 三易拓「新学工5」(山东农业大学 xsgz.sdau.edu.cn):
CAS(含 wisedu 滑块) → ST → RSA 加密 ticket → /api/cas/callback 换 access_token
→ /api/leave/* 查询。关键细节:
- /api/cas/callback 必须用 application/x-www-form-urlencoded 的 `ticket=xxx`
表单串(用 JSON 会被服务端返回 500);
- RSA 是 RSAUtils 自定义格式(非 PKCS#1):encodeURIComponent(DK+ticket) 按
charCode 小端装 BigInt,chunkSize=模数字节数-2,输出 hex+空格分隔;
- 公钥盐 DK 每次请求都变,必须与模数同批取用。
已用真实前端 security.js 生成向量交叉验证算法逐字节一致;
端到端实测登录/请假列表/类型/表单字段全部通过。
3. leave_xsfw.go —— 金智 xsfw 学工「学生请假」(云南警官学院):
突破点:辅导猫登录拿到的 MOD_AUTH_CAS(ST-iap 格式)在 xg 学工域无效,
需用 CASTGC 到 authserver 重新换一次 service 指向学工应用的票据,
回调后 xg 域会话才建立(应用首页返回完整 HTML,含 WIS_CONFIG/APPID/APPNAME)。
另修复该校响应的多重 UTF-8 BOM(标准 json.Unmarshal 会直接失败)。
遗留:modules 业务接口仍 403,待真实抓包确认所需请求头。
4. 单测覆盖 RSA 加密向量、encodeURIComponent 边界、BOM 剥离(含前置空白组合)、
schoolId 分流映射。go build/vet/test 全绿。
v1.2.154
|
2026-10-17 16:59:13 +08:00 |
|
becomer
|
18ead961de
|
修复请假/销假提交偶发「连接失败,请检查网络」
ci/woodpecker/tag/build Pipeline was successful
现象:iOS 独立 QQ 浏览器 App 提交请假时偶发报「连接失败,请检查网络」,
重试十余次偶尔成功;同页其他接口(axios/XHR)与打卡(EventSource)均正常。
根因:请假/销假是唯一使用 fetch + res.body.getReader() 手动解析 SSE 的接口
(因需 POST + JSON body,无法用 EventSource)。这些浏览器对 fetch 响应流的
支持不稳定,偶发 res.body 为 null 或流读取失败,命中 !res.body 分支误报
「连接失败」;服务端实测始终返回 200 + 正确 SSE(生产日志 189 次全 200)。
修复:
- 前端 leaveSSE 改用 XMLHttpRequest 增量解析(onprogress 累积 responseText),
回调签名与事件语义不变;XHR 在这些浏览器上已被证明可靠,且能取到
状态码与错误响应体,错误提示更准确。
- 后端 gzip 排除列表由仅 /api/task/execute/ 扩大到全部 SSE 端点
(/api/task/execute/、/api/leave/apply、/api/leave/execute),SSE 流保持明文。
排查过程:曾怀疑 gzip 缓冲破坏 SSE,写了两个探针测试(事件到达时序、
压缩 vs 不压缩),证伪——压缩下事件仍逐块即时到达,gzip 非根因。
v1.2.153
|
2026-10-17 00:32:58 +08:00 |
|
becomer
|
57903d9b34
|
工单状态语义对齐:处理中改蓝、已完成/关闭文案、用户删除自动关闭
ci/woodpecker/tag/build Pipeline was successful
- 状态颜色:处理中由品牌青绿改为蓝色(adm-badge-sky),即
待处理(黄)/处理中(蓝)/已完成(绿)/关闭(灰)。
- 状态文案:resolved「已解决」→「已完成」,closed「已关闭」→「关闭」;
管理端列表筛选与详情按钮(标记已完成)同步。
- 用户删除自动关闭:DeleteTicket 软删除前先将 status 置为 closed,
此后不可回复,管理端也不再计入未关闭待办。
- 回复权限维持:closed 阶段禁止回复,其余状态(待处理/处理中/已完成)
用户均可在客户端回复(用户回复已完成工单会自动重开为待处理)。
v1.2.151
v1.2.152
|
2026-10-17 00:03:30 +08:00 |
|
becomer
|
486e7ede04
|
工单与公告问题修复:回复强制弹窗、消息左右顺序、指定用户搜索、用户回复邮件通知
ci/woodpecker/tag/build Pipeline was successful
1. 工单新回复任意页面强制弹出:通知新增 kind 字段(reply/status),
MobileLayout 轮询检测本设备本账号新出现的未读 reply 通知并强制弹窗
(打卡/请假/我的等所有页面均生效);首次加载只记基线不弹历史,
基线按用户 id 分开存储避免多账号串扰;状态变更通知不弹窗。
2. 修复管理端工单对话左右顺序:此前 user/admin 位置对调(admin 在左、
user 在右),现 manager 侧 admin 居右、user 居左,与用户端一致。
3. 公告指定用户改为搜索选择:新增 /admin/announcements/targets,
按用户名/学号/姓名/学校(代理按用户名)模糊查询,前端改为
搜索候选列表点选,替代手动输入用户ID。
4. 用户回复工单也邮件通知管理员:新增 NotifyTicketUserReply,
AddTicketMessage 后异步发信。
v1.2.150
|
2026-10-16 23:15:26 +08:00 |
|
becomer
|
27157a7b39
|
合肥工业大学登录适配 + 人脸复核等待序列改为 0/1/2/3 秒
ci/woodpecker/tag/build Pipeline was successful
合肥工业大学(school_id 46a6ac8d-...):平台配置 joinType=CLOUD、idsUrl=hfut.campusphere.net/iap,
但网关 IAP 账密接口不兼容,登录报「账号或密码错误」。实测真实链路为
/iap 302 → cas.hfut.edu.cn/cas/login(标准 Apereo CAS,表单 id=fm1):
- 密码需 AES-ECB/PKCS7 加密,key 来自 /cas/checkInitParams 下发的 LOGIN_FLAVORING cookie
(通用 casLogin 从不调该接口,拿不到 key,password 加密错误导致误报密码错误);
- 提交账密后 302 回调 /iap/loginSuccess?sessionToken=...&ticket=ST-... 建立网关会话(MOD_AUTH_CAS)。
新增 login_hfut.go 单独适配(hfutCasLogin/hfutEncryptPassword/hfutCasEntry),并在
CpdailyLogin 分发中接入;已用真实账号实测登录成功。
另:人脸识别复核轮询等待序列由 0/1/2(累计3s)改为 0/1/2/3 秒,逐次等待递增到 3 秒。
v1.2.149
|
2026-10-16 22:54:06 +08:00 |
|
becomer
|
44de36c47b
|
修复人脸识别签到「假成功」:提交后复核结果并按真实结果扣费
ci/woodpecker/tag/build Pipeline was successful
今日校园对开启人脸识别的查寝/签到任务,submitSign 返回 code=0 仅表示受理成功,
人脸核验在服务端异步进行,可能事后被系统自动驳回(如 FRS.0036 从url获取文件失败)。
此前仅凭 code=0 即判成功,导致日志记成功、照常扣费,而今日校园侧实为未签到。
- 后端:提交成功后若任务开启人脸识别,回查 detailSignInstance 的
isFaceIdentify/recognitionProgress/recognitionProgressCode/signStatus 判定真实结果;
立即查 → 无结果等 1s → 再无结果等 2s(累计≤3s,t≈0/1/3)。
驳回则经 SSE 返回错误并标记失败,不扣费;明确成功才扣费;
三轮后仍无明确结果按成功处理,避免误杀。
- 前端:上传/提交前校验图片非空且体积>0,压缩产物为空回退原文件,
上传失败提示重新选择照片,防止 0 字节空对象入 OBS。
- 新增 evalFaceIdentify/NeedsFaceIdentify/rawCodeString 单测。
v1.2.148
|
2026-10-16 22:47:16 +08:00 |
|
becomer
|
0ca3528afd
|
请假/续假与销假弹窗新增操作提示
ci/woodpecker/tag/build Pipeline was successful
- 请假/续假申请弹窗表单顶部提示:定位经纬度请使用微信小程序「经纬度查询定位助手」
- 销假表单弹窗顶部提示:销假请使用请假设备,授权设备码,否则无法正常销假
v1.2.147
|
2026-10-15 09:34:24 +08:00 |
|
becomer
|
c47fb0a66e
|
修复 cookie 过期且自动续期失败时不提示重新登录
ci/woodpecker/tag/build Pipeline was successful
手机号登录(无密码)账号 cookie 过期后自动续期失败时,错误链
只带普通错误、不带 needReLogin 信号,导致前端只显示普通错误、
不弹「重新登录」引导,用户无法跳转登录页。
- 新增 ErrNeedReLogin 哨兵 + needReLoginSignal() 统一判定:
密码错误或无法自动续期都视为需要重新登录
- refreshCpdailyCookie 无密码/MFA 无法自动续期时返回 ErrNeedReLogin
- 所有执行路径(签到/请假/销假/返校/工作日志/表单/上传/预览)
的续期失败分支统一升级为 wrapNeedReLogin,SSE task_error 与
HTTP 401 均按 needReLoginSignal 判定
v1.2.146
|
2026-10-14 22:09:33 +08:00 |
|
becomer
|
2a989a15ef
|
购买 CDK 入口改为原生锚点链接:修复手机浏览器 window.open 被强制当前页打开
ci/woodpecker/tag/build Pipeline was successful
手机浏览器(UC/夸克等)把程序化 window.open 当弹窗强制降级为当前页跳转,
换成 <a target=_blank rel=noopener> 走浏览器原生新标签页处理。
微信 WebView 无 tab 概念,仍为当前页打开(平台限制)。
v1.2.145
|
2026-10-14 11:05:05 +08:00 |
|
becomer
|
150cd86a43
|
「我的」页新增购买 CDK 显眼入口:后台可配跳转链接与显隐,新标签页打开
ci/woodpecker/tag/build Pipeline was successful
- 用户端:VIP 卡下方展示渐变购买按钮(含 HOT 角标与光泽动画),
点击新标签页打开后台配置的链接;开关关闭或链接为空时不渲染
- 后台:系统设置新增「CDK 购买入口」卡(滑动开关 + 链接输入),
链接校验 http(s) 协议,开关值归一化 true/false
- 配置经既有 /cdk/vip-info 接口下发,零新增请求
v1.2.144
|
2026-10-14 10:50:00 +08:00 |
|
becomer
|
43ca4df9eb
|
拍照签到执行时重签过期照片 URL:用对象 key 换新鲜签名,无需用户重传
任务里存的 signPhotoUrl 是云端签发的短时效预览 URL(约 5 分钟),
隔日执行时签名早已过期,直接提交被学校拒为「签到失败」。
执行时先从 URL 提取稳定对象 key 重签新鲜 URL(同一张照片),
重签失败回退原 URL 不阻断。
|
2026-10-14 10:49:50 +08:00 |
|
becomer
|
1724a37bcf
|
修复部分浏览器销假/请假上传图片「选完无任何反应」
ci/woodpecker/tag/build Pipeline was successful
用户反馈:销假时选完图片后无转圈、无预览、无报错,服务端也收不到
upload-policy 请求(访问日志可证)。
原实现用游离的 input(createElement 后未挂 DOM)且 accept 限定
image/png,image/jpeg,image/jpg;在部分第三方内核(如 iPhone 夸克)
下,游离 input 不触发 onchange,iOS 相册的 HEIC 等格式也会被判为
不匹配,导致 input.files 为空 → 命中 `if (!file) return` 静默返回。
抽出 pickImageFiles() 统一修复:
- accept 改 image/*(与首页上传一致);
- input 挂到 DOM,用完移除;
- 监听 cancel 区分「用户取消」(静默)与「选了但读不到文件」(可见提示);
- 销假图片、请假附件两处共用,并修正 uploading 标志在提前 return 之后才置位的问题。
v1.2.143
|
2026-10-08 23:02:43 +08:00 |
|
becomer
|
d56769b265
|
修复手机号登录账号无法自动续期:RefreshCasCookie 未使用 CASTGC
ci/woodpecker/tag/build Pipeline was successful
手机号验证码登录的账号没有 raw_password,cookie 过期后只能靠长期凭证
续期。但 RefreshCasCookie 只发 AmpCookies 头、未把长期凭证 CASTGC(TGC)
预置进客户端,且不跟随 CAS 重定向,导致请求被 302 到登录页,续期必然失败
(表现为「解析续期响应失败: unexpected end of JSON input」),用户被误判
为登录失效。
对齐 ExchangeCasCookie:预置 CASTGC/sessionToken/tenantId 等 cookie,并
跟随 CAS 重定向(跨跳保留原请求头),使网关能换回 MOD_AUTH_CAS。
新增 sms_login_test.go:以 mock 网关覆盖「带 CASTGC 续期成功」与
「缺 sessionToken 直接报错」;旧代码下前者复现同一失败,修复后通过。
v1.2.142
|
2026-10-08 14:34:13 +08:00 |
|
becomer
|
7f39b39643
|
续假在后台执行日志中单独归类(新增 action=extend)
ci/woodpecker/tag/build Pipeline was successful
现象:后台用户管理「当天执行总次数」明细里,续假与请假同显示为「请假」。
根因:续假与新建请假都写 action='apply',后台 UNION SQL 的
CASE action WHEN 'apply' THEN 'leave_apply' 因此把两者归一。
改动:
- 模型新增 LeaveActionExtend="extend"
- 请假/续假写日志按 req.IsExtend 分流:续假记 extend,请假仍记 apply
- 后台 UNION SQL 映射 extend → leave_extend
- 前端执行日志类型标签新增「续假」(紫色徽标)
说明:存量历史的续假记录已写入 apply,无原始标记可回溯,不做数据迁移。
go build/vet/test 与前端 vue-tsc/build 通过。
v1.2.141
|
2026-10-08 10:39:01 +08:00 |
|
becomer
|
4f6095f6ba
|
修复旧套续假报「请假定位参数错误」:坐标改放提交体顶层
ci/woodpecker/tag/build Pipeline was successful
根因:旧套续假走 /leave/saveExtend,官方提交体把定位放在**顶层**
address/latitude/longitude(由前端 locationData 展开),不使用 location 子对象。
此前续假表单直接复用请假的 location:{address,latitude,longitude} 结构,
服务端找不到顶层坐标,返回「请假定位参数错误」。
修复:BuildLeaveExtendForm 删除 location 子对象,改用顶层 address/latitude/longitude
(+ locationType),其余沿用请假体。依据:官方 home.*.js 中 saveExtend 请求体构造
({...model, address, latitude, longitude, locationType, leaveId, extendReason, ...})。
新增单测覆盖:顶层坐标存在、无 location 子对象、leaveId/extendReason 正确。
go build/vet/test 通过。
v1.2.140
|
2026-10-08 10:24:37 +08:00 |
|
becomer
|
1fc54efad8
|
修复旧套(老版请假)续假列表为空:可续假判定改查详情 isAllowedExtend + 状态
ci/woodpecker/tag/build Pipeline was successful
现象:云南大学用户点续假显示「没有可以续假的请假单」,但其假单实际正在休假中。
根因:旧套可续假判定此前取列表行的 isAllowedExtend——该字段是新套的,
旧套列表行并不返回,导致 CanExtend 恒为 false,续假列表被前端过滤为空。
修复:
- 旧套详情 detailOther 顶层 isAllowedExtend 才是可续假开关(实测:在假期中的单为 true,
已完成的单为 false),FetchLeaveDetail 解析并暴露该字段
- 列表改为查详情后经 leaveOldCanExtend(status, isAllowedExtend) 判定:
状态在假期中(2 处理中/5 即将休假/6 正在休假中)且 isAllowedExtend=true
验证:该用户假单(status=6 正在休假中)现识别为 canExtend=true。
构建/go vet/test 通过;未做提交测试。
v1.2.139
|
2026-10-08 09:45:44 +08:00 |
|
becomer
|
13faba27b6
|
修复请假自定义选项字段值类型:提交 optionSeq 数字而非选项文本
ci/woodpecker/tag/build Pipeline was successful
现象:请假成功但查看该单详情报 For input string: "是"(服务端返回 code -1),
且列表接口随之整体报错。
根因:自定义附加字段的选项值应提交 optionSeq(数字),此前提交 optionContent("是")。
服务端把文本存进数字字段,导致该单详情(及含它的列表)解析崩溃。
证据:官方 ReApply 分包提交前把服务端的 optionContent 反查成 optionSeq 再提交;
提交体 fieldValue 为数字。
改动:前端选项字段提交 Number(optionSeq);文本字段仍提交字符串。
验证:用该校真实类型配置在浏览器核对,提交体为 {"fieldValue":1,...}。
注:已被坏数据写入的那张单需要单独处理(见后续说明)。
v1.2.138
|
2026-10-07 19:01:13 +08:00 |
|
becomer
|
46a4bd3b80
|
请假附加信息:selfDefineAnswers 补 fieldType,与官方前端一致
ci/woodpecker/tag/build Pipeline was successful
官方 Apply 分包构造:selfDefineAnswers = selfDefineFields.map(c => ({fieldWid, fieldName, fieldValue, fieldType}))。
此前漏了 fieldType,补上以降低服务端校验拒绝风险。构建/vue-tsc 通过。
v1.2.137
|
2026-10-07 18:42:02 +08:00 |
|
becomer
|
248e28858b
|
修复新套请假「请检查附加信息是否全部填写正确」
ci/woodpecker/tag/build Pipeline was successful
根因(用该校真实类型配置核对):typeWid=1873「离校请假」详情里
- isRequiredEvidenceFile=true(证明材料必填),但类型列表接口返回 false,
我们的映射只看了 isVisibledEvidenceFile,未透传必填开关 → 漏检附件
- selfDefineFields 有必填自定义字段「当天是否返校」(fieldWid=2422),
我们完全未采集,提交时 selfDefineAnswers 恒为 [] → 服务端拒绝
改动:
- LeaveType 增 requireEvidence/evidenceMin/selfDefineFields 并映射(取详情字段)
- LeaveNewFormDetail 暴露 selfDefineFields;提交体 selfDefineAnswers 从请求透传
- ExecuteLeaveApplyRequest 增 selfDefineAnswers
- 前端:请假表单渲染自定义字段(单选/文本),必填校验(附加字段 + 证明材料最少张数),
附件标签按 requireEvidence 显示「必传」
- api/leave.ts 补 SelfDefineField/SelfDefineAnswer 类型
验证:用该校真实类型配置在浏览器核对,表单出现「当天是否返校」、附件标注「必传」。
未做提交(按用户要求)。构建/go vet/test/vue-tsc 通过。
v1.2.136
|
2026-10-07 18:40:37 +08:00 |
|
becomer
|
005ce14013
|
工作日志签到:改为选择校区,用该校区规定经纬度签到
ci/woodpecker/tag/build Pipeline was successful
按需求把签到项的「获取 GPS 定位」改为「选择校区」:用户选所在校区,直接使用该校区
规定的经纬度(学校按点位半径校验)。不再依赖设备定位。
- 后端透传签到范围点(地址/经纬度/半径)到 formItems;新增 WorklogScopePoints 复用
查寝的 SignPlace 解析与 coordsWithinSignRange 距离判定;执行时校验坐标是否在范围内,
不在则明确拒绝并提示(不伪造定位)
- 前端签到字段渲染校区下拉,选中即把该校区的 lat,lng 写入字段 content
- 移除 GPS 定位按钮与相关函数
验证:用真实范围数据(许东/奶槐等校区)在浏览器核对,下拉正确列出校区,选择后
显示"已选:某某校区"并写入坐标。构建/go vet/test/vue-tsc 均通过。
v1.2.135
|
2026-10-06 19:44:24 +08:00 |
|
becomer
|
d2d4847171
|
工作日志签到项:编辑页提供「获取当前位置」,坐标取真实 GPS
ci/woodpecker/tag/build Pipeline was successful
实测确认真实字段定义:值班签到 fieldType=5、signCondition=true,
templateScopes 给出 4 个校区点位(含经纬度与半径 500/600m)——即学校按点位半径校验,
与查寝同理,需真实定位;signScopeWids 是允许签到的范围点位 id(非坐标)。
- 前端:isWorklogSign 字段渲染「获取当前位置」按钮,调用 navigator.geolocation
取真实经纬度写入该字段 content("lat,lng"),已定位显示回显与「重新定位」
- 后端:执行时从签到字段 content 解析坐标传 submitSign;缺定位时提示先获取定位
- FormItem 类型补 isWorklogSign/signScopeWids
验证:用任务 315 真实数据在浏览器核对,签到字段正确渲染为定位按钮,其余字段正常。
构建/go vet/test/vue-tsc 均通过。未做真实提交。
v1.2.134
|
2026-10-06 19:37:00 +08:00 |
|
becomer
|
22e84f86a4
|
工作日志签到项修正:按 signCondition 判定,坐标取用户定位
ci/woodpecker/tag/build Pipeline was successful
用教师账号查真实 detail 字段定义,纠正两处误判:
- 「值班签到」(fieldType=5) 的 signCondition=true 且 signScopeWids 非空,
确属签到项(此前误判为普通时间字段而移除了标记)
- signScopeWids 是「允许签到的范围点位 id」,不是坐标;submitSign 应传用户定位
(与签到任务一致:取表单 lon/lat 字段),此前误用 templateScopes 取坐标
改动:
- WorklogField 增 SignCondition;formItems 透传 signCondition/signScopeWids
- 执行逻辑改按 SignCondition 判定签到项;坐标取表单 lon/lat;缺定位时给出明确
提示(请先在编辑中填写定位),不再静默跳过
- 移除 worklogScopePoint(用范围点当坐标的错误实现)
未验证:签到项在编辑页的定位填写入口尚未在前端提供,端到端未实测。
v1.2.133
|
2026-10-06 19:16:51 +08:00 |
|
becomer
|
7fb15d17c8
|
修复工作日志字段在打卡页不渲染,导致必填校验报「缺少值班时间/记录/签到」
ci/woodpecker/tag/build Pipeline was successful
现象:教师账号执行「学管值班」报「签到信息未配置完整,缺少:值班时间、值班记录、值班签到」。
根因:打卡页表单渲染只覆盖 type 0/1(文本)、2/3(单选多选)、图片、二维码;
工作日志用到的 type 5(日期时间)、7(日期时间)、9(多行文本)没有任何 v-else-if 匹配,
字段根本不渲染,用户填不了 → 保存为空 → 执行前必填校验拦截。
修复:
- 前端加三类渲染分支(5/7 → datetime-local 输入;9 → textarea),并加兜底分支
(未识别类型按文本渲染),避免今后再出现「字段静默丢弃」;内置 toLocalInput 回显
- 模型枚举补 ItemTypeDate(5)/ItemTypeDateTime(7)/ItemTypeTextarea(9)
- 去掉误导性的 isWorklogSign 标记:实测「值班签到」signScopeWids 为空、是 type 5 普通
可填字段,并非定位签到项(此前照第三方实现误判)
验证:用任务 315 真实 formData 在浏览器核对,编辑弹窗正确渲染全部 6 字段
(2 个 datetime + 1 个 textarea + 单选)。构建/vue-tsc 通过。
未验证:该 type 5「值班签到」的真实填写要求(时间点还是留空由后端处理)待真实使用确认。
v1.2.132
|
2026-10-06 19:14:57 +08:00 |
|