Commit Graph
155 Commits
Author SHA1 Message Date
QiuSWandClaude Sonnet 5.5 e351a69912 feat(web): add shop column to SYB product export (#372)
Insert a text "店铺" column (row.shopName) right after SYB订单 and move
the 0.00 number format to the shifted price column. Sync the wiki mirror.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-10-10 11:50:18 +08:00
QiuSWandClaude Sonnet 5.5 d8d9ab9edd feat(web): add SYB price column and numeric amount cells to exports (#372)
SYB product export gets "SYB售价(TWD)" after "虾皮商品ID" (unitPriceCent / 100,
same source as the page price column). Purchase price and SYB price are written
as numeric cells; an optional numberFormatCols option on export_json_to_excel
shows them as 0.00 without changing other pages' exports.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-10-10 09:40:02 +08:00
QiuSWandClaude Sonnet 5.5 a4a793f449 feat(web): export all filtered pages to Excel for purchase tasks and SYB products (#372)
- Web: shared list-export util (5000-row cap, progress, no partial file,
  filters frozen at click time) and Export buttons on the purchase
  management and SYB products pages; return matches are read per batch
  of product ids and only matched/confirmed ones are used.
- Server: purchase list gains inclusive orderSubmittedFrom/To day filter
  (UTC+8, same as SYB createdFrom/To, INVALID_REQUEST on bad dates);
  return-matches list items gain read-only yeekeQuantity from the
  existing join. No migration, no new endpoint or permission.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-10-10 08:58:28 +08:00
QiuSW 4581ee5687 feat: capture private purchase failure diagnostics end to end (#364) 2026-10-08 15:05:32 +08:00
QiuSW 845853c68a test(#363): verify page-local pagination and document default 2026-10-07 11:15:35 +08:00
QiuSW 955869b5f5 fix(#363): default inner code page size to 200 2026-10-07 11:09:26 +08:00
QiuSW 0a79c83a95 merge(#356): support pasted purchase order filters 2026-10-06 09:33:41 +08:00
QiuSW d6639a4a0d feat(#357): display SYB cancellation status without changing procurement 2026-10-05 17:51:07 +08:00
QiuSW bb3990e432 feat(#356): support pasted purchase order filters 2026-10-05 16:33:38 +08:00
QiuSW ddc18f5370 merge(#353,#354,#355): integrate shop filter, pagination and list timeout 2026-10-05 15:02:12 +08:00
QiuSW a24c2068ef fix(#355): extend SYB product list timeout to 60 seconds 2026-10-05 14:50:22 +08:00
QiuSW 6752b149d8 feat(#353): filter SYB products by managed shops 2026-10-05 11:54:32 +08:00
QiuSW c6c7fbe2d4 feat(#354): add top pagination to SYB products 2026-10-05 11:12:48 +08:00
QiuSW d01540b9fd fix(web): label SYB sync filter counts as hits (#340) 2026-09-29 16:31:54 +08:00
QiuSWandClaude Opus 5.5 baa20af530 fix(syb): #340 phase 3 — recompute race protection, fingerprint, wording
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
2026-09-29 15:34:20 +08:00
QiuSW f54ace7ccf Merge remote-tracking branch 'origin/main' into feat/340-syb-excluded-products 2026-09-29 15:25:46 +08:00
QiuSW ee8f80f987 fix: submit only frozen eligible inner-code apply selections (#351) 2026-09-29 14:57:23 +08:00
QiuSW 9c5e20e249 fix: recover inner-code matching and enable batch rematch #350 2026-09-29 11:45:46 +08:00
QiuSWandClaude Opus 5.5 8c95790135 feat(syb): #340 sync-run detail wording + excluded count (phase 2 cont.)
Rewords 结构过滤命中/关键词过滤命中 -> 结构过滤标记/关键词过滤标记 on the
sync-run detail (SYB 同步记录), and shows "其中无需采购 N 条" next to 商品明细,
computed client-side as charFilterSkipped + keywordFilterSkipped (exactly
the rows this run marked pdd_purchase_excluded) — no backend field needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-29 11:00:15 +08:00
QiuSWandClaude Opus 5.5 316d5aa412 feat(syb): #340 phase 2 — merge main, recompute samples, frontend UI
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
2026-09-29 10:59:12 +08:00
QiuSW 14231c62a7 docs: record batch return use contract and verification #349 2026-09-29 10:24:54 +08:00
QiuSW 4280115e11 feat: batch use pending return matches #349 2026-09-29 10:21:28 +08:00
QiuSW a4c7b5e97b feat: add pending return match batch cancellation #347 2026-09-29 09:40:16 +08:00
QiuSW 3b95d4e879 feat(yeeke): sync reshipped returns and exclude new matching (#345) 2026-09-28 16:30:37 +08:00
QiuSW 266418ac47 fix(syb): place return matching after purchase (#342) 2026-09-27 17:23:57 +08:00
QiuSW 58c8c077ad fix(syb): arrange created-time filter row (#342) 2026-09-27 17:19:35 +08:00
QiuSW 977ac235e2 feat(syb): filter products by created time (#342) 2026-09-27 16:59:22 +08:00
QiuSWandClaude Opus 5.5 573f5b8cf3 feat(syb): support 200/500 rows per page on SYB products list (#339)
Bump the syb-products page-size options to 20/50/100/200/500 with a new
100 default, cap the server-side sybimport.List page size at 500, chunk
the per-page purchase-readiness preview into <=100-id requests, and add
per-button selection limits (with disabled+tooltip) for AI 匹配, 创建采购,
创建采集, 图搜采集 and 匹配退货 so a larger page never silently exceeds a
batch endpoint's cap. 创建采购's 100-item server cap is left untouched.
Also caps returnmatch.BatchMatch at 500 ids (INVALID_REQUEST beyond that).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-27 16:52:09 +08:00
QiuSW d415971268 feat(returnmatch): publish return matching and after-sales access (#338 #341) 2026-09-27 11:16:18 +08:00
QiuSW 6fe588d5e3 fix(web): show after-sales GoAuto actions (#341) 2026-09-27 11:04:25 +08:00
QiuSWandClaude Opus 5.5 b72fa105a8 feat(web): flag matched returns past their destroy deadline (#338)
A match whose return has passed its destroy deadline keeps blocking
purchase (user decision: remind, don't auto-cancel). Show 「退货已过销毁截止」
in the SYB products match column and compare dialog, and 「已过销毁截止」
under the occupying SYB product on the yeeke returns page. Uses the live
package deadline, so a resync that moves the deadline updates the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-24 16:27:38 +08:00
QiuSWandClaude Opus 5.5 5ac8e4c8fd feat(yeeke): show missing-marked and recovered counts per sync run (#338)
Persist how many return items each sync run flipped to "missing" and how
many came back to "ok" (yeeke_sync_run.missing_marked_count /
recovered_count, migration 1789801000000), return them from the sync-runs
API and add 「标记不可用」「恢复可用」 columns to the sync-runs page. When the
20% safety valve skips marking the count stays 0 and the reason remains in
error_message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-24 16:14:30 +08:00
QiuSWandClaude Opus 5.5 ff6e87649c feat(returnmatch): mark yeeke returns unavailable when a complete sync drops them (#338)
A COMPLETE yeeke sync run (natural page-walk end, not stopped by a
duplicate-fingerprint break or MaxPages exhaustion) that also wrote zero
record failures now flips any yeeke_return_package/yeeke_return_item still
"ok" from before the run to sync_status="missing" with missing_since
stamped, in one transaction. Rows are never deleted. A 20% safety valve
skips marking (and records why in error_message) when the candidate count
would be too large; a reappearing record recovers to "ok" with
missing_since cleared.

returnmatch.availableReturnPool now excludes missing items/packages from
matching, without auto-cancelling any existing active match; List()/Detail()
surface syncStatus/missingSince so the SYB products match column, its
compare dialog, and the yeeke returns list can warn or label rows as
unavailable.

Adds migration 1789800900000_return_missing (AutoMigrate alone does not
reach existing databases).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-24 15:57:51 +08:00
QiuSWandClaude Opus 5.5 f2c8307c17 feat(web): return-matching UI on SYB products and yeeke returns pages (#338)
SYB 订单商品页 (web/src/views/goauto/syb-products/index.vue):
- 「匹配退货」按钮,作用于勾选行中处于参与匹配阶段的商品(与服务端
  returnmatch.participatingStages 同一口径),canPurchase 门禁,忙碌时
  loading,无候选时禁用。
- 批量结果对话框:成功匹配/无候选/阶段不参与跳过/并发冲突跳过四类计数
  + 逐行原因表格。
- 处理阶段筛选与展示新增「退货待确认」「已用退货」两个选项/标签(复用
  sybimport 现有的、基于 purchase.ProcessStages 的阶段筛选机制,无需
  额外服务端改动——process_stage.go 上一次提交已让这两个阶段参与其中)。
- 新增「匹配到的退货商品」列(缩略图/退货订单号/规格/销毁截止),逐行
  「查看对比」「备注」「取消匹配」(后两者仅 canPurchase 可见)。
- 已有效匹配的行不可勾选(isReturnMatchCandidate 排除 return_pending/
  return_used 两个阶段)。
- 匹配详情对话框:SYB/yeeke 双方字段与归一化规格文字并排对比、备注
  编辑、操作日志列表、确认(仅 matched 状态可用)/取消匹配(matched
  或 confirmed 可用)按钮,取消前均有二次确认。

退货商品页 (web/src/views/goauto/yeeke-returns/index.vue):
- 新增「匹配状态」筛选(全部/未匹配/退货待确认/已用退货),对所有用户
  可见(纯只读筛选,不需要 canPurchase)。
- 新增「被占用 SYB 订单商品」列(订单号 + 当前阶段,点击跳转 SYB 商品
  页)。

New file web/src/api/goauto/return-matches.js: listReturnMatches/
getReturnMatch/batchMatchReturns/confirmReturnMatch/cancelReturnMatch/
remarkReturnMatch.

Backend support for the match column: returnmatch.Service.List now
returns ListItem (match + orderSn/image/variationName/destroyDeadline)
via one bounded join over the page's yeeke_return_item ids, so the SYB
product page's match column never needs a second request per row
(service.go, service_test.go: TestList_EnrichesWithYeekeReturnDisplayFields).

采购创建拦截的 UI 呈现沿用现有机制:ProcessStageReturnPending/
ProcessStageReturnUsed 让 isPurchaseCandidate(row) 天然为 false(与其余
非 purchase_ready 阶段的既有行为一致),「采购」按钮不显示,处理阶段列
展示服务端返回的拦截原因文案;未额外改动采购创建流程/UI。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-24 12:03:10 +08:00
QiuSWandClaude Sonnet 5 d0b86092a6 fix(web): map claim status 1 to 已认领 instead of showing the raw digit (#337)
The frontend's claimStatusMeta only mapped English pseudo-values
(pending/claimed/rejected) that never existed in yeeke's real data — the
API's confirmed status is the raw string "1" (HAR, #336 requirement
doc). Every known-status row fell through to the unstyled default and
showed a bare "1". The claimStatus filter had the same mismatch: its
options never equaled the backend's claim_status = <raw value> filter,
so filtering by 已认领/待认领/已拒绝 silently returned nothing.

Map "1" to 已认领 and drop the two never-observed pseudo-statuses from
the filter, per the project rule that unmapped values must stay visible
and flagged rather than guessed at.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-24 08:57:35 +08:00
QiuSWandClaude Sonnet 5 5faf870d0f refactor(web): rename returns list columns, add resync-updates-times test (#337)
商品标题/规格 -> 商品标题 (drop the spec subtitle, already shown in the
独立 规格数据 column). 认领时间 -> 上架时间 in the list. The detail drawer
still says 认领时间, unchanged per the request scope.

Also locks in existing sync behavior with a regression test: a resync of
the same package must overwrite claim_time and destroy_dead_line, not
just last_synced_at (server/.../sync.go already did this; the test only
adds coverage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-24 08:52:47 +08:00
QiuSW 6773c88a64 fix(yeeke): align return item table labels (#336) 2026-09-23 17:11:37 +08:00
QiuSWandClaude Opus 5.5 248eae01ee feat(yeeke): switch returns admin list to item-level rows with shop filter (#337)
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
2026-09-23 16:37:56 +08:00
QiuSWandClaude Opus 5.5 ac5d8336ec refactor(web): drop sync-runs footnote and let the table fill the page (#337)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 16:32:28 +08:00
QiuSWandClaude Opus 5 3bc4784402 refactor(web): simplify yeeke returns and sync-runs pages per review (#337)
Drop the page headings and descriptions on both pages; remove the manual
sync / view sync-runs buttons and the tracking-no and item/variation
filters from the returns page; place manual sync right-aligned after
reset on the sync-runs page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-23 14:48:13 +08:00
QiuSWandClaude Opus 5 ab292d6a17 feat(yeeke): add admin read-only return-package/sync-run modules (#337)
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
2026-09-23 14:23:51 +08:00
QiuSW bb1a410e8a feat(#329): add device purchaser ownership 2026-09-21 09:59:00 +08:00
QiuSW 368f2c2357 feat: filter purchase tasks by successful SYB order writeback (#327) 2026-09-19 15:38:42 +08:00
QiuSW 3a2472dd20 feat: complete purchase order information and simplify SYB writeback (#326) 2026-09-19 15:16:39 +08:00
QiuSW e89de1a085 feat: queue and reconcile SYB purchase order numbers (#305) 2026-09-18 10:13:20 +08:00
QiuSW 07a3817591 test: verify optional order amount in purchase detail (#306) 2026-09-18 09:30:20 +08:00
QiuSW 8c01329f97 feat: capture optional PDD order amount in manual backfill (#306) 2026-09-18 09:22:12 +08:00
QiuSW 9194fd664f fix(ops): allow local Admin LAN access (#304) 2026-09-17 17:02:35 +08:00
QiuSWandClaude Opus 5 60c75261d3 fix: 采购管理批量重试改为原地重试选中任务,不再新建 (#303)
采购员勾选失败任务点重试,要的是这条任务本身再跑一次。原先 BatchRetry 一律
调用 Create 新建(CG-224 → CG-234),任务号变化,同一 SYB 明细的多次尝试分散在
多个任务上。

- BatchRetry 改为逐条调用既有 Reset:任务号不变,状态回到 pending,本次执行记入
  purchase_task_attempt。Reset 的全部保护原样沿用——碰过下单边界、同一明细已有
  更新任务、规格快照不完整均拒绝并返回原因,拒绝后不退回新建。
- 先识别重放再做资格预检:首次重试后任务已是 pending,先预检会把同一 requestId
  的重复提交判为「只有失败任务可以重试」,破坏幂等。测试抓到后修正。
- 资格预检关闭设备占用检查,同一设备上勾选的多条可排队;真正的占用判断在 Reset
  事务内,ensureDeviceFree 不把租约为空的 pending 计为占用。
- AgentRetry(替代商品已匹配、继续采购)改走 batchRetryCreate,保持新建:商品已
  替换,原任务的商品与规格快照不能复用。
- 前端文案改为「已重试 / 第 N 次执行」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-17 14:50:05 +08:00
QiuSWandClaude Opus 5 03647dd7dd feat(web): 采集任务页增加来源筛选与「取消未开始的任务」按钮 (#298)
页面混显三种来源却只有 goodsId 和状态两个筛选,批量取消时无法限定范围。

- 服务端 AdminList 增加 source 过滤,非法值报参数错误而非静默忽略。
- 前端增加来源筛选与来源列;状态补 cancelled(info 色,取消不是错误,不与
  failed 共用红色)。
- 「取消未开始的任务」按钮带实时数量,确认框列出将被取消的任务,并写明范围是
  整个筛选条件而非当前页。hasMore 时提示还有未处理的任务。

`[必须]` 取消范围包含 goodsId。少了这一维,按 goods_id 筛出两条、按钮却取消
三十几条——那正是当初放弃「两个固定按钮」、改用「筛选 + 一个按钮」想避免的事。
BatchCancel 的 goodsId 与 AdminList 用同一种匹配方式,否则两边范围会悄悄错开。

实施:sonnet 子代理;goodsId 范围一致性由复核补入。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-16 16:39:07 +08:00