模板项目 cmautobuy/admin 已有一套经抓包还原、带 31 个 httptest 用例的顺云宝 ERP 客户端(syb/client.go 854 行、syb/ocr.go、docs/admin/08-顺运宝接口.md)。#41 当初留下的「没有导入端点」缺口,可以靠移植这套代码补上,不必从零实现。
cmautobuy/admin
syb/client.go
syb/ocr.go
docs/admin/08-顺运宝接口.md
来源代码与本仓库同属一个负责人,可直接复用;但两边数据层不同(对方是 database/sql 手写 SQL,本仓库是 gorm),编排层需要重写。
database/sql
status === true
data
/am/stock/list
total
/am/stock/detail/listByStock
用户已明确:SYB 登录验证码必须走线上 OCR 服务。原永久规则「不使用 OCR/VLM」是针对 Android Agent 端采集写的,本工单需要按端重新划定边界:
需要同步修改 AGENTS.md 与 docs/03-business-rules-and-glossary.md(Wiki 优先)。
AGENTS.md
docs/03-business-rules-and-glossary.md
待确认:验证码图片会被发送到外部 OCR 服务,这一点要在文档中显式记录。
server/app/goauto/sybclient/
LoginWithOCR
CheckSession
ListPage
ListTotal
ListByOrderNumber
DetailListByStock
syb_session
config.yaml
base_url
username
password
page_size
max_matches
ocr_url
config.example.yaml
POST /api/admin/v1/syb-products/import
sybimport.ApplyDetail
提交 a31c477。
a31c477
sybclient/session.go
models.SYBSession
config/extend.go
config/settings.yml
SYB-ERP-Interface-Contract
docs/12-syb-erp-interface.md
Business-Rules-and-Glossary
Architecture-and-Code-Map
b4861f5
上游的 client.go / columns.go / ocr.go 只依赖标准库,不碰数据库也不认识 *gin.Context,所以移植几乎是原样搬:只改了包名和文档引用。上游 31 个 httptest 用例一并搬入,第一次运行即全部通过,没有改动任何断言。
client.go
columns.go
ocr.go
*gin.Context
上游把账号密码放 config.yaml(gitignore)。GoAuto 已有 ApplyEnvironment() 的现成模式,改成:
ApplyEnvironment()
baseurl
pagesize
maxmatches
ocrurl
ocrmaxattempts
settings.yml
GOAUTO_SYB_USERNAME
GOAUTO_SYB_PASSWORD
顺带避开了上游踩过的 YAML 坑(纯数字密码不加引号会被解析成整数、前导零丢失)。密码读取时不做 trim,首尾空白可能是密码的一部分。
go build ./...
go vet ./app/goauto/... ./config/...
go test ./...
sybclient
config
httptest
两个守卫测试做了变异验证,确认不是空转的断言:
SYBSession.CookiesJSON
json:"-"
TestCookiesJSONIsNotSerialisedOverHTTP
TestSYBSettingsInRepoBindToExtendStruct
extend.syb
DeleteInnerCode
CreateInnerCodeDetail
UpdateDetailCode
4886e0d
用户提供凭据后,已对顺云宝真站跑通验证。测试文件带 syblive 构建标签,默认不参与 go test ./...;凭据只走环境变量,文件里不含凭据;只调读接口,输出只有结构和计数,不打印货运单号、标题和金额。
syblive
--- PASS: TestLiveLoginAndSession (6.33s) --- PASS: TestLiveListPagingContract (5.12s) --- PASS: TestLiveDetailShape (5.17s)
ExpiresAt
listTotal
list.total
productPrice
details[].id 的唯一性此前记录为「未验证」,而 #41 拿它当业务唯一键的一半。本批 28 个 id 互不重复。
details[].id
[待定] 这只证明了同一批内唯一,没有证明全局唯一。#41 的唯一键是 (order_code, detail_id) 复合键,不依赖 detail_id 单独全局唯一,所以当前设计是安全的。
[待定]
(order_code, detail_id)
detail_id
最近 7 天就有 7404 张货运单。 配置里的 max_matches: 10000 大约 9~10 天就会触顶。Stage 2 的同步编排必须:
max_matches: 10000
CheckSession(ctx, userID, username) 必须传登录返回的真实 user id。传 0 时顺云宝返回 msg=品名不能为空 code=-100。
CheckSession(ctx, userID, username)
msg=品名不能为空 code=-100
值得记一笔的是:客户端没有把这个业务错误误判成「未登录」,正是 §3.5 要求的行为——只有明确的未登录信号才包成 ErrSessionInvalid,其余一律是别的错误。这条防御在真实数据上生效了。
ErrSessionInvalid
用户提供的 MySQL(185.216.248.75:3307,TLS verify_ca)只读探测结果:
185.216.248.75:3307
verify_ca
autobuy autobuy_contract_test information_schema performance_schema
没有 goauto 库——这台是 cmautobuy 的库,不是 GoAuto 的。GoAuto 配置指向 127.0.0.1:3307/goauto,是另一台(Windows 本机)。
goauto
127.0.0.1:3307/goauto
所以 syb_session 表的迁移验证仍未完成,等用户确认在哪个库上做。未经确认不会在对方生产库上建 GoAuto 的表。
7edb4e7
从 SYB 接口到入库的整条路径打通了。
sybimport/sync.go
Sync
Connect
sybimport/import_handler.go
web/src/views/goauto/syb-products/index.vue
分页由 listTotal 驱动,不用 list 响应里的 total。 实测确认后者是当前页行数,用它当总数循环会停在第一页。这条写进了代码注释和测试。
list
完整性证明不了就停。 缺行的分页、分页期间总数漂移、明细响应少返回货运单,三种情况都立即停止并报错。已写入的数据保留——ApplyDetail 对 (order_code, detail_id) 幂等,重跑覆盖。不能把跑了一半的同步报成成功,否则漏掉的单永远补不回来。
ApplyDetail
会话缓存必须连 user id 一起存。 这是实现过程中发现的真问题:CheckSession 调 /am/user/get?id=,id 不对时顺云宝返回的是业务错误(msg=品名不能为空)而不是「未登录」。只存 Cookie 的话恢复出来的会话永远校验不过,缓存等于白做,每次都要重新登录。已给 models.SYBSession 加 UserID 列。
/am/user/get?id=
msg=品名不能为空
UserID
导入端点单飞(single-flight)且脱离请求上下文。 两个并发导入会在同一批行上互相覆盖,数据没事但两份报告都会少算;操作员关掉标签页也不该让跑了一半的范围中断。
页面在范围过大时警告。 实测一周 7000+ 张,默认范围给 3 天。
go vet
sybimport
eslint
TestSyncStopsWhenTotalDriftsDuringPaging
一个值得记的插曲:写假服务端时我把请求体字段写成了 pageSize,真实字段是 length,测试立刻以 panic 暴露了这个错。
pageSize
length
user_id
vite build
last_synced_at
提交 4f7f3a6。用真实凭据对顺云宝真站跑了一次完整导入(落库到 SQLite,不是 MySQL,原因见下)。
4f7f3a6
① 登录成功,会话已写入 ② 首次导入 2026-08-19:货运单 986 张,明细 1524 条,新增 1524,覆盖 0 ③ 落库:syb_product 1524 行,shopee_product 1056 行 解析 success 1395 / uncertain 125 / failed 4 ④ 会话复用成功,未重新登录(第二次 Connect 走缓存,没有再过验证码)
解析质量:1485/1524(97.4%)拿到了完整的颜色+尺码。
productSpec
4 条 failed 都是源数据本身为空——按规则标记失败而不是伪造缺失字段,符合预期。125 条 uncertain 进入人工复核路径。
models.SYBSession 漏了 TableName(),gorm 悄悄建成了 syb_sessions(复数),而仓库里其它模型全部显式声明单数表名。
TableName()
syb_sessions
这个缺陷的麻烦之处在于迁移照样成功——不报错,直到有人按约定的名字查询才会发现。是这次真实导入把它暴露出来的(syb_session 0 行)。发现得算及时:MySQL 上还没有跑过迁移,不用处理已经建错的表。
syb_session 0 行
已修,并且把 Migrate 的模型清单提取成 MigratedModels(),让新增的约定测试覆盖的正是 Migrate 真正创建的那批模型,而不是一份会漂移的手抄清单。变异验证:去掉 TableName() 后测试失败。
Migrate
MigratedModels()
WSL 里能连到 Windows 侧的 3307 端口,但被 MySQL 拒绝:
3307
Error 1130: Host '192.168.181.191' is not allowed to connect to this MySQL server
root 账号只授权了本机。这不是代码问题。两个办法二选一:
root
CREATE USER 'root'@'192.168.%'
采集→解析→落库→幂等这条链路已经在真实数据上验证过。唯一没验证的是 MySQL 建表——SQLite 和 MySQL 的 DDL 差异在这个项目里出过事(之前 image_url 的 TEXT DEFAULT 问题),所以这一条必须真跑过才能算数。
image_url
TEXT DEFAULT
用户于 2026-08-28 明确确认本工单验收通过。按项目流程记录验收结论并关闭工单;本次仅更新工单状态,无新增长期文档事实,不重复同步 Wiki。
No dependencies set.
The note is not visible to the blocked user.
背景
模板项目
cmautobuy/admin已有一套经抓包还原、带 31 个 httptest 用例的顺云宝 ERP 客户端(syb/client.go854 行、syb/ocr.go、docs/admin/08-顺运宝接口.md)。#41 当初留下的「没有导入端点」缺口,可以靠移植这套代码补上,不必从零实现。来源代码与本仓库同属一个负责人,可直接复用;但两边数据层不同(对方是
database/sql手写 SQL,本仓库是 gorm),编排层需要重写。已核对的关键接口结论(来自对方文档,非官方文档)
status === true,业务失败也返回 HTTP 200;data可能是裸整数。/am/stock/list响应里的total是当前页条数,不是筛选范围总数。/am/stock/detail/listByStock单批最多 100 个 id,超出由调用方分批。决策:登录验证码使用线上 OCR 服务
用户已明确:SYB 登录验证码必须走线上 OCR 服务。原永久规则「不使用 OCR/VLM」是针对 Android Agent 端采集写的,本工单需要按端重新划定边界:
需要同步修改
AGENTS.md与docs/03-business-rules-and-glossary.md(Wiki 优先)。待确认:验证码图片会被发送到外部 OCR 服务,这一点要在文档中显式记录。
交付拆分
Stage 1(不需要凭据,可独立验证)
syb/client.go至server/app/goauto/sybclient/:登录、LoginWithOCR、会话保活CheckSession、ListPage/ListTotal/ListByOrderNumber、DetailListByStock。syb/ocr.go验证码识别客户端。syb_session表)。config.yaml:base_url/username/password/page_size/max_matches/ocr_url。凭据只进config.yaml(已 gitignore),config.example.yaml只放占位符。Stage 2(需要真实凭据)
POST /api/admin/v1/syb-products/import,对接已有的sybimport.ApplyDetail。门禁
安全约束
Stage 1 完成,等待验收
提交
a31c477。做了什么
server/app/goauto/sybclient/sybclient/session.go+models.SYBSessionconfig/extend.go、config/settings.ymlSYB-ERP-Interface-Contract→docs/12-syb-erp-interface.mdAGENTS.md、Business-Rules-and-Glossary、Architecture-and-Code-Map(提交b4861f5)上游的
client.go/columns.go/ocr.go只依赖标准库,不碰数据库也不认识*gin.Context,所以移植几乎是原样搬:只改了包名和文档引用。上游 31 个 httptest 用例一并搬入,第一次运行即全部通过,没有改动任何断言。凭据处理与上游不同
上游把账号密码放
config.yaml(gitignore)。GoAuto 已有ApplyEnvironment()的现成模式,改成:baseurl/pagesize/maxmatches/ocrurl/ocrmaxattempts)进settings.ymlGOAUTO_SYB_USERNAME/GOAUTO_SYB_PASSWORD顺带避开了上游踩过的 YAML 坑(纯数字密码不加引号会被解析成整数、前导零丢失)。密码读取时不做 trim,首尾空白可能是密码的一部分。
验证结果
go build ./...通过go vet ./app/goauto/... ./config/...通过go test ./...全部通过;sybclient49 个用例,config新增 6 个httptest假服务端,没有打过顺云宝真实接口两个守卫测试做了变异验证,确认不是空转的断言:
SYBSession.CookiesJSON的json:"-"改回去 →TestCookiesJSONIsNotSerialisedOverHTTP失败settings.yml的pagesize改成page_size→TestSYBSettingsInRepoBindToExtendStruct失败(绑定静默失效正是这里最容易出的错)需要用户验证
syb_session表能建出来(本机 MySQL 在 Windows 侧,我连不上)settings.yml新增的extend.syb不影响现有配置加载明确没做
DeleteInnerCode/CreateInnerCodeDetail/UpdateDetailCode虽然随客户端一起搬了进来(它们是上游客户端的一部分),但本工单不调用它们;对应的是 #37。Stage 2 待办(需要凭据)
POST /api/admin/v1/syb-products/import,对接已有的sybimport.ApplyDetail真实网络验证通过(提交
4886e0d)用户提供凭据后,已对顺云宝真站跑通验证。测试文件带
syblive构建标签,默认不参与go test ./...;凭据只走环境变量,文件里不含凭据;只调读接口,输出只有结构和计数,不打印货运单号、标题和金额。用真实数据确认的结论
ExpiresAt= 24h0m0slistTotal是筛选总数list.total是当前页条数total= 20productPrice单位是元顺带关闭一个 #41 的已知缺口
details[].id的唯一性此前记录为「未验证」,而 #41 拿它当业务唯一键的一半。本批 28 个 id 互不重复。[待定]这只证明了同一批内唯一,没有证明全局唯一。#41 的唯一键是(order_code, detail_id)复合键,不依赖detail_id单独全局唯一,所以当前设计是安全的。一个影响 Stage 2 设计的实测数字
最近 7 天就有 7404 张货运单。 配置里的
max_matches: 10000大约 9~10 天就会触顶。Stage 2 的同步编排必须:list.total当总数——按实测它恒等于当前页行数,循环会停在第一页修正一处上游客户端的调用约定
CheckSession(ctx, userID, username)必须传登录返回的真实 user id。传 0 时顺云宝返回msg=品名不能为空 code=-100。值得记一笔的是:客户端没有把这个业务错误误判成「未登录」,正是 §3.5 要求的行为——只有明确的未登录信号才包成
ErrSessionInvalid,其余一律是别的错误。这条防御在真实数据上生效了。MySQL 侧的情况
用户提供的 MySQL(
185.216.248.75:3307,TLSverify_ca)只读探测结果:没有
goauto库——这台是 cmautobuy 的库,不是 GoAuto 的。GoAuto 配置指向127.0.0.1:3307/goauto,是另一台(Windows 本机)。所以
syb_session表的迁移验证仍未完成,等用户确认在哪个库上做。未经确认不会在对方生产库上建 GoAuto 的表。Stage 2 完成,等待验收(提交
7edb4e7)从 SYB 接口到入库的整条路径打通了。
sybimport/sync.go的Syncsybimport/sync.go的ConnectPOST /api/admin/v1/syb-products/importsybimport/import_handler.goweb/src/views/goauto/syb-products/index.vue几个基于实测数据的设计决定
分页由
listTotal驱动,不用list响应里的total。 实测确认后者是当前页行数,用它当总数循环会停在第一页。这条写进了代码注释和测试。完整性证明不了就停。 缺行的分页、分页期间总数漂移、明细响应少返回货运单,三种情况都立即停止并报错。已写入的数据保留——
ApplyDetail对(order_code, detail_id)幂等,重跑覆盖。不能把跑了一半的同步报成成功,否则漏掉的单永远补不回来。会话缓存必须连 user id 一起存。 这是实现过程中发现的真问题:
CheckSession调/am/user/get?id=,id 不对时顺云宝返回的是业务错误(msg=品名不能为空)而不是「未登录」。只存 Cookie 的话恢复出来的会话永远校验不过,缓存等于白做,每次都要重新登录。已给models.SYBSession加UserID列。导入端点单飞(single-flight)且脱离请求上下文。 两个并发导入会在同一批行上互相覆盖,数据没事但两份报告都会少算;操作员关掉标签页也不该让跑了一半的范围中断。
页面在范围过大时警告。 实测一周 7000+ 张,默认范围给 3 天。
验证结果
go build ./...、go vet、go test ./...全部通过sybimport9 个同步用例 + 1 个路由注册用例,全部用 httptest 假服务端eslint干净,SFC 编译通过TestSyncStopsWhenTotalDriftsDuringPaging失败一个值得记的插曲:写假服务端时我把请求体字段写成了
pageSize,真实字段是length,测试立刻以 panic 暴露了这个错。需要用户验证
syb_session表和它的user_id列能建出来(本机 MySQL 在 Windows 侧)vite build通过(WSL 缺 rolldown 原生依赖,我跑不了)GOAUTO_SYB_USERNAME/GOAUTO_SYB_PASSWORD环境变量明确没做
last_synced_at覆盖游标机制没有移植——手动触发场景用不到,等真需要无人值守时再单独做。端到端真实导入跑通了
提交
4f7f3a6。用真实凭据对顺云宝真站跑了一次完整导入(落库到 SQLite,不是 MySQL,原因见下)。解析质量:1485/1524(97.4%)拿到了完整的颜色+尺码。
productSpec为空4 条 failed 都是源数据本身为空——按规则标记失败而不是伪造缺失字段,符合预期。125 条 uncertain 进入人工复核路径。
顺带发现并修掉一个真缺陷
models.SYBSession漏了TableName(),gorm 悄悄建成了syb_sessions(复数),而仓库里其它模型全部显式声明单数表名。这个缺陷的麻烦之处在于迁移照样成功——不报错,直到有人按约定的名字查询才会发现。是这次真实导入把它暴露出来的(
syb_session 0 行)。发现得算及时:MySQL 上还没有跑过迁移,不用处理已经建错的表。已修,并且把
Migrate的模型清单提取成MigratedModels(),让新增的约定测试覆盖的正是Migrate真正创建的那批模型,而不是一份会漂移的手抄清单。变异验证:去掉TableName()后测试失败。MySQL 仍然没验证,原因是具体的
WSL 里能连到 Windows 侧的
3307端口,但被 MySQL 拒绝:root账号只授权了本机。这不是代码问题。两个办法二选一:CREATE USER 'root'@'192.168.%'之类)——这是改用户的数据库权限,未经明确要求我不会做。结论:能用,但有一条没验证
采集→解析→落库→幂等这条链路已经在真实数据上验证过。唯一没验证的是 MySQL 建表——SQLite 和 MySQL 的 DDL 差异在这个项目里出过事(之前
image_url的TEXT DEFAULT问题),所以这一条必须真跑过才能算数。用户于 2026-08-28 明确确认本工单验收通过。按项目流程记录验收结论并关闭工单;本次仅更新工单状态,无新增长期文档事实,不重复同步 Wiki。