diff --git a/app/admin/apis/shopee_product.go b/app/admin/apis/shopee_product.go index 5e53ca7..ce80d5e 100644 --- a/app/admin/apis/shopee_product.go +++ b/app/admin/apis/shopee_product.go @@ -137,6 +137,32 @@ func (e ShopeeProduct) SyncWaitByShopeeID(c *gin.Context) { e.getByShopeeID(c, true, true) } +// SyncIfStaleByShopeeID synchronizes only when the local sync timestamp is +// older than staleDays (or no local product has been synchronized yet). +func (e ShopeeProduct) SyncIfStaleByShopeeID(c *gin.Context) { + e.MakeContext(c) + staleDays := 1 + if raw := strings.TrimSpace(c.Query("staleDays")); raw != "" { + value, err := strconv.Atoi(raw) + if err != nil || value < 0 || value > 3650 { + e.Error(400, fmt.Errorf("staleDays must be between 0 and 3650"), "参数错误") + return + } + staleDays = value + } + db := primaryDB() + if db == nil { + e.Error(500, fmt.Errorf("database is not initialized"), "数据库未初始化") + return + } + var row models.ShopeeProduct + needsSync := true + if err := db.Where("shopee_id = ?", strings.TrimSpace(c.Param("shopeeId"))).First(&row).Error; err == nil && row.LastSyncedAt != nil { + needsSync = time.Since(*row.LastSyncedAt) >= time.Duration(staleDays)*24*time.Hour + } + e.getByShopeeID(c, true, needsSync) +} + // SyncStatus returns the current asynchronous refresh state. func (e ShopeeProduct) SyncStatus(c *gin.Context) { e.MakeContext(c) @@ -185,6 +211,7 @@ func (e ShopeeProduct) getByShopeeID(c *gin.Context, requireAPIKey bool, syncSho ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() want := strings.TrimSpace(c.Param("shopeeId")) + var syncedAt *time.Time if want == "" { e.Error(400, fmt.Errorf("shopeeId is required"), "参数错误") return @@ -249,6 +276,9 @@ func (e ShopeeProduct) getByShopeeID(c *gin.Context, requireAPIKey bool, syncSho e.Error(502, err, "Shopee 商品同步失败:"+err.Error()) return } + now := time.Now() + syncedAt = &now + db.Model(&models.ShopeeProduct{}).Where("shopee_id = ?", want).Update("last_synced_at", now) // The sync task updates the ERP asynchronously. Locate the record again so // getDetail uses the current Huohanhan internal ID and metadata. page, err = client.GetProductPage(ctx, huohanhan.ProductPageParams{ @@ -307,6 +337,7 @@ func (e ShopeeProduct) getByShopeeID(c *gin.Context, requireAPIKey bool, syncSho platformInfo, _ := detail.PlatformInfo() platformInfoObject, _ := detailObject(platformInfo) row := models.ShopeeProduct{ShopeeID: want, Payload: string(detail.RawJSON())} + row.LastSyncedAt = syncedAt row.ItemName = firstNonEmpty(detailString(itemInfoObject, "itemName"), record.ItemName) row.ShopName = firstNonEmpty(detailString(platformInfoObject, "shopName"), record.ShopName) row.Status = firstNonEmpty(detailString(itemInfoObject, "itemStatus"), record.ItemStatus) diff --git a/app/admin/models/shopee_product.go b/app/admin/models/shopee_product.go index f0f57fa..05e7153 100644 --- a/app/admin/models/shopee_product.go +++ b/app/admin/models/shopee_product.go @@ -1,6 +1,9 @@ package models -import "go-admin/common/models" +import ( + "go-admin/common/models" + "time" +) type ShopeeProduct struct { models.Model @@ -10,7 +13,8 @@ type ShopeeProduct struct { Status string `json:"status" gorm:"size:64"` // Detail responses include images, variants, logistics and descriptions; // some real responses exceed MySQL TEXT's 65,535-byte limit. - Payload string `json:"payload" gorm:"type:longtext"` + Payload string `json:"payload" gorm:"type:longtext"` + LastSyncedAt *time.Time `json:"lastSyncedAt"` models.ModelTime models.ControlBy } diff --git a/app/admin/router/shopee_product.go b/app/admin/router/shopee_product.go index 6c5802e..a82413e 100644 --- a/app/admin/router/shopee_product.go +++ b/app/admin/router/shopee_product.go @@ -16,6 +16,7 @@ func registerShopeeProductRouter(v1 *gin.RouterGroup) { v1.GET("/shopee/products/:shopeeId", a.GetByShopeeID) v1.POST("/shopee/products/:shopeeId/sync", a.SyncByShopeeID) v1.POST("/shopee/products/:shopeeId/sync-wait", a.SyncWaitByShopeeID) + v1.POST("/shopee/products/:shopeeId/sync-if-stale", a.SyncIfStaleByShopeeID) v1.GET("/shopee/products/:shopeeId/sync-status", a.SyncStatus) } func registerShopeeProductAdminRouter(v1 *gin.RouterGroup, auth *jwt.GinJWTMiddleware) { diff --git a/tickets/LOCAL-012-shopee-sync-if-stale.md b/tickets/LOCAL-012-shopee-sync-if-stale.md new file mode 100644 index 0000000..d9c816d --- /dev/null +++ b/tickets/LOCAL-012-shopee-sync-if-stale.md @@ -0,0 +1,17 @@ +# LOCAL-012:Shopee 商品条件同步查询 + +- 状态:待验收 +- 目标:按指定天数判断商品是否需要先同步,再获取最新 detail。 + +## 接口 + +`POST /api/v1/shopee/products/{shopeeId}/sync-if-stale?staleDays=1` + +支持 API Key 查询参数或请求头。没有本地同步记录,或 `lastSyncedAt` 超过指定天数时同步;否则直接读取 detail。默认 `staleDays=1`,允许 0 至 3650。 + +## 实施 + +- 新增 `shopee_product.last_synced_at` 字段。 +- 同步完成后记录同步时间。 +- 保留 `GET`、`/sync`、`/sync-wait` 和 `/sync-status` 原有语义。 +- 验证:`go test ./app/admin/apis ./app/admin/router ./internal/huohanhan` 通过。