feat: 顺运宝货运单同步 (#46)
顺运宝模块此前是骨架,「同步」点了提示"待接入"。5195 个蝦皮商品已经 进系统,但货运单(真实订单)一条都没有,后面的规格匹配无从谈起。 按接口契约(docs/admin/08,从 4 份 HAR 还原)实现:配置、登录(界面 手工输验证码)、会话缓存到 SQLite、按日期范围增量同步、落 syb_orders。 shopee_sku_id 绝不被同步覆盖。它是规格匹配的结果,顺运宝那边根本没有 这个值(只给 11 位商品ID,蝦皮規格ID 是 12 位)。同步写进去就是写空, 把人工攒的匹配成果洗掉且不报错。它只出现在 INSERT 列清单里,不在 DO UPDATE SET 里;repository 层和 service 端到端各有一个测试守着。 增量从「上次同步日期当天」重拉,不是第二天。created 筛选粒度是日期而 last_synced_at 精确到秒,从第二天拉会漏掉当天晚些时候创建的单且不报错。 宁可重复拉(upsert 幂等)也不能漏。中途失败不更新 last_synced_at, 否则下次跳过这段区间,漏的单永远补不回来。 日期运算用 UTC+8,不是 UTC。审查时从 HAR 确认 created 是当地时间: 抓包于 2026-07-28T03:31:45Z(= 11:31 UTC+8),同一响应里 created 是 "2026-07-28 10:37:59";若它是 UTC 则等于 18:37 UTC+8,比抓包晚 7 小时, 订单创建于未来,不成立。用 UTC 算会在本地 00:00-08:00 把"今天"算成昨天, 当天早晨的单这轮拉不到。用 time.FixedZone 写死,不用 LoadLocation—— 那要读系统 tzdata,Windows 默认没有,打包成 exe 会失败。 金额一律取 detail/listByStock 的值:08 §5.1 实测同一响应里 amtOrder 在列表接口是分、escrowAmount 却不是,单位不统一,取错差 100 倍。 迁移 v5 纯追加(syb_session、syb_sync_state、syb_orders.product_spec), v1-v4 逐字未动,CheckSchema 覆盖新表新列。 会话有效性判断把「网络故障」和「明确未登录」的分类集中在 Client.do() 一处——网络抖一下就判定登出的话,验证码会弹个不停,还会丢掉有效会话。 测试全部用 httptest 假服务端,不打真实站点。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
<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>
|
||||
|
||||
@@ -27,6 +28,12 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{if .ConfigProblem}}
|
||||
<p class="missing">
|
||||
顺运宝配置有问题:{{.ConfigProblem}}
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -35,6 +42,7 @@
|
||||
<th>货运单 ID</th>
|
||||
<th>订单号</th>
|
||||
<th>商品标题</th>
|
||||
<th>规格</th>
|
||||
<th>蝦皮商品 ID</th>
|
||||
<th>规格 SKU</th>
|
||||
<th>数量</th>
|
||||
@@ -46,14 +54,37 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
{{/* TODO(骨架): 行渲染。价格是台币,显示成 NT$xx.xx,
|
||||
要和人民币一眼分得清。图片显示小缩略图,存的是 URL。
|
||||
双击行打开匹配弹窗。 */}}
|
||||
<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="11">
|
||||
还没有货运单。<br>
|
||||
<small>顺运宝同步方式尚未确定,当前可先手工录入用于联调。</small>
|
||||
<td colspan="12">
|
||||
{{if .IsFiltered}}
|
||||
当前筛选条件下没有货运单明细。<br>
|
||||
<small>换个订单号或清空搜索词再试。<a href="/syb">查看全部</a></small>
|
||||
{{else}}
|
||||
还没有货运单明细。<br>
|
||||
<small>点上方「同步」从顺运宝拉取,第一次同步需要先登录(验证码需要手工输入)。</small>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
@@ -61,13 +92,52 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{/* TODO(骨架): 匹配弹窗 templates/syb/match_modal.html
|
||||
左边蝦皮规格,右边该商品 pdd_data 里的 PDD 规格下拉。
|
||||
三条硬规则:
|
||||
1. 右侧下拉选项来自 pdd_data.dimensions,不要写死"颜色/尺码"两个维度;
|
||||
2. 打开时先查 sku_mappings,有记录就自动带出并提示"已自动带出";
|
||||
3. 该商品还没采集时,直接提示"请先到蝦皮数据模块采集"并给跳转链接,
|
||||
不要显示一个空下拉让人困惑。 */}}
|
||||
<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">
|
||||
<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}}
|
||||
|
||||
Reference in New Issue
Block a user