Compare commits

...
Author SHA1 Message Date
QiuSWandClaude Opus 5 0ab5a96ca4 fix(server): read purchaser identity from JWT claims for owned devices (#333)
go-admin's Authorizator runs per request with the IdentityHandler map,
which carries no user entry, so c.Get("userId") was always 0 and every
purchaser got an empty device list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-22 10:46:01 +08:00
QiuSWandClaude Opus 5 5e0a9d108c docs: sync wiki mirrors after restore and #330 update (#330)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
2026-09-21 16:16:57 +08:00
21 changed files with 188 additions and 54 deletions
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Project-Profile
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Project-Profile.-
wiki_revision: 3b78360779ae520f1ff9e51be3118a04cd549f51
synchronized_at: 2026-09-07T09:27:40Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:21Z
<!-- gitea-wiki-mirror:end -->
# 项目档案
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Development-Workflow
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Development-Workflow.-
wiki_revision: 62ddbe4469740c02ce4a6ca2fd1966a89a79322f
synchronized_at: 2026-09-05T07:16:44Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:27Z
<!-- gitea-wiki-mirror:end -->
# 开发工作流
+8 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Architecture-and-Code-Map
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.-
wiki_revision: 46067d78327476f14e6cfd202458a53f295bf763
synchronized_at: 2026-09-19T07:18:28Z
wiki_revision: 8e2cfa74bc7cf228278221e0f7ef488ac1b596d3
synchronized_at: 2026-09-21T08:14:31Z
<!-- gitea-wiki-mirror:end -->
# 架构与代码地图
@@ -486,6 +486,12 @@ Web 唯一展示位置为“采集采购 → SYB 同步记录”:列表状态
- Web purchase-tasks API/页面:共享勾选、按动作筛选、独立状态列/详情及逐项接受结果;付款和物流流程保持原样。
- 验证入口:go test ./app/goauto/purchase ./app/goauto/sybclient ./app/goauto/access ./app/goauto/migrations ./cmd/migrate/migration/version-local;Web tests/e2e/purchase-order-writeback.spec.ts。测试只使用隔离SQLite和fake/httptest,不代表MySQL多实例或真实SYB验收。
### 会话类失败自动重试(#330)
- purchase/order_writeback_worker.go:restoreOrderWritebackClient 在 ImportCookiesJSON 后调用 sybclient.CheckSession,UserID<=0 显式判不可用;finishSessionUnavailable 复用 lease_expires_at 作为退避到期时间(maxSessionRetryAttempts=6,sessionRetryBackoff 5/10/15/30/30m),领取条件增加 failed+SYB_SESSION_UNAVAILABLE+到期+未达上限。无迁移。
- purchase/order_writeback.go:会话类失败的 CanSubmit 不受退避租约限制;手工重新提交 attempt_count 置 0。
- 验证:go test ./app/goauto/purchase(含 httptest 模拟 /am/user/get 与断言 syb_session 未删除)。
## Chrome PDD 订单回填扩展(#316)
- `chrome-extension/` 是独立 Manifest V3 交付单元:popup 只负责配置、启动/停止、状态轮询和逐项结果;content script 只在 `mobile.yangkeduo.com` 的隔离世界中按可见 DOM 串行读取;service worker 负责持久运行状态、聚合冲突和分批提交。
+6 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Business-Rules-and-Glossary
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
wiki_revision: b6df3e2d3d497827c9f3cc5b5ec859d5bda3c1f2
synchronized_at: 2026-09-19T07:39:56Z
wiki_revision: 6cb34c99ccdcf4b64b01cb9a25ff1549d8aeeec9
synchronized_at: 2026-09-21T08:14:37Z
<!-- gitea-wiki-mirror:end -->
# 业务规则与术语
@@ -630,6 +630,10 @@ Web 唯一展示位置为“采集采购 → SYB 同步记录”:列表状态
订单回填状态独立于采购成功、支付复核及物流 writeback_status。实付金额只存 Admin,SYB cost=0 为接口固定参数,不以金额推断付款。SYB 接口会同时更新采购状态/平台/时间,不能视作纯展示修改。
采购管理增加独立状态列、批量回填和详情补偿;复用既有访问权限,不增支付确认或审批。批量受理与最终成功分开展示;重试采购和回填分别筛选勾选项。远端无原子CAS,对系统外人工并发修改/超长延迟请求不能承诺绝对互斥;有冲突应人工核对,禁止强制覆盖。
### SYB 会话类失败的有界自动重试(#330)
实现 01510a8/08b7095(2026-09-21,已合并 main,未部署、未生产验证)。回填 worker 从缓存会话恢复客户端后调用 SYB 会话校验;会话缺失/过期、串号失效、校验网络错误等均记为 `SYB_SESSION_UNAVAILABLE`,error_message 只记录类别和“将自动重试;如持续失败请恢复登录后重试”,不含原始错误。该类失败发生在任何写入之前,最多自动重试 6 次,退避 5/10/15/30/30 分钟(约 90 分钟,大于一个整点同步周期),达上限保持 failed 等人工。会话仍只由每小时 SYB 同步刷新;回填不登录、不 OCR、不删除或写入会话。其他失败码仍不自动重试。退避期内可手工重新回填,手工提交重置尝试次数。历史失败记录不会被自动领取。
## Agent 回填订单入口兼容(#307)
- 打开我的订单后最多采样8次,每次间隔1秒,连续两次订单导航结构一致才继续;登录/风控仍立即停止,超时给出明确原因。
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Local-Development-and-Verification
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Local-Development-and-Verification.-
wiki_revision: f8996a09b2158253553c9e929eab72e1e53e8f61
synchronized_at: 2026-09-18T07:57:06Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:42Z
<!-- gitea-wiki-mirror:end -->
# 本地开发与验证
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Common-Changes
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Common-Changes.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:12Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:50Z
<!-- gitea-wiki-mirror:end -->
# 常见修改指南
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Troubleshooting
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Troubleshooting
wiki_revision: 18744477bfd17f396e8c76ec7a2fcc6720acdf6f
synchronized_at: 2026-09-11T09:10:00Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:54Z
<!-- gitea-wiki-mirror:end -->
# 故障排查
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Product-Requirements-Overview
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Product-Requirements-Overview.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:20Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:00Z
<!-- gitea-wiki-mirror:end -->
# 产品需求总览与当前 MVP
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Android-Agent-API-Contract
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.-
wiki_revision: 275caa306765440b0888dea701198d45481bfb40
synchronized_at: 2026-09-19T07:40:19Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:05Z
<!-- gitea-wiki-mirror:end -->
<!-- gitea-wiki-mirror:start -->
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Delivery-Issues
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Delivery-Issues.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:28Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:09Z
<!-- gitea-wiki-mirror:end -->
# 当前 MVP 交付工单索引
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: OnePlus-Real-Device-Acceptance
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/OnePlus-Real-Device-Acceptance.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:32Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:13Z
<!-- gitea-wiki-mirror:end -->
# 一加真机验收记录
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: PDD-Detail-Rule-Migration-Analysis
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/PDD-Detail-Rule-Migration-Analysis.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:37Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:17Z
<!-- gitea-wiki-mirror:end -->
# PDD 商品详情采集规则迁移分析
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: SYB-ERP-Interface-Contract
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/SYB-ERP-Interface-Contract.-
wiki_revision: 432e392ebe428ea19c0aa260f8e5938f36e4c3f0
synchronized_at: 2026-09-18T02:18:53Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:30Z
<!-- gitea-wiki-mirror:end -->
# 12 顺云宝(SYB)ERP 接口契约
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Deployment-and-Operations
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Deployment-and-Operations.-
wiki_revision: c208aeeeb4baa3ae2da29f13f3651910c500e360
synchronized_at: 2026-09-18T07:57:16Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:47Z
<!-- gitea-wiki-mirror:end -->
# 部署与运维
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Home
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Home
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:29:34Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:14:18Z
<!-- gitea-wiki-mirror:end -->
# GoAuto 文档中心
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Deployment-Template
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Deployment-Template.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:41Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:22Z
<!-- gitea-wiki-mirror:end -->
# 部署文档模板
+2 -2
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Task-Archive-Template
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Task-Archive-Template.-
wiki_revision: b1b1b343917e66288f4282bc6b3b90ea4ff3cca0
synchronized_at: 2026-09-04T11:30:46Z
wiki_revision: 03ea269058b50fea2be7842b9c284018987c82d9
synchronized_at: 2026-09-21T08:15:26Z
<!-- gitea-wiki-mirror:end -->
> 本模板只用于用户明确要求的专项历史快照或读取既有归档,不属于标准任务闭环。单次任务的唯一事实来源是 Gitea 工单;不要为了完成普通任务创建本页面,也不要自动导出到 `docs/task/`。
+14 -9
View File
@@ -47,19 +47,24 @@ func (handler Handler) List(context *gin.Context) {
}
func currentUserID(c *gin.Context) uint64 {
value, ok := c.Get("userId")
if !ok {
return 0
}
switch id := value.(type) {
// #333: go-admin's Authorizator runs on every request with the
// IdentityHandler map, which has no "user" entry, so c.Get("userId") is
// always 0 there. The JWT "identity" claim is the authenticated user id.
switch id := jwt.ExtractClaims(c)["identity"].(type) {
case float64:
if id > 0 {
return uint64(id)
}
case int:
return uint64(id)
if id > 0 {
return uint64(id)
}
case int64:
return uint64(id)
if id > 0 {
return uint64(id)
}
case uint64:
return id
case float64:
return uint64(id)
}
return 0
}
@@ -0,0 +1,84 @@
package device
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/goauto/models"
)
// #333: mirrors the real go-admin middleware, whose Authorizator sets
// userId to 0 on every request while the JWT claims carry the identity.
func listDevicesAs(t *testing.T, handler Handler, claims jwt.MapClaims) []DeviceListItem {
t.Helper()
gin.SetMode(gin.TestMode)
engine := gin.New()
engine.GET("/devices", func(c *gin.Context) {
c.Set(jwt.JwtPayloadKey, claims)
c.Set("userId", 0)
c.Next()
}, handler.List)
recorder := httptest.NewRecorder()
engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/devices?page=1&pageSize=20", nil))
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
var body struct {
Data DeviceListResponse `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
t.Fatalf("decode: %v", err)
}
return body.Data.Items
}
func seedOwnedDevice(t *testing.T, handler Handler, name string, owner *uint64) models.AgentDevice {
t.Helper()
device := models.AgentDevice{
InstallID: "install-" + name, Name: name, Manufacturer: "test", Model: "test",
AndroidVersion: "14", AgentVersion: "1", PDDVersion: "1", CapabilitiesJSON: "[]",
Status: models.DeviceStatusOnline, TokenDigest: fmt.Sprintf("digest-%s", name),
TokenIssuedAt: time.Now(), OwnerUserID: owner,
}
if err := handler.DB.Create(&device).Error; err != nil {
t.Fatalf("create device: %v", err)
}
return device
}
func deviceNames(items []DeviceListItem) map[string]bool {
names := map[string]bool{}
for _, item := range items {
names[item.Name] = true
}
return names
}
func TestDeviceListPurchaserSeesOnlyOwnDevicesFromJWTIdentity(t *testing.T) {
handler := Handler{DB: openTestDatabase(t)}
two, three := uint64(2), uint64(3)
seedOwnedDevice(t, handler, "caigou1-phone", &two)
seedOwnedDevice(t, handler, "caigou2-phone", &three)
seedOwnedDevice(t, handler, "unowned-phone", nil)
names := deviceNames(listDevicesAs(t, handler, jwt.MapClaims{"rolekey": "purchaser", "identity": float64(3)}))
if len(names) != 1 || !names["caigou2-phone"] {
t.Fatalf("purchaser 3 should see only own device, got %v", names)
}
all := deviceNames(listDevicesAs(t, handler, jwt.MapClaims{"rolekey": "admin", "identity": float64(1)}))
if len(all) != 3 {
t.Fatalf("admin should see all devices, got %v", all)
}
none := listDevicesAs(t, handler, jwt.MapClaims{"rolekey": "purchaser"})
if len(none) != 0 {
t.Fatalf("purchaser without identity must see no devices, got %d", len(none))
}
}
@@ -0,0 +1,31 @@
package shopeeproduct
import (
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
)
// #333: the real middleware sets userId=0 on every request; the operator used
// for owned-device collection lookup and audit must come from the JWT identity.
func TestCurrentUserIDReadsJWTIdentityNotUserIDKey(t *testing.T) {
gin.SetMode(gin.TestMode)
cases := []struct {
claims jwt.MapClaims
want uint64
}{
{jwt.MapClaims{"rolekey": "purchaser", "identity": float64(3)}, 3},
{jwt.MapClaims{"rolekey": "purchaser"}, 0},
{jwt.MapClaims{"rolekey": "purchaser", "identity": float64(-1)}, 0},
}
for _, tc := range cases {
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Set(jwt.JwtPayloadKey, tc.claims)
c.Set("userId", 0)
if got := currentUserID(c); got != tc.want {
t.Fatalf("claims %v: got %d want %d", tc.claims, got, tc.want)
}
}
}
+15 -11
View File
@@ -427,22 +427,26 @@ func (handler Handler) service(c *gin.Context) (*Service, bool) {
// purchasing roles per #40; role membership itself is enforced by the router
// middleware, not here.
func currentUserID(c *gin.Context) uint64 {
value, exists := c.Get("userId")
if !exists {
return 0
}
switch id := value.(type) {
// #333: go-admin's Authorizator runs on every request with the
// IdentityHandler map, which has no "user" entry, so c.Get("userId") is
// always 0 there. The JWT "identity" claim is the authenticated user id.
switch id := jwt.ExtractClaims(c)["identity"].(type) {
case float64:
if id > 0 {
return uint64(id)
}
case int:
return uint64(id)
if id > 0 {
return uint64(id)
}
case int64:
return uint64(id)
if id > 0 {
return uint64(id)
}
case uint64:
return id
case float64:
return uint64(id)
default:
return 0
}
return 0
}
func currentRole(c *gin.Context) string {