feat: 支持批量导入 PDD 商品链接 (#104)
This commit is contained in:
@@ -7,6 +7,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"cmautobuy/admin/service"
|
||||
)
|
||||
|
||||
// CSRF 防护,用的是「双提交 Cookie」这个最简单的方案:
|
||||
@@ -57,6 +59,17 @@ func CSRFMiddleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// multipart 表单会在下面 PostForm 时被解析,所以请求体上限必须在它之前设置。
|
||||
// 放到具体上传 Handler 里已经太晚:那时文件可能已经被读进临时目录。
|
||||
switch c.Request.URL.Path {
|
||||
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)
|
||||
if submitted == "" {
|
||||
submitted = c.GetHeader("X-CSRF-Token")
|
||||
|
||||
@@ -2,12 +2,15 @@ package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"cmautobuy/admin/config"
|
||||
"cmautobuy/admin/service"
|
||||
)
|
||||
|
||||
@@ -18,9 +21,13 @@ import (
|
||||
|
||||
// PddList 渲染 PDD 商品列表页。
|
||||
func (h *Handler) PddList(c *gin.Context) {
|
||||
keyword := c.Query("q")
|
||||
status := service.ParseCollectStatus(c.Query("status"))
|
||||
pageNum := service.ParsePage(c.Query("page"))
|
||||
h.renderPddList(c, c.Query("q"), c.Query("status"), c.Query("page"), "", nil)
|
||||
}
|
||||
|
||||
// renderPddList 统一渲染普通列表和导入结果页,避免两条入口逐渐长成两套页面。
|
||||
func (h *Handler) renderPddList(c *gin.Context, keyword, statusRaw, pageRaw, message string, importResult *service.PddImportResult) {
|
||||
status := service.ParseCollectStatus(statusRaw)
|
||||
pageNum := service.ParsePage(pageRaw)
|
||||
|
||||
result, err := service.ListPddProducts(h.db, keyword, status, pageNum)
|
||||
if err != nil {
|
||||
@@ -38,8 +45,11 @@ func (h *Handler) PddList(c *gin.Context) {
|
||||
// 底部状态条平时显示统计,刚做完操作时先显示操作结果。
|
||||
// 操作结果是通过跳转带过来的 msg 参数传的(见 pddRedirect)。
|
||||
statusLine := result.StatusLine()
|
||||
if msg := c.Query("msg"); msg != "" {
|
||||
statusLine = msg + " · " + statusLine
|
||||
if msg := c.Query("msg"); msg != "" && message == "" {
|
||||
message = msg
|
||||
}
|
||||
if message != "" {
|
||||
statusLine = message + " · " + statusLine
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
@@ -66,6 +76,7 @@ func (h *Handler) PddList(c *gin.Context) {
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
"DetailURL": "/pdd/detail?" + detailValues.Encode(),
|
||||
"AssignableClients": assignableClients,
|
||||
"ImportResult": importResult,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -112,6 +123,73 @@ func (h *Handler) PddCreate(c *gin.Context) {
|
||||
h.pddRedirect(c, fmt.Sprintf("已保存商品 %s,现在可以勾选它创建采集任务", goodsID))
|
||||
}
|
||||
|
||||
// PddImport 把一列式 xlsx 中的完整 PDD 链接批量导入商品档案。
|
||||
// 上传只建档,不会自动创建采集任务;导入结果页提供单独的确认入口。
|
||||
func (h *Handler) PddImport(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest,
|
||||
"没有收到文件(也可能是文件超过了 10MB 上限)。请重新选择 Excel;没有写入任何商品。")
|
||||
return
|
||||
}
|
||||
src, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "无法打开上传文件,请重新选择;没有写入任何商品。")
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
head := make([]byte, 8)
|
||||
n, _ := io.ReadFull(src, head)
|
||||
if err := service.ValidatePddUpload(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
|
||||
}
|
||||
tmp, err := os.CreateTemp(uploadDir, "pdd-*.xlsx")
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "无法保存临时上传文件,请重试;没有写入任何商品。")
|
||||
return
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err := io.Copy(tmp, src); err != nil {
|
||||
tmp.Close()
|
||||
fail(c, http.StatusInternalServerError, "保存临时上传文件失败,请重试;没有写入任何商品。")
|
||||
return
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
fail(c, http.StatusInternalServerError, "保存临时上传文件失败,请重试;没有写入任何商品。")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := service.ImportPddExcel(h.db, tmpPath)
|
||||
if err != nil {
|
||||
if service.IsInvalidPddImport(err) {
|
||||
fail(c, http.StatusBadRequest, err.Error()+"。请修正文件后重新导入;没有写入任何商品。")
|
||||
return
|
||||
}
|
||||
fail(c, http.StatusInternalServerError,
|
||||
"PDD 商品导入时数据库写入失败,本批合法行已经整体回滚。请稍后重试;一直失败请联系维护者。")
|
||||
return
|
||||
}
|
||||
|
||||
message := fmt.Sprintf(
|
||||
"批量导入完成:总行数 %d,新建 %d,已存在 %d,恢复 %d,文件内重复 %d,失败 %d",
|
||||
result.TotalRows, result.CreatedCount, result.ExistingCount, result.RevivedCount,
|
||||
result.DuplicateCount, len(result.Failures))
|
||||
h.renderPddList(c, "", "", "1", message, result)
|
||||
}
|
||||
|
||||
// PddSave 保存弹窗里改过的链接。
|
||||
func (h *Handler) PddSave(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.PostForm("id"), 10, 64)
|
||||
|
||||
@@ -65,6 +65,7 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
pages.GET("/pdd", h.PddList)
|
||||
pages.GET("/pdd/detail", h.PddDetail) // 双击行时前端来取弹窗内容
|
||||
pages.POST("/pdd/create", h.PddCreate)
|
||||
pages.POST("/pdd/import", h.PddImport)
|
||||
pages.POST("/pdd/save", h.PddSave)
|
||||
pages.POST("/pdd/collect", h.PddCollect)
|
||||
pages.POST("/pdd/delete", h.PddDelete)
|
||||
|
||||
Reference in New Issue
Block a user