fix(server): SYB 会话到期前提前续登,同步预检遇到未登录时重新登录并重试一次 #371

Open
opened 2026-10-09 09:47:51 +08:00 by ila · 0 comments
Owner

基本信息

  • 类型:缺陷(服务端 SYB 会话:到期边界同步失败、同步预检遇到未登录不重新登录)。
  • 状态:待用户确认方案与实施授权;本次只建单。
  • 来源:2026-10-09 用户要求分析「syb 同步在 09:05 时的失败原因,为什么登录无效不会重新登录」。Claude Code 分析后给出 3 条修法,用户选定:「1和2,按照你的建议建工单」(第 3 条「收到未登录时删除会话」只在第 2 条的范围内使用,不单独做)。
  • 关联:#48(SYB 登录验证码 OCR)、#330(订单回写共用会话获取)。

已核验事实

线上数据(只读查询,2026-10-09)

同步 来源 开始 耗时 结果
1107 系统定时同步 10-09 09:05:00.003 1.7 秒 failed:「查询 2026-10-09 货运单总数失败: 顺运宝接口 /am/stock/listTotal: 用户未登录: 顺运宝会话未登录或已过期」
1108 管理员手动 10-09 09:36:49 — 正常拉取;syb_session.updated_at = 09:36:51.396,expires_at = 10-10 09:36:51.000(取 JWT exp,精确到秒),说明此次重新登录成功
  • 近 3 天同步记录中,「未登录」只出现在 1107,其余失败都是网络超时。
  • 定时同步每小时 xx:05:00 开始。

代码事实(origin/main e450ac28989de6827328b411e4bdc050dded9ab3)

  • sybclient/client.go Login:ExpiresAt = min(JWT exp, now+24h),没有提前量。docs/12 §3.3:会话正好 24 小时,不滚动续期。
  • sybclient/session.go Load:now >= ExpiresAt 才返回 ErrNoSession。
  • sybclient/session_manager.go AcquireSession:只在开始时判断:缓存未过期,并且 CheckSession(GET /am/user/get)有效,就直接复用。
  • sybimport/sync.go SyncWithShopSnapshot:先对每个日期调用 ListTotal 做预检(不写任何数据),预检失败直接返回;注释写明「a session that dies mid-run surfaces as ErrSessionInvalid and is treated like any other mid-run failure」,没有重新登录后重试。
  • sybimport/import_handler.go runImport:Connect → SyncWithShopSnapshot,出错即 failed,没有重试。
  • SessionStore.Delete 在生产代码中没有调用方;docs/12 §3.5 写「判定未登录,清会话」,但实际没有清除。
  • AcquireSession 的其他调用方:订单回写(purchase/order_writeback_worker.go)、入库码(sybinnercode/runtime.go 经 sybimport.Connect)、店铺发现(Discover)。

推断(证据较强,未看服务日志,未完全证实)

1107 正好在会话失效前一两秒开始:本地缓存未过期,CheckSession 也有效;约 1 秒后 ListTotal 时会话到期,顺运宝返回「用户未登录」。

推测前一天 09:05 的同步(1083)在开始时重新登录过,所以会话第二天同一时刻失效。

整点定时 + 24 小时硬上限 + 没有提前量,会让这种情况在某些小时反复出现:只要某次 xx:05 的同步刚好重新登录,第二天同一时刻就可能撞上。

目标

  1. 会话快到期时提前重新登录,不在最后几分钟里继续使用。
  2. 同步预检(尚未写任何数据)遇到明确的「未登录」时,重新登录并重试一次,而不是直接失败。

非目标

  • 不处理拉取明细途中(已写入数据之后)的会话失效:仍按现有逻辑失败或部分成功。
  • 不改 CheckSession 判定规则、网络故障与未登录的区分(§3.5)、OCR 登录流程与登录租约。
  • 不处理同步每小时卡在同一页超时的问题(如 10-08 第 15 页、10-07 第 21 页),另行分析。
  • 不改数据库结构、API 和前端。

方案

  1. 提前续登(sybclient)
    • 增加常量提前量(暂定 10 分钟):判断缓存会话是否可用时,用 now + 提前量 和 ExpiresAt 比较;剩余时间不足提前量就当作已过期,走现有的租约 + OCR 登录流程。
    • 只改「是否复用缓存」的判断;Load 的现有语义和测试可以保留,改为调用方传入 time.Now().Add(margin),或新增带提前量的方法,由实施时选最小改法。
    • 登录成功后的回读校验(validSessionOrError)必须对新会话成立,不能因为提前量导致刚登录的会话被判定不可用。
    • 等待其他进程登录的分支(validCachedSession)使用同一个提前量,避免等到即将过期的旧会话。
    • 影响:订单回写、入库码、店铺发现共用这条路径,同样受益;验证码请求次数基本不变(每次登录只是提前约 10 分钟)。
  2. 预检遇到未登录时重登重试一次(sybimport)
    • 只在预检阶段(ListTotal,尚未写任何数据)返回 errors.Is(err, sybclient.ErrSessionInvalid) 时触发;用可区分的错误类型标明「预检阶段」,不靠匹配错误文字判断。
    • 处理:删除该账号的缓存会话(SessionStore.Delete,这里符合 §3.5「明确未登录才清会话」),再走 Connect 重新登录,然后重新执行同一次同步。最多重试一次;第二次仍失败就按原逻辑记失败,错误信息保留两次的原因(不含 Cookie、Token、密码)。
    • 同一个 syb_sync_run 记录内完成,不新建同步记录,进度从 0 重新开始。
    • 网络超时、5xx、格式错误不触发;拉取明细阶段的未登录不触发。
    • 登录失败(OCR 识别失败等)按现有逻辑报错,不再重试。

子项目影响、依赖与并行

  • Server:sybclient/session_manager.go(及 session.go,如需要)、sybimport/sync.go、sybimport/import_handler.go,以及对应测试。
  • Web/Android:无。
  • 数据库、API:无变化。
  • 无前置依赖,可并行。

设计证据

非 UI 改动,无需原型;以本正文为准。

验收标准

  • 单元测试:缓存会话剩余时间少于提前量时,AcquireSession 走登录流程;剩余时间充足时复用缓存,不请求验证码。
  • 单元测试:刚登录的会话通过回读校验;等待他人登录的分支不会接受即将过期的会话。
  • 单元测试(模拟顺运宝):预检 ListTotal 首次返回「未登录」→ 删除缓存 → 重新登录 → 第二次成功,同步结果为成功,只登录一次。
  • 单元测试:预检连续两次未登录 → 只重试一次,最终 failed,错误信息含原因且不含凭据。
  • 单元测试:预检网络超时、5xx 不触发重登;拉取明细阶段的未登录不触发重登,仍按现有逻辑结束。
  • sybclient、sybimport、purchase(订单回写)、sybinnercode 相关回归测试通过;服务端构建通过,如实记录。
  • 发布后观察:定时同步不再出现「查询货运单总数失败:用户未登录」;只读查询 syb_sync_run 与 syb_session 确认。发布需用户另行授权。

风险与回退

  • 提前量太大会增加登录次数和验证码请求;10 分钟只让每次会话少用约 0.7%,影响很小。
  • 删除缓存会话时,其他进程(订单回写、入库码)正在使用同一会话,它们会在下一次请求时得到未登录,并通过现有的 AcquireSession 和登录租约重新获取,不会重复请求验证码。
  • 推断未完全证实:如果真实原因是账号在别处登录导致会话被挤掉,第 2 条同样能恢复,第 1 条无效但无害。
  • 回退:去掉提前量和预检重试,恢复原逻辑;无数据迁移。

文档影响

  • 更新 Wiki「SYB ERP 接口」(docs/12):§3.4 增加「到期前提前续登」;§3.5 写明当前只在预检阶段遇到未登录时清除会话并重登一次。
  • 更新 Troubleshooting:同步报「用户未登录」时的排查方法和自动恢复行为。
  • 先改线上 Wiki 并回读 revision,再执行一次 sync 与 sync --check。

备注

本单由 Claude Code 依据线上只读查询和代码阅读起草;不含账号、密码、Cookie 或 Token。

## 基本信息 - 类型:缺陷(服务端 SYB 会话:到期边界同步失败、同步预检遇到未登录不重新登录)。 - 状态:**待用户确认方案与实施授权**;本次只建单。 - 来源:2026-10-09 用户要求分析「syb 同步在 09:05 时的失败原因,为什么登录无效不会重新登录」。Claude Code 分析后给出 3 条修法,用户选定:「1和2,按照你的建议建工单」(第 3 条「收到未登录时删除会话」只在第 2 条的范围内使用,不单独做)。 - 关联:#48(SYB 登录验证码 OCR)、#330(订单回写共用会话获取)。 ## 已核验事实 ### 线上数据(只读查询,2026-10-09) | 同步 | 来源 | 开始 | 耗时 | 结果 | |---|---|---|---|---| | 1107 | 系统定时同步 | 10-09 09:05:00.003 | 1.7 秒 | `failed`:「查询 2026-10-09 货运单总数失败: 顺运宝接口 /am/stock/listTotal: 用户未登录: 顺运宝会话未登录或已过期」 | | 1108 | 管理员手动 | 10-09 09:36:49 | — | 正常拉取;`syb_session.updated_at` = 09:36:51.396,`expires_at` = 10-10 09:36:51.000(取 JWT exp,精确到秒),说明此次重新登录成功 | - 近 3 天同步记录中,「未登录」只出现在 1107,其余失败都是网络超时。 - 定时同步每小时 xx:05:00 开始。 ### 代码事实(`origin/main` `e450ac28989de6827328b411e4bdc050dded9ab3`) - `sybclient/client.go` `Login`:`ExpiresAt = min(JWT exp, now+24h)`,没有提前量。docs/12 §3.3:会话正好 24 小时,不滚动续期。 - `sybclient/session.go` `Load`:`now >= ExpiresAt` 才返回 `ErrNoSession`。 - `sybclient/session_manager.go` `AcquireSession`:只在开始时判断:缓存未过期,并且 `CheckSession`(`GET /am/user/get`)有效,就直接复用。 - `sybimport/sync.go` `SyncWithShopSnapshot`:先对每个日期调用 `ListTotal` 做预检(不写任何数据),预检失败直接返回;注释写明「a session that dies mid-run surfaces as ErrSessionInvalid and is treated like any other mid-run failure」,没有重新登录后重试。 - `sybimport/import_handler.go` `runImport`:`Connect` → `SyncWithShopSnapshot`,出错即 `failed`,没有重试。 - `SessionStore.Delete` 在生产代码中没有调用方;docs/12 §3.5 写「判定未登录,清会话」,但实际没有清除。 - `AcquireSession` 的其他调用方:订单回写(`purchase/order_writeback_worker.go`)、入库码(`sybinnercode/runtime.go` 经 `sybimport.Connect`)、店铺发现(`Discover`)。 ### 推断(证据较强,未看服务日志,未完全证实) 1107 正好在会话失效前一两秒开始:本地缓存未过期,`CheckSession` 也有效;约 1 秒后 `ListTotal` 时会话到期,顺运宝返回「用户未登录」。 推测前一天 09:05 的同步(1083)在开始时重新登录过,所以会话第二天同一时刻失效。 整点定时 + 24 小时硬上限 + 没有提前量,会让这种情况在某些小时反复出现:只要某次 xx:05 的同步刚好重新登录,第二天同一时刻就可能撞上。 ## 目标 1. 会话快到期时提前重新登录,不在最后几分钟里继续使用。 2. 同步预检(尚未写任何数据)遇到明确的「未登录」时,重新登录并重试一次,而不是直接失败。 ## 非目标 - 不处理拉取明细途中(已写入数据之后)的会话失效:仍按现有逻辑失败或部分成功。 - 不改 `CheckSession` 判定规则、网络故障与未登录的区分(§3.5)、OCR 登录流程与登录租约。 - 不处理同步每小时卡在同一页超时的问题(如 10-08 第 15 页、10-07 第 21 页),另行分析。 - 不改数据库结构、API 和前端。 ## 方案 1. **提前续登(`sybclient`)** - 增加常量提前量(暂定 10 分钟):判断缓存会话是否可用时,用 `now + 提前量` 和 `ExpiresAt` 比较;剩余时间不足提前量就当作已过期,走现有的租约 + OCR 登录流程。 - 只改「是否复用缓存」的判断;`Load` 的现有语义和测试可以保留,改为调用方传入 `time.Now().Add(margin)`,或新增带提前量的方法,由实施时选最小改法。 - 登录成功后的回读校验(`validSessionOrError`)必须对新会话成立,不能因为提前量导致刚登录的会话被判定不可用。 - 等待其他进程登录的分支(`validCachedSession`)使用同一个提前量,避免等到即将过期的旧会话。 - 影响:订单回写、入库码、店铺发现共用这条路径,同样受益;验证码请求次数基本不变(每次登录只是提前约 10 分钟)。 2. **预检遇到未登录时重登重试一次(`sybimport`)** - 只在预检阶段(`ListTotal`,尚未写任何数据)返回 `errors.Is(err, sybclient.ErrSessionInvalid)` 时触发;用可区分的错误类型标明「预检阶段」,不靠匹配错误文字判断。 - 处理:删除该账号的缓存会话(`SessionStore.Delete`,这里符合 §3.5「明确未登录才清会话」),再走 `Connect` 重新登录,然后重新执行同一次同步。**最多重试一次**;第二次仍失败就按原逻辑记失败,错误信息保留两次的原因(不含 Cookie、Token、密码)。 - 同一个 `syb_sync_run` 记录内完成,不新建同步记录,进度从 0 重新开始。 - 网络超时、5xx、格式错误不触发;拉取明细阶段的未登录不触发。 - 登录失败(OCR 识别失败等)按现有逻辑报错,不再重试。 ## 子项目影响、依赖与并行 - Server:`sybclient/session_manager.go`(及 `session.go`,如需要)、`sybimport/sync.go`、`sybimport/import_handler.go`,以及对应测试。 - Web/Android:无。 - 数据库、API:无变化。 - 无前置依赖,可并行。 ## 设计证据 非 UI 改动,无需原型;以本正文为准。 ## 验收标准 - [ ] 单元测试:缓存会话剩余时间少于提前量时,`AcquireSession` 走登录流程;剩余时间充足时复用缓存,不请求验证码。 - [ ] 单元测试:刚登录的会话通过回读校验;等待他人登录的分支不会接受即将过期的会话。 - [ ] 单元测试(模拟顺运宝):预检 `ListTotal` 首次返回「未登录」→ 删除缓存 → 重新登录 → 第二次成功,同步结果为成功,只登录一次。 - [ ] 单元测试:预检连续两次未登录 → 只重试一次,最终 `failed`,错误信息含原因且不含凭据。 - [ ] 单元测试:预检网络超时、5xx 不触发重登;拉取明细阶段的未登录不触发重登,仍按现有逻辑结束。 - [ ] `sybclient`、`sybimport`、`purchase`(订单回写)、`sybinnercode` 相关回归测试通过;服务端构建通过,如实记录。 - [ ] 发布后观察:定时同步不再出现「查询货运单总数失败:用户未登录」;只读查询 `syb_sync_run` 与 `syb_session` 确认。发布需用户另行授权。 ## 风险与回退 - 提前量太大会增加登录次数和验证码请求;10 分钟只让每次会话少用约 0.7%,影响很小。 - 删除缓存会话时,其他进程(订单回写、入库码)正在使用同一会话,它们会在下一次请求时得到未登录,并通过现有的 `AcquireSession` 和登录租约重新获取,不会重复请求验证码。 - 推断未完全证实:如果真实原因是账号在别处登录导致会话被挤掉,第 2 条同样能恢复,第 1 条无效但无害。 - 回退:去掉提前量和预检重试,恢复原逻辑;无数据迁移。 ## 文档影响 - 更新 Wiki「SYB ERP 接口」(docs/12):§3.4 增加「到期前提前续登」;§3.5 写明当前只在预检阶段遇到未登录时清除会话并重登一次。 - 更新 Troubleshooting:同步报「用户未登录」时的排查方法和自动恢复行为。 - 先改线上 Wiki 并回读 revision,再执行一次 sync 与 sync --check。 ## 备注 本单由 Claude Code 依据线上只读查询和代码阅读起草;不含账号、密码、Cookie 或 Token。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#371