Merge origin/main (1e582cd, #350/#351 inner-code fixes) — no conflicts.
sybproductfilter/recompute.go:
1. Row-level protection in RecomputeExecute: the write phase is split out
into writeRecomputeChanges(ctx, tx, planned), independently testable.
For every planned change it takes the same clause.Locking{Strength:
"UPDATE"} row lock purchase.Service.create and returnmatch's
matchOneWithLock take, then re-checks under that lock: a purchase task
or active return match that appeared after planning skips the row
(counted), and a row already at its target mark is left alone. The
execute response and audit log now report ACTUAL writes/skips
(plan-time skips + write-time skips), not the initial plan.
2. Preview/execute binding: RecomputePreview returns a `fingerprint`
(sha256 over the sorted id:direction:ruleId list). RecomputeExecute now
requires it, recomputes the plan inside the same transaction and
compares before writing; a mismatch returns RECOMPUTE_PREVIEW_STALE
(HTTP 409, "数据或规则已变化,请重新预览后再执行") and writes nothing.
Frontend passes the preview's fingerprint to execute and re-previews
automatically on that error.
3. syb-product-filters List gains a per-rule `markedCount` (one grouped
COUNT(*)...GROUP BY excluded_rule_id query, no N+1): the REAL current
count of syb_product rows marked by that rule. The disable-structure-
rule confirm dialog now quotes this instead of the stale lastHitCount
sync snapshot; the 上次同步命中 column still shows lastHitCount.
4. recomputeChanges now selects only id/order_code/shopee_item_id/
raw_json/pdd_purchase_excluded instead of full syb_product rows.
Wording (user-approved deviation from the prototype text):
- syb-sync-runs detail: 其中无需采购 N 条 -> 其中本次规则命中 N 条.
- syb-product-filters: 上次同步标记 reverted back to 上次同步命中 (both
tables); the confirm-dialog text now cites markedCount, not lastHitCount.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Merge origin/main (through #345/e7c049d) into feat/340-syb-excluded-products.
Resolved conflicts in sybimport/service.go, handler.go, service_test.go
(kept #342's createdFrom/createdTo AND #340's purchaseType, all combined
with processStage), and took origin/main's syb-products/index.vue as the
base for the new UI work below. Renamed the migration version file from
1789801100000 to 1789801500000 (next free slot after main's highest,
1789801400000) — content unchanged, version comes from the filename.
Backend: sybproductfilter recompute preview now also returns up to 20
sample rows (order code, shopee item id, change direction, matched rule)
alongside the existing counts; execute stays count + audit-log only.
Frontend (feat/340 issue "## 设计证据"/"## 页面", prototype v1):
- SYB 订单商品页: 采购类型 filter (需 PDD 采购 default / 无需 PDD 采购 / 全部),
处理阶段 gains 无需采购 (pdd_excluded); selecting 退货待确认/已用退货/无需采购
auto-switches 采购类型 to 全部; pdd_excluded rows are tickable only for
匹配退货 (never collection/purchase/AI-match/image-search); rows show the
stage tag plus 规则:<kind> <keyword>; detail drawer shows 采购类型 and rule.
- SYB 过滤规则页: 跳过导入 -> 标记为无需 PDD 采购 wording, 上次同步命中 ->
上次同步标记, rewritten scope note, admin-only 按当前规则重算 button with
preview dialog (4 counts + up to 20 samples) -> confirm -> execute.
- SYB 同步记录: 结构/关键词过滤命中 -> 结构/关键词过滤标记 (see #340 point 8;
the run-level "其中无需采购 N 条" count is derivable client-side from the
existing char/keyword counts, no backend field added).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Change GET /api/admin/v1/yeeke-returns to return one row per
yeeke_return_item joined with its parent package, with a placeholder
row (hasItem:false) for packages that have no items. Add
GET /yeeke-returns/shops (distinct sorted shop names) and
GET /yeeke-returns/items/:itemId (item + parent package + sibling
items) as static-prefixed routes registered before the /:packageId
wildcard so they are not swallowed by it. Shop filter is now an exact
match instead of a partial LIKE. Remove trackingNo/itemCount from the
list projection while keeping them on package detail.
Update the admin web list to item-level rows (reference image column,
店铺 dropdown sourced from /shops) and retitle the detail drawer to
"退货商品详情", switching it to the item-detail endpoint for real rows
and the existing package-detail endpoint for placeholder rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
Add read-only server endpoints for yeeke return packages (paginated list
with order/tracking/shop/item/variation/claim-status/create-time filters,
plus package detail with items) and extend the existing sync-runs endpoint
with trigger/status/started-at filters, backward compatible. Split the
admin menu into two modules per the ticket's confirmed scope adjustment:
yeeke_returns (packages) and the new yeeke_sync_runs, both under the
采集采购 menu group, wired via a new sys_menu migration mirroring #237's
precedent. Add the corresponding Vue pages, API wrappers and Go tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
request.js's shared axios instance times out every admin call at 10s,
but AI matching settings allow the server to wait up to 600s
(aimatching.maxTimeoutSecs) for a real chat/completions round trip.
For shopee product 49915704844 the size dimension had no deterministic
match and genuinely called the model; the frontend cut the connection
before the server replied, which the browser reports as a generic
network failure ("服务器连接异常") that has nothing to do with the
actual cause. Override the timeout on just these two calls instead of
the shared default, which stays tight for every other (fast) admin
request.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkjpksKeizaRpYXfgD74Lf
Add batch AI suggestion for Shopee color/size spec mapping: a
specContextVersion snapshot guards both the new suggest endpoints and
must be echoed back, short id binding keeps the model from ever
returning a value outside the server-supplied PDD candidates, and
suggestions only prefill the draft above AutoConfirmMinConfidence -
low-confidence answers show the reason but stay unapplied. Every
AI-sourced mapping is still written as pending by the existing
SetMapping rule and needs an explicit operator confirm, same as
before for manual edits.
Server: server/app/goauto/aimatching/suggest.go (batch provider call),
server/app/goauto/shopeeproduct/{context_version,ai_suggest}.go (spec
context version + suggest-colors/suggest-sizes), wired into
handler.go/router.go/service.go.
Web: shopee-products AI 匹配 button on both tabs, low-confidence rows
left for manual pick, pending AI mappings get an explicit 确认 action.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkjpksKeizaRpYXfgD74Lf
Stage 2. Completes the path from the SYB API to the archive.
sybimport.Sync pages a date range day by day and folds each detail line
through the existing idempotent ApplyDetail. The paging loop is driven by
listTotal, never by the list response's own total, which live data
confirmed is the page's row count rather than the filtered total. A day
whose completeness cannot be proven — a short page, a total that drifted
while paging, a detail response missing an order — stops the run instead
of reporting a partial import as a whole one. Rows already written stay:
they are idempotent on (order_code, detail_id), so a re-run overwrites.
sybimport.Connect reuses the cached session and only discards it when SYB
explicitly says it is invalid. Caching the numeric user id alongside the
cookies is required, not incidental: session validation calls
/am/user/get?id=, and a wrong id comes back as a business error rather
than a logout, so cookies alone can never be revalidated.
The import endpoint is single-flight and detached from the request
context, so closing the tab cannot abandon a half-finished range. It
reads only; no SYB write endpoint is reachable from GoAuto.
The page swaps its "no import entry" warning for a date-range dialog that
warns when the span is wide — a single week held over 7000 orders — and
reports partial progress when a run fails midway.
Verified: go build, go vet, go test ./... all pass. The drift check was
mutation-tested. Not verified: the MySQL migration for syb_session, and
any browser walkthrough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>