Files
cmautobuy/admin/templates/syb/list.html
T
chengmaandClaude Opus 5 2e686b17a4 feat: 顺运宝登录接入验证码自动识别 (#47)
#46 的登录只有手工输验证码一条路,而会话 24 小时就过期——每天第一次
同步都得有人在场,将来也做不了定时同步。

docs/admin/08 §8 当时写死"不引入 OCR 服务",理由是"多一个必须先启动的
东西"。那条判断基于示例脚本里的 http://127.0.0.1:8000/ocr(本机服务)。
用户提供了托管地址后前提不成立,本工单推翻它——文档里改写并保留原文,
让后来人知道这个决定变过、为什么变。

OCR 优先、手工兜底:识别成功直接登录,失败或服务不可达降级到 #46 已有的
手工弹窗,并在弹窗里说明是"已尝试 N 次"还是"服务不可用"。手工路径不删,
外部服务挂了不该让整个同步功能不可用。

识别失败也是 code:200。实测拿无文字图片探测 https://ocr.ilapage.cn/ocr
返回 {"code":200,"message":"Success","data":""}——不是错误码。所以
Recognize 只负责"这次 HTTP 调用有没有问题",空 data 照常返回 (", nil),
业务校验交给调用方;空 data 和长度不对收敛到同一个 len(code) != 4,
一条规则覆盖两种情况。

不合格的验证码不拿去登录:白费一次尝试,且频繁错误登录可能触发风控。
审查时变异测试发现这条没有测试守着——原测试只断言"重新取图了"和
"最终登录成功",禁用长度校验后依然成立。已补 loginRecorder 记录每次
提交到 /am/auth/login 的 code,断言登录只被调用一次且提交的是合格的那个。

每次重试重新取图(同一张图再识别结果一样,且可能已被上次失败的登录作废);
OCR 用独立 HTTP 客户端不带顺运宝 Cookie;验证码图片只在内存里传,不落盘。

OCR 不可达立即降级、不占用重试次数——对着连不上的地址重试 5 次,
操作员要等 50 秒才看到手工输入框,结果注定一样。

测试全部用 httptest,不打真实的 ocr.ilapage.cn 和 shunyunbaoerp.com。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 12:35:02 +08:00

150 lines
5.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "syb/list"}}
{{template "header" .}}
<div class="toolbar">
<form class="inline" method="post" action="/syb/sync">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<button type="submit">同步</button>
</form>
<form class="inline" method="post" action="/syb/create-task">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<button type="submit" data-need-checked>创建采购任务</button>
</form>
<form class="inline grow" method="get" action="/syb">
<label for="q">订单号</label>
{{/* search-narrow 把搜索框收到 30%,让「搜索」按钮紧挨着它。
用类名而不是改 .toolbar input[type="text"]——那条被五个页面共用。 */}}
<input id="q" type="text" name="order_no" value="{{.Keyword}}"
class="search-narrow" placeholder="订单号">
<button type="submit">搜索</button>
</form>
<form class="inline" method="post" action="/syb/delete" data-confirm-delete>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<button type="submit" class="danger" data-need-checked>删除</button>
</form>
</div>
{{if .ConfigProblem}}
<p class="missing">
顺运宝配置有问题:{{.ConfigProblem}}
</p>
{{end}}
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="col-check"><input type="checkbox" data-check-all></th>
<th>货运单 ID</th>
<th>订单号</th>
<th>商品标题</th>
<th>规格</th>
<th>蝦皮商品 ID</th>
<th>规格 SKU</th>
<th>数量</th>
<th>价格</th>
<th>图片</th>
<th>匹配状态</th>
<th>更新时间</th>
</tr>
</thead>
<tbody>
{{range .Rows}}
<tr data-detail-id="{{.SybID}}" {{if not .Matched}}class="row-warn"{{end}}>
<td class="col-check">
<input type="checkbox" value="{{.SybID}}" name="ids"
aria-label="选择货运单明细 {{.SybID}}">
</td>
<td>{{.SybID}}</td>
<td>{{.OrderNo}}</td>
<td class="truncate" title="{{.Title}}">{{.Title}}</td>
<td class="truncate" title="{{.ProductSpec}}">{{.ProductSpec}}</td>
<td>{{.ShopeeGoodsID}}</td>
{{/* 匹配状态是算出来的(shopee_sku_id 是否非空),本工单不做匹配功能,
待匹配的行整行标黄提醒,但不提供匹配入口——那是后续工单的范围 */}}
<td>{{if .ShopeeSKUID}}{{.ShopeeSKUID}}{{else}}—{{end}}</td>
<td>{{.Quantity}}</td>
<td>{{.PriceText}}</td>
<td>
{{if .ImageURL}}<img src="{{.ImageURL}}" alt="" class="thumb">{{else}}—{{end}}
</td>
<td>{{.MatchText}}</td>
<td>{{.UpdatedAt}}</td>
</tr>
{{else}}
<tr class="empty">
<td colspan="12">
{{if .IsFiltered}}
当前筛选条件下没有货运单明细。<br>
<small>换个订单号或清空搜索词再试。<a href="/syb">查看全部</a></small>
{{else}}
还没有货运单明细。<br>
<small>点上方「同步」从顺运宝拉取,第一次同步需要先登录(验证码需要手工输入)。</small>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<p class="hint">
「匹配状态」是根据规格 SKU 是否已填算出来的,本页暂不提供匹配入口
(规格匹配是后续工单的范围)。
</p>
{{/* ── 登录弹窗 ─────────────────────────────────
会话未登录/已过期时自动打开(不加 hidden);
`[必须]` 密码不在界面上显示、也不回显到 HTML,只显示只读账号。 */}}
<div class="modal-backdrop" id="login-modal" {{if not .NeedLogin}}hidden{{end}}>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="login-modal-title">
<div class="modal-head">
<h2 id="login-modal-title">登录顺运宝</h2>
<button type="button" class="modal-x" data-modal-close aria-label="关闭">×</button>
</div>
<form method="post" action="/syb/login-and-sync">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="order_no" value="{{.Keyword}}">
<div class="modal-body">
{{/* `[必须]` 工单 #47:自动识别验证码失败/降级时要说明原因
(已尝试 N 次 / 服务不可用),操作员才知道是配置错了还是
服务挂了。ocr_url 留空时压根没尝试过自动识别,不显示这条。 */}}
{{if .NeedLoginReason}}
<p class="hint login-degrade-reason">{{.NeedLoginReason}}</p>
{{end}}
<div class="field">
<label for="login-username">账号</label>
<input id="login-username" type="text" value="{{.Username}}" readonly>
</div>
<div class="field">
<label for="login-captcha-img">验证码</label>
{{/* 图片地址带一个时间戳查询参数,确保每次打开弹窗/点"换一张"
都拿到新图,不被浏览器缓存吃掉旧的 */}}
<img id="login-captcha-img" src="/syb/captcha" alt="验证码"
class="captcha-img" data-captcha-refresh="/syb/captcha">
<button type="button" id="login-captcha-refresh">换一张</button>
</div>
<div class="field">
<label for="login-code">验证码文字</label>
<input id="login-code" type="text" name="code" required autocomplete="off"
maxlength="8" placeholder="图片里的 4 位字母数字">
</div>
<p class="hint">
账号密码取自 <code>admin/config.yaml</code>,界面上不显示密码。
</p>
</div>
<div class="modal-foot">
<button type="button" data-modal-close>取消</button>
<button type="submit" class="primary">登录并同步</button>
</div>
</form>
</div>
</div>
{{template "footer" .}}
{{end}}