0e42f0e417
- 项目目录结构:backend/ deploy/ schema/ migrations/ - 数据库 Schema:所有建表 SQL,含 hotel_id 多租户隔离 - Go 后端:config、model、handler、service、middleware、router - 认证:账号密码登录 + JWT(Access + Refresh Token) - 许可证:HMAC-SHA256 激活码生成 + 设备绑定验证 - 业务模块:商品、仓库、往来单位、入库、出库、库存、盘点 - 库存事务:入库/出库审核时原子更新库存 + 流水记录 - 数据导入:Excel/CSV 批量导入商品、往来单位 - Docker Compose:本地 MySQL + Adminer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
3.7 KiB
Go
158 lines
3.7 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/xuri/excelize/v2"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||
"github.com/wangjia/jiu/backend/internal/model"
|
||
)
|
||
|
||
type ImportHandler struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
func NewImportHandler(db *gorm.DB) *ImportHandler {
|
||
return &ImportHandler{db: db}
|
||
}
|
||
|
||
// ImportProducts POST /api/v1/import/products
|
||
// 支持 .xlsx / .csv,列顺序:名称,系列,规格,单位,品牌,进价,售价,最低库存,备注
|
||
func (h *ImportHandler) ImportProducts(c *gin.Context) {
|
||
hotelID := middleware.GetHotelID(c)
|
||
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
|
||
return
|
||
}
|
||
|
||
f, err := file.Open()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
defer f.Close()
|
||
|
||
xl, err := excelize.OpenReader(f)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid excel file: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
sheetName := xl.GetSheetName(0)
|
||
rows, err := xl.GetRows(sheetName)
|
||
if err != nil || len(rows) < 2 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "empty or invalid sheet"})
|
||
return
|
||
}
|
||
|
||
var products []model.Product
|
||
var errRows []map[string]interface{}
|
||
|
||
for i, row := range rows[1:] { // 跳过表头
|
||
if len(row) < 1 || strings.TrimSpace(row[0]) == "" {
|
||
continue
|
||
}
|
||
p := model.Product{
|
||
TenantBase: model.TenantBase{HotelID: hotelID},
|
||
}
|
||
p.Name = cell(row, 0)
|
||
p.Series = cell(row, 1)
|
||
p.Spec = cell(row, 2)
|
||
p.Unit = cell(row, 3)
|
||
p.Brand = cell(row, 4)
|
||
p.Remark = cell(row, 8)
|
||
|
||
if p.Name == "" {
|
||
errRows = append(errRows, map[string]interface{}{"row": i + 2, "error": "name is empty"})
|
||
continue
|
||
}
|
||
products = append(products, p)
|
||
}
|
||
|
||
if len(products) == 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "no valid rows", "errors": errRows})
|
||
return
|
||
}
|
||
|
||
// 批量写入(upsert by hotel_id+name+spec)
|
||
if err := h.db.CreateInBatches(&products, 100).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"imported": len(products),
|
||
"errors": errRows,
|
||
})
|
||
}
|
||
|
||
// ImportPartners POST /api/v1/import/partners
|
||
// 列顺序:名称,类型(supplier/customer),联系人,电话,地址,备注
|
||
func (h *ImportHandler) ImportPartners(c *gin.Context) {
|
||
hotelID := middleware.GetHotelID(c)
|
||
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
|
||
return
|
||
}
|
||
|
||
f, err := file.Open()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
defer f.Close()
|
||
|
||
xl, err := excelize.OpenReader(f)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid excel file"})
|
||
return
|
||
}
|
||
|
||
rows, err := xl.GetRows(xl.GetSheetName(0))
|
||
if err != nil || len(rows) < 2 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": "empty sheet"})
|
||
return
|
||
}
|
||
|
||
var partners []model.Partner
|
||
for _, row := range rows[1:] {
|
||
if len(row) < 1 || strings.TrimSpace(row[0]) == "" {
|
||
continue
|
||
}
|
||
t := cell(row, 1)
|
||
if t == "" {
|
||
t = "supplier"
|
||
}
|
||
partners = append(partners, model.Partner{
|
||
TenantBase: model.TenantBase{HotelID: hotelID},
|
||
Name: cell(row, 0),
|
||
Type: t,
|
||
Contact: cell(row, 2),
|
||
Phone: cell(row, 3),
|
||
Address: cell(row, 4),
|
||
Remark: cell(row, 5),
|
||
})
|
||
}
|
||
|
||
if err := h.db.CreateInBatches(&partners, 100).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"imported": len(partners)})
|
||
}
|
||
|
||
func cell(row []string, idx int) string {
|
||
if idx >= len(row) {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(row[idx])
|
||
}
|