refactor: 切换蝦皮数据到目录接口 (#135)
This commit is contained in:
@@ -65,9 +65,6 @@ func CSRFMiddleware() gin.HandlerFunc {
|
||||
case "/pdd/import":
|
||||
c.Request.Body = http.MaxBytesReader(
|
||||
c.Writer, c.Request.Body, service.MaxPddUploadBytes+1<<20)
|
||||
case "/shopee/import":
|
||||
c.Request.Body = http.MaxBytesReader(
|
||||
c.Writer, c.Request.Body, service.MaxShopeeUploadBytes+1<<20)
|
||||
}
|
||||
|
||||
submitted := c.PostForm(csrfFieldName)
|
||||
|
||||
@@ -2,17 +2,13 @@ package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"cmautobuy/admin/config"
|
||||
"cmautobuy/admin/repository"
|
||||
"cmautobuy/admin/service"
|
||||
)
|
||||
@@ -22,7 +18,7 @@ import (
|
||||
// **商品级一行**(不是 SKU 级),数据来自 shopee_products 和 shopee_skus
|
||||
// 聚合联查。列定义见 docs/admin/05-ui-specification.md §4.2、工单 #41。
|
||||
func (h *Handler) ShopeeList(c *gin.Context) {
|
||||
h.renderShopeeList(c, c.Query("goods_id"), c.Query("status"), c.Query("page"), c.Query("msg"), nil)
|
||||
h.renderShopeeList(c, c.Query("goods_id"), c.Query("status"), c.Query("page"), c.Query("msg"))
|
||||
}
|
||||
|
||||
// ShopeeDetail 渲染双击行弹出的那个弹窗的**内容**(不是整页)。
|
||||
@@ -54,99 +50,11 @@ func (h *Handler) ShopeeDetail(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ShopeeImport 处理 Excel 上传导入。
|
||||
//
|
||||
// 关键规则(写错会丢数据,见 docs/admin/03-data-model.md §3.3):
|
||||
// - 商品汇总行(商品規格ID 为 "-")进 shopee_products,
|
||||
// SKU 行进 shopee_skus,两者要分开处理;
|
||||
// - 按列名找索引,不要写死列号;
|
||||
// - 规格原文两种格式各占一半,解析失败留空并把 parse_ok 置 0,不要猜;
|
||||
// - **只能 upsert,绝不允许先清空再导入**,
|
||||
// 否则人工填的 PDD 链接会被洗掉;
|
||||
// - upsert 的 DO UPDATE SET 里不得出现 pdd_goods_url / pdd_goods_id。
|
||||
//
|
||||
// 导入结果(含全部失败行)直接渲染在返回页面里,**不走 303 跳转**——
|
||||
// 失败行可能有几十条,塞进跳转用的 URL 查询参数既丑又有长度限制,
|
||||
// 没法满足"失败行要能全部看到"的要求(见工单 #38)。
|
||||
// 代价是这个页面刷新(F5)会弹出浏览器自带的"重新提交表单"确认,
|
||||
// 这是标准行为,比丢失失败行信息更容易接受。
|
||||
func (h *Handler) ShopeeImport(c *gin.Context) {
|
||||
// 限制整个请求体大小:50MB 上限 + 给表单其它字段留一点余量。
|
||||
// 放在 FormFile 之前,这样超限时请求体读到一半就会出错,
|
||||
// 不会先把整个超大文件读进内存再拒绝。
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, service.MaxShopeeUploadBytes+1<<20)
|
||||
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest,
|
||||
"没有收到文件(也可能是文件超过了 50MB 上限),请重新选择 Excel 后再提交。未写入数据库。")
|
||||
return
|
||||
}
|
||||
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "无法打开上传的文件,请重试。未写入数据库。")
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// 只读文件开头几个字节判断是不是真的 xlsx(zip),不用整份读进内存。
|
||||
head := make([]byte, 8)
|
||||
n, _ := io.ReadFull(src, head)
|
||||
if err := service.ValidateShopeeUpload(fileHeader.Filename, fileHeader.Size, head[:n]); err != nil {
|
||||
fail(c, http.StatusBadRequest, err.Error()+"。未写入数据库。")
|
||||
return
|
||||
}
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取上传文件失败,请重试。未写入数据库。")
|
||||
return
|
||||
}
|
||||
|
||||
uploadDir, err := config.SubDir("uploads")
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "无法准备上传目录,请重试。未写入数据库。")
|
||||
return
|
||||
}
|
||||
// `[必须]` 自己生成文件名,不用 fileHeader.Filename 拼路径——
|
||||
// 用户可控的文件名里塞一个 "../../etc/passwd" 就是路径穿越。
|
||||
savePath := filepath.Join(uploadDir, service.NewShopeeUploadFilename())
|
||||
|
||||
dst, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "保存上传文件失败,请重试。未写入数据库。")
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
dst.Close()
|
||||
fail(c, http.StatusInternalServerError, "保存上传文件失败,请重试。未写入数据库。")
|
||||
return
|
||||
}
|
||||
if err := dst.Close(); err != nil {
|
||||
fail(c, http.StatusInternalServerError, "保存上传文件失败,请重试。未写入数据库。")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := service.ImportShopeeExcel(h.db, savePath)
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest,
|
||||
"导入失败:"+err.Error()+"。这次导入整体没有生效(已回滚),可以修好文件后重新上传。")
|
||||
return
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("导入完成:%d 个商品 / %d 个 SKU", result.ProductCount, result.SKUCount)
|
||||
if len(result.Failures) > 0 {
|
||||
msg += fmt.Sprintf(",%d 行解析失败(见下方列表)", len(result.Failures))
|
||||
}
|
||||
// 导入完成后回到第 1 页、清空筛选——刚导入的数据从第一页就能看到,
|
||||
// 沿用筛选反而可能让人以为导入没生效(筛出来的还是老的几条)。
|
||||
h.renderShopeeList(c, "", "", "", msg, result.Failures)
|
||||
}
|
||||
|
||||
// renderShopeeList 是 ShopeeList 和 ShopeeImport 共用的渲染逻辑。
|
||||
// renderShopeeList 统一渲染蝦皮列表;数据写入改由商品目录接口负责。
|
||||
//
|
||||
// `[必须]` 分页控件用 <a href> 纯 GET 导航,翻页要保留 keyword/statusRaw,
|
||||
// 所以这里把它们原样透传回模板去拼下一页的链接,不在这里丢掉(工单 #43)。
|
||||
func (h *Handler) renderShopeeList(c *gin.Context, keyword, statusRaw, pageRaw, msg string, failures []service.ImportFailure) {
|
||||
func (h *Handler) renderShopeeList(c *gin.Context, keyword, statusRaw, pageRaw, msg string) {
|
||||
filter := repository.ShopeeFilter{
|
||||
Keyword: keyword,
|
||||
Status: service.ParseShopeeStatus(statusRaw),
|
||||
@@ -183,7 +91,6 @@ func (h *Handler) renderShopeeList(c *gin.Context, keyword, statusRaw, pageRaw,
|
||||
"Status": status,
|
||||
"HasAnyProducts": result.HasAnyProducts,
|
||||
"IsFiltered": result.IsFiltered,
|
||||
"Failures": failures,
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
"DetailURL": "/shopee/detail?" + detailValuesForShopee(values, result.Page),
|
||||
}))
|
||||
|
||||
@@ -56,7 +56,6 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
// 1. 蝦皮数据
|
||||
pages.GET("/shopee", h.ShopeeList)
|
||||
pages.GET("/shopee/detail", h.ShopeeDetail) // 双击行时前端来取弹窗内容
|
||||
pages.POST("/shopee/import", h.ShopeeImport)
|
||||
pages.POST("/shopee/save", h.ShopeeSave)
|
||||
pages.POST("/shopee/delete", h.ShopeeDelete)
|
||||
pages.POST("/shopee/collect", h.ShopeeCollect)
|
||||
|
||||
Reference in New Issue
Block a user