From 518e6df349348acc9d7017678cd067da43d83686 Mon Sep 17 00:00:00 2001 From: chengma Date: Sun, 9 Aug 2026 22:40:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E8=9D=A6=E7=9A=AE?= =?UTF-8?q?=E4=B8=8E=E9=A1=BA=E8=BF=90=E5=AE=9D=20PDD=20=E5=85=B3=E8=81=94?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=20(#68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/handler/web/others.go | 21 +++++ admin/handler/web/pagination_test.go | 18 ++++ admin/handler/web/shopee.go | 67 ++++++++++----- admin/handler/web/web.go | 2 + admin/repository/shopee.go | 21 +++++ admin/service/shopee_list.go | 20 +++-- admin/service/shopee_pdd.go | 94 +++++++++++++++++++++ admin/service/shopee_pdd_test.go | 103 +++++++++++++++++++++++ admin/service/syb.go | 10 +++ admin/templates/shopee/detail_modal.html | 82 +++++++++--------- admin/templates/shopee/list.html | 12 +-- admin/templates/syb/detail_modal.html | 37 +++++++- docs/admin/01-requirements.md | 17 ++-- docs/admin/05-ui-specification.md | 23 ++--- 14 files changed, 429 insertions(+), 98 deletions(-) create mode 100644 admin/service/shopee_pdd.go create mode 100644 admin/service/shopee_pdd_test.go diff --git a/admin/handler/web/others.go b/admin/handler/web/others.go index 616242f..1fd5e72 100644 --- a/admin/handler/web/others.go +++ b/admin/handler/web/others.go @@ -565,6 +565,27 @@ func (h *Handler) SybSelectSKU(c *gin.Context) { h.sybRedirect(c, "蝦皮规格已确认,下一步按处理状态继续。") } +// SybAssociatePdd 从货运单上下文关联 PDD 商品,实际业务规则与蝦皮详情共用。 +func (h *Handler) SybAssociatePdd(c *gin.Context) { + goodsID, err := service.AssociateSybPdd(h.db, c.PostForm("syb_id"), c.PostForm("pdd_url"), + c.PostForm("confirm_replace") == "1") + if err != nil { + h.sybRedirect(c, "PDD 关联未保存:"+err.Error()+"。请核对链接后重试。") + return + } + h.sybRedirect(c, fmt.Sprintf("已关联 PDD 商品 %s,可继续创建采集任务", goodsID)) +} + +// SybCollectPdd 为货运单当前关联的 PDD 商品创建采集任务。 +func (h *Handler) SybCollectPdd(c *gin.Context) { + result, err := service.CreateSybPddCollectTask(h.db, c.PostForm("syb_id")) + if err != nil { + h.sybRedirect(c, "采集任务未创建:"+err.Error()) + return + } + h.sybRedirect(c, service.FormatCollectTaskMessage(result)) +} + // SybMatch 保存规格匹配结果。 // // 关键:保存的是**可复用的 SKU 映射**(写 sku_mappings), diff --git a/admin/handler/web/pagination_test.go b/admin/handler/web/pagination_test.go index 4c8f8b2..1ec146d 100644 --- a/admin/handler/web/pagination_test.go +++ b/admin/handler/web/pagination_test.go @@ -38,6 +38,24 @@ func assertRedirectQuery(t *testing.T, recorder *httptest.ResponseRecorder, want } func Test列表写操作跳转保留筛选和页码(t *testing.T) { + t.Run("蝦皮", func(t *testing.T) { + context, recorder := listPostContext(t, "/shopee/save", url.Values{ + "list_goods_id": {"1001"}, "status": {"no_link"}, "page": {"2"}, + }) + (&Handler{}).shopeeRedirect(context, "完成") + assertRedirectQuery(t, recorder, "/shopee", map[string]string{ + "goods_id": "1001", "status": "no_link", "page": "2", "msg": "完成", + }) + }) + t.Run("顺运宝", func(t *testing.T) { + context, recorder := listPostContext(t, "/syb/associate-pdd", url.Values{ + "order_no": {"SYB-1"}, "stage": {"pdd_missing"}, "page": {"3"}, + }) + (&Handler{}).sybRedirect(context, "完成") + assertRedirectQuery(t, recorder, "/syb", map[string]string{ + "order_no": "SYB-1", "stage": "pdd_missing", "page": "3", "msg": "完成", + }) + }) t.Run("PDD", func(t *testing.T) { context, recorder := listPostContext(t, "/pdd/delete", url.Values{ "q": {"737"}, "status": {"pending"}, "page": {"3"}, diff --git a/admin/handler/web/shopee.go b/admin/handler/web/shopee.go index c18e2d4..07d66e7 100644 --- a/admin/handler/web/shopee.go +++ b/admin/handler/web/shopee.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "github.com/gin-gonic/gin" @@ -29,8 +30,6 @@ func (h *Handler) ShopeeList(c *gin.Context) { // 复用 #18 的弹窗机制:行上写 data-detail-id="{{.GoodsID}}", // 前端拿 goods_id 拼到 data-detail-url 后面,以 "id" 作为查询参数名 // (固定写法,见 static/js/app.js「双击行打开详情弹窗」)。 -// -// `[必须]` 这个弹窗只读,不含保存表单——ShopeeSave 仍是 501,见 #41。 func (h *Handler) ShopeeDetail(c *gin.Context) { goodsID := strings.TrimSpace(c.Query("id")) if goodsID == "" { @@ -48,7 +47,11 @@ func (h *Handler) ShopeeDetail(c *gin.Context) { return } - c.HTML(http.StatusOK, "shopee/detail_modal", gin.H{"D": detail}) + c.HTML(http.StatusOK, "shopee/detail_modal", gin.H{ + "D": detail, "CSRFToken": csrfToken(c), + "Keyword": c.Query("goods_id"), "StatusFilter": c.Query("status"), + "CurrentPage": service.ParsePage(c.Query("page")), + }) } // ShopeeImport 处理 Excel 上传导入。 @@ -182,9 +185,19 @@ func (h *Handler) renderShopeeList(c *gin.Context, keyword, statusRaw, pageRaw, "IsFiltered": result.IsFiltered, "Failures": failures, "Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()), + "DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page), })) } +func detailValuesForShopee(values url.Values, pageNum int) string { + detailValues := url.Values{} + for key, entries := range values { + detailValues[key] = append([]string(nil), entries...) + } + detailValues.Set("page", strconv.Itoa(pageNum)) + return detailValues.Encode() +} + // shopeeStatusLine 组装底部状态条的默认文案(没有 msg 覆盖时)。 // // `[必须]` 显示的是筛选后的**全量**总数(result.Total),不是本页行数, @@ -198,22 +211,15 @@ func shopeeStatusLine(statusFilter string, result *service.ShopeeListResult) str return fmt.Sprintf("%s · 第 %d/%d 页", prefix, result.Page, result.TotalPages) } -// ShopeeSave 保存编辑弹窗里的内容(PDD 链接、颜色、尺码、建议)。 -// -// 这是**从蝦皮商品出发**录入 PDD 链接的入口, -// 见 docs/admin/05-ui-specification.md §4.3。PDD 商品页(§5)也能录入, -// 两处并存;本页这个目前还是骨架,合并与否由「蝦皮↔PDD 关联入口」工单决定。 -// -// `[不做]` 本工单(#38)明确不实现这个接口,保持 501。 +// ShopeeSave 保存蝦皮商品当前关联的 PDD 商品。 func (h *Handler) ShopeeSave(c *gin.Context) { - // TODO(骨架): 保存 PDD 链接时调 repository.EnsurePddProduct, - // 由它去建 / 复用 / 复活 pdd_products 那一行。 - // - // **采集状态不在蝦皮这边**:它属于 PDD 商品(pdd_products.collect_status)。 - // "还没填链接"由 shopee_products.pdd_goods_id IS NULL 表达—— - // 没填链接时 pdd_products 里根本不会有那一行。 - // 不要写 no_link,那个值在 #16 已从 CHECK 约束里删掉了,写了会直接撞约束。 - fail(c, http.StatusNotImplemented, "保存功能尚未实现。") + goodsID, err := service.AssociateShopeePdd(h.db, c.PostForm("shopee_goods_id"), + c.PostForm("pdd_url"), c.PostForm("confirm_replace") == "1") + if err != nil { + h.shopeeRedirect(c, "PDD 关联未保存:"+err.Error()+"。请核对链接后重试。") + return + } + h.shopeeRedirect(c, fmt.Sprintf("已关联 PDD 商品 %s,可继续创建采集任务", goodsID)) } // ShopeeDelete 批量删除勾选的行。 @@ -236,6 +242,27 @@ func (h *Handler) ShopeeDelete(c *gin.Context) { // // `[不做]` 本工单(#38)明确不实现这个接口,保持 501。 func (h *Handler) ShopeeCollect(c *gin.Context) { - // TODO(骨架): 调 service.CreateCollectTasks(goodsIDs, clientID) - fail(c, http.StatusNotImplemented, "采集功能尚未实现。") + result, err := service.CreateShopeePddCollectTask(h.db, c.PostForm("shopee_goods_id")) + if err != nil { + h.shopeeRedirect(c, "采集任务未创建:"+err.Error()) + return + } + h.shopeeRedirect(c, service.FormatCollectTaskMessage(result)) +} + +func (h *Handler) shopeeRedirect(c *gin.Context, msg string) { + params := url.Values{} + if value := strings.TrimSpace(c.PostForm("list_goods_id")); value != "" { + params.Set("goods_id", value) + } + if value := strings.TrimSpace(c.PostForm("status")); value != "" { + params.Set("status", service.ParseShopeeStatus(value)) + } + if value := strings.TrimSpace(c.PostForm("page")); value != "" { + params.Set("page", value) + } + if msg != "" { + params.Set("msg", msg) + } + c.Redirect(http.StatusSeeOther, "/shopee?"+params.Encode()) } diff --git a/admin/handler/web/web.go b/admin/handler/web/web.go index 64e760e..c5d4781 100644 --- a/admin/handler/web/web.go +++ b/admin/handler/web/web.go @@ -77,6 +77,8 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) { pages.POST("/syb/sync", h.SybSync) pages.POST("/syb/login-and-sync", h.SybLoginAndSync) pages.POST("/syb/select-sku", h.SybSelectSKU) + pages.POST("/syb/associate-pdd", h.SybAssociatePdd) + pages.POST("/syb/collect-pdd", h.SybCollectPdd) pages.POST("/syb/match", h.SybMatch) pages.POST("/syb/create-task", h.SybCreateTask) pages.POST("/syb/delete", h.SybDelete) diff --git a/admin/repository/shopee.go b/admin/repository/shopee.go index cf3ca65..7f8ad91 100644 --- a/admin/repository/shopee.go +++ b/admin/repository/shopee.go @@ -244,6 +244,27 @@ func GetShopeeProductByGoodsID(q Execer, goodsID string) (*model.ShopeeProduct, return &p, nil } +// UpdateShopeePddLink 更新蝦皮商品当前关联的 PDD 商品。 +// 调用方必须先在同一事务里保证 PDD 商品存在,避免留下悬空关联。 +func UpdateShopeePddLink(q Execer, shopeeGoodsID, pddGoodsID, pddURL string) error { + result, err := q.Exec(` + UPDATE shopee_products + SET pdd_goods_id = ?, pdd_goods_url = ?, updated_at = ? + WHERE goods_id = ?`, + pddGoodsID, pddURL, model.NowISO(), shopeeGoodsID) + if err != nil { + return fmt.Errorf("更新蝦皮商品 %s 的 PDD 关联失败: %w", shopeeGoodsID, err) + } + n, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("读取蝦皮商品 %s 的更新结果失败: %w", shopeeGoodsID, err) + } + if n != 1 { + return fmt.Errorf("蝦皮商品 %s 不存在", shopeeGoodsID) + } + return nil +} + // GetShopeeCollectStatus 查一个 PDD 商品的采集状态和失败原因。 // pddGoodsID 为空(蝦皮商品还没填链接)或查不到对应的 pdd_products 行时, // 返回的两个 sql.NullString 都是 Valid=false,调用方按"未填链接"/"PDD 商品缺失"处理。 diff --git a/admin/service/shopee_list.go b/admin/service/shopee_list.go index c54eeae..4cbdf5f 100644 --- a/admin/service/shopee_list.go +++ b/admin/service/shopee_list.go @@ -192,18 +192,19 @@ type ShopeeSpecView struct { } // ShopeeProductDetail 是双击弹窗要显示的全部内容。 -// -// `[必须]` 这个弹窗只读,不含可提交的表单——本工单不实现保存, -// ShopeeSave 仍是 501,见 #41。 type ShopeeProductDetail struct { GoodsID string Title string ShopeeStatus string MainSKUCode string - PddURL string - PddMissing bool - StatusText string + PddURL string + PddGoodsID string + PddMissing bool + StatusText string + CollectStatus string + CollectMsg string + CanCollect bool SKUs []ShopeeSpecView PendingCount int @@ -222,6 +223,7 @@ func GetShopeeProductDetail(db *sql.DB, goodsID string) (*ShopeeProductDetail, e Title: p.Title, ShopeeStatus: p.ShopeeStatus, MainSKUCode: p.MainSKUCode, + PddGoodsID: p.PddGoodsID, } if d.Title == "" { d.Title = placeholder @@ -239,12 +241,16 @@ func GetShopeeProductDetail(db *sql.DB, goodsID string) (*ShopeeProductDetail, e d.StatusText = "未填链接" } else { d.PddURL = p.PddGoodsURL - status, _, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID) + status, msg, err := repository.GetShopeeCollectStatus(db, p.PddGoodsID) if err != nil { return nil, err } if status.Valid { + d.CollectStatus = status.String + d.CollectMsg = msg.String d.StatusText = collectStatusText(model.CollectStatus(status.String)) + d.CanCollect = status.String == string(model.CollectPending) || + status.String == string(model.CollectFailed) } else { d.StatusText = "PDD 商品缺失" } diff --git a/admin/service/shopee_pdd.go b/admin/service/shopee_pdd.go new file mode 100644 index 0000000..44a3acb --- /dev/null +++ b/admin/service/shopee_pdd.go @@ -0,0 +1,94 @@ +package service + +import ( + "database/sql" + "errors" + "fmt" + "strings" + + "cmautobuy/admin/repository" +) + +// ErrPddAssociationReplaceRequired 表示当前蝦皮商品已经关联了另一个 PDD 商品。 +var ErrPddAssociationReplaceRequired = errors.New("更换 PDD 商品需要明确确认") + +// AssociateShopeePdd 把蝦皮商品关联到 PDD 商品档案。 +// 解析、建档/复活和关联更新在同一事务内完成,任一步失败都不留下半成品。 +func AssociateShopeePdd(db *sql.DB, shopeeGoodsID, rawURL string, confirmReplace bool) (string, error) { + shopeeGoodsID = strings.TrimSpace(shopeeGoodsID) + rawURL = strings.TrimSpace(rawURL) + pddGoodsID, err := ParsePddGoodsID(rawURL) + if err != nil { + return "", err + } + + tx, err := db.Begin() + if err != nil { + return "", fmt.Errorf("开始关联 PDD 商品事务失败: %w", err) + } + defer tx.Rollback() + + product, err := repository.GetShopeeProductByGoodsID(tx, shopeeGoodsID) + if err != nil { + return "", err + } + if product == nil { + return "", fmt.Errorf("蝦皮商品 %s 不存在", shopeeGoodsID) + } + if product.PddGoodsID != "" && product.PddGoodsID != pddGoodsID && !confirmReplace { + return "", fmt.Errorf("%w:当前是 %s,新链接解析为 %s", + ErrPddAssociationReplaceRequired, product.PddGoodsID, pddGoodsID) + } + if _, err := repository.EnsurePddProduct(tx, pddGoodsID, rawURL); err != nil { + return "", err + } + if err := repository.UpdateShopeePddLink(tx, shopeeGoodsID, pddGoodsID, rawURL); err != nil { + return "", err + } + if err := tx.Commit(); err != nil { + return "", fmt.Errorf("提交 PDD 商品关联失败: %w", err) + } + return pddGoodsID, nil +} + +// CreateShopeePddCollectTask 按蝦皮商品当前关联创建采集任务,避免相信表单里的 PDD ID。 +func CreateShopeePddCollectTask(db *sql.DB, shopeeGoodsID string) (CollectTaskResult, error) { + product, err := repository.GetShopeeProductByGoodsID(db, strings.TrimSpace(shopeeGoodsID)) + if err != nil { + return CollectTaskResult{}, err + } + if product == nil { + return CollectTaskResult{}, fmt.Errorf("蝦皮商品不存在") + } + if product.PddGoodsID == "" { + return CollectTaskResult{}, fmt.Errorf("请先关联 PDD 商品") + } + return CreatePddCollectTasks(db, []string{product.PddGoodsID}) +} + +// AssociateSybPdd 从顺运宝明细解析出可信的蝦皮商品,再复用统一关联逻辑。 +func AssociateSybPdd(db *sql.DB, sybID, rawURL string, confirmReplace bool) (string, error) { + context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID)) + if err != nil { + return "", err + } + if context == nil { + return "", fmt.Errorf("顺运宝明细不存在") + } + if context.Order.ShopeeSKUID == "" { + return "", fmt.Errorf("请先确认蝦皮规格") + } + return AssociateShopeePdd(db, context.Order.ShopeeGoodsID, rawURL, confirmReplace) +} + +// CreateSybPddCollectTask 按顺运宝明细当前关联创建采集任务。 +func CreateSybPddCollectTask(db *sql.DB, sybID string) (CollectTaskResult, error) { + context, err := repository.GetSybOrderContext(db, strings.TrimSpace(sybID)) + if err != nil { + return CollectTaskResult{}, err + } + if context == nil { + return CollectTaskResult{}, fmt.Errorf("顺运宝明细不存在") + } + return CreateShopeePddCollectTask(db, context.Order.ShopeeGoodsID) +} diff --git a/admin/service/shopee_pdd_test.go b/admin/service/shopee_pdd_test.go new file mode 100644 index 0000000..8f5834a --- /dev/null +++ b/admin/service/shopee_pdd_test.go @@ -0,0 +1,103 @@ +package service + +import ( + "errors" + "testing" + + "cmautobuy/admin/repository" +) + +const ( + pddURLA = "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267" + pddURLB = "https://mobile.yangkeduo.com/goods.html?goods_id=937122477375" +) + +func TestAssociateShopeePdd_新建并重复复用(t *testing.T) { + db := newTestDB(t) + seedShopeeProduct(t, db, "1001", "商品一") + + for i := 0; i < 2; i++ { + got, err := AssociateShopeePdd(db, "1001", pddURLA, false) + if err != nil { + t.Fatalf("第 %d 次关联失败: %v", i+1, err) + } + if got != "737116531267" { + t.Fatalf("goods_id = %q", got) + } + } + var count int + db.QueryRow(`SELECT COUNT(*) FROM pdd_products WHERE goods_id = ?`, "737116531267").Scan(&count) + if count != 1 { + t.Fatalf("同一 PDD 商品建了 %d 条,想要 1", count) + } +} + +func TestAssociateShopeePdd_无效链接不写任何数据(t *testing.T) { + db := newTestDB(t) + seedShopeeProduct(t, db, "1001", "商品一") + if _, err := AssociateShopeePdd(db, "1001", "https://p.pinduoduo.com/short", false); err == nil { + t.Fatal("短链接应被拒绝") + } + var pddCount int + db.QueryRow(`SELECT COUNT(*) FROM pdd_products`).Scan(&pddCount) + product, _ := repository.GetShopeeProductByGoodsID(db, "1001") + if pddCount != 0 || product.PddGoodsID != "" { + t.Fatalf("失败后仍写入:pdd=%d, link=%q", pddCount, product.PddGoodsID) + } +} + +func TestAssociateShopeePdd_换商品必须确认且旧档案保留(t *testing.T) { + db := newTestDB(t) + seedShopeeProduct(t, db, "1001", "商品一") + if _, err := AssociateShopeePdd(db, "1001", pddURLA, false); err != nil { + t.Fatal(err) + } + if _, err := AssociateShopeePdd(db, "1001", pddURLB, false); !errors.Is(err, ErrPddAssociationReplaceRequired) { + t.Fatalf("未确认换品错误 = %v", err) + } + product, _ := repository.GetShopeeProductByGoodsID(db, "1001") + if product.PddGoodsID != "737116531267" { + t.Fatalf("未确认时关联被改成 %q", product.PddGoodsID) + } + if _, err := AssociateShopeePdd(db, "1001", pddURLB, true); err != nil { + t.Fatal(err) + } + product, _ = repository.GetShopeeProductByGoodsID(db, "1001") + if product.PddGoodsID != "937122477375" { + t.Fatalf("确认后关联 = %q", product.PddGoodsID) + } + old, _ := repository.GetPddProductByGoodsID(db, "737116531267") + if old == nil { + t.Fatal("换品不应删除旧 PDD 档案") + } +} + +func TestAssociateShopeePdd_软删除商品可复活(t *testing.T) { + db := newTestDB(t) + seedShopeeProduct(t, db, "1001", "商品一") + repository.EnsurePddProduct(db, "737116531267", pddURLA) + if _, err := repository.SoftDeletePddProducts(db, []string{"737116531267"}); err != nil { + t.Fatal(err) + } + if _, err := AssociateShopeePdd(db, "1001", pddURLA, false); err != nil { + t.Fatal(err) + } + p, _ := repository.GetPddProductByGoodsID(db, "737116531267") + if p == nil || p.IsDeleted() { + t.Fatal("关联应复活软删除的 PDD 商品") + } +} + +func TestCreateShopeePddCollectTask_按当前关联创建(t *testing.T) { + db := newTestDB(t) + seedShopeeProduct(t, db, "1001", "商品一") + AssociateShopeePdd(db, "1001", pddURLA, false) + result, err := CreateShopeePddCollectTask(db, "1001") + if err != nil || result.Created != 1 { + t.Fatalf("创建结果 = %+v, err=%v", result, err) + } + _, _, _, ok := collectTaskOf(t, db, "737116531267") + if !ok { + t.Fatal("没有创建采集任务") + } +} diff --git a/admin/service/syb.go b/admin/service/syb.go index 9851c3f..67e9443 100644 --- a/admin/service/syb.go +++ b/admin/service/syb.go @@ -1177,6 +1177,11 @@ type SybProcessingDetail struct { StageHelp string Candidates []SybSKUCandidateView ShopeeExists bool + PddGoodsID string + PddURL string + CollectStatus string + CollectMsg string + CanCollect bool } func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, error) { @@ -1189,7 +1194,12 @@ func GetSybProcessingDetail(db *sql.DB, sybID string) (*SybProcessingDetail, err SybID: o.SybID, OrderNo: o.OrderNo, Title: o.Title, ProductSpec: o.ProductSpec, ShopeeGoodsID: o.ShopeeGoodsID, ShopeeSKUID: o.ShopeeSKUID, Quantity: o.Quantity, ShopeeExists: context.ShopeeExists, + PddGoodsID: context.PddGoodsID, PddURL: context.PddGoodsURL, + CollectStatus: context.PddCollectStatus, CollectMsg: context.PddCollectMsg, } + d.CanCollect = context.PddGoodsID != "" && + (context.PddCollectStatus == string(model.CollectPending) || + context.PddCollectStatus == string(model.CollectFailed)) d.Stage, d.StageText, d.StageHelp, _ = sybStageFor(*context) if context.ShopeeExists { skus, err := repository.ListShopeeSKUsByGoodsID(db, o.ShopeeGoodsID) diff --git a/admin/templates/shopee/detail_modal.html b/admin/templates/shopee/detail_modal.html index 6e2102c..f17c7ab 100644 --- a/admin/templates/shopee/detail_modal.html +++ b/admin/templates/shopee/detail_modal.html @@ -1,61 +1,65 @@ {{define "shopee/detail_modal"}} -{{/* 双击一行时弹出的内容。 - 这是一个**片段**,不是整页——外面的弹窗壳子在 shopee/list.html 里。 - - `[必须]` 这个弹窗只读,没有保存表单——ShopeeSave 仍是 501, - 不要放一个点了没反应的保存按钮,见工单 #41。 */}} {{with .D}} -