feat: 持久化采集记录去重 + Goods系统批量导入
- collected.txt 跨文件/跨次记录已查询条码,避免重复采集 - Web UI 显示历史已采集条码计数 - Goods API 新增 POST /api/import/bypos 批量导入端点(含upsert) - 采集器 Web UI 新增一键导入按钮(登录+上传JSONL) - Windows exe 重新编译 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -70,6 +72,7 @@ func main() {
|
||||
mux.HandleFunc("/api/stats", handleStats)
|
||||
mux.HandleFunc("/api/download", handleDownload)
|
||||
mux.HandleFunc("/api/export.csv", handleExportCSV)
|
||||
mux.HandleFunc("/api/import", handleImport)
|
||||
|
||||
ln, err := net.Listen("tcp", *addr)
|
||||
if err != nil {
|
||||
@@ -168,6 +171,79 @@ func handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, f)
|
||||
}
|
||||
|
||||
func handleImport(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "method", 405)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
APIURL string `json:"api_url"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, 400, map[string]string{"error": "请求格式错误"})
|
||||
return
|
||||
}
|
||||
req.APIURL = strings.TrimRight(req.APIURL, "/")
|
||||
if req.APIURL == "" || req.Username == "" || req.Password == "" {
|
||||
writeJSON(w, 400, map[string]string{"error": "请填写完整的 API 地址、用户名和密码"})
|
||||
return
|
||||
}
|
||||
|
||||
s := collector.snapshot()
|
||||
if s.OutFile == "" {
|
||||
writeJSON(w, 400, map[string]string{"error": "没有采集数据可导入"})
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
loginBody, _ := json.Marshal(map[string]string{"username": req.Username, "password": req.Password})
|
||||
loginResp, err := client.Post(req.APIURL+"/api/login", "application/json", bytes.NewReader(loginBody))
|
||||
if err != nil {
|
||||
writeJSON(w, 502, map[string]string{"error": "无法连接 Goods 系统: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer loginResp.Body.Close()
|
||||
var loginResult struct {
|
||||
Token string `json:"token"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
json.NewDecoder(loginResp.Body).Decode(&loginResult)
|
||||
if loginResp.StatusCode != 200 || loginResult.Token == "" {
|
||||
msg := "登录失败"
|
||||
if loginResult.Error != nil {
|
||||
msg = loginResult.Error.Message
|
||||
}
|
||||
writeJSON(w, 401, map[string]string{"error": msg})
|
||||
return
|
||||
}
|
||||
|
||||
jsonlData, err := os.ReadFile(s.OutFile)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, map[string]string{"error": "读取采集文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
importReq, _ := http.NewRequest("POST", req.APIURL+"/api/import/bypos", bytes.NewReader(jsonlData))
|
||||
importReq.Header.Set("Content-Type", "application/x-ndjson")
|
||||
importReq.Header.Set("Authorization", "Bearer "+loginResult.Token)
|
||||
importResp, err := client.Do(importReq)
|
||||
if err != nil {
|
||||
writeJSON(w, 502, map[string]string{"error": "导入请求失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer importResp.Body.Close()
|
||||
respBody, _ := io.ReadAll(importResp.Body)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(importResp.StatusCode)
|
||||
w.Write(respBody)
|
||||
}
|
||||
|
||||
func handleExportCSV(w http.ResponseWriter, r *http.Request) {
|
||||
s := collector.snapshot()
|
||||
if s.OutFile == "" {
|
||||
|
||||
Reference in New Issue
Block a user