607 lines
18 KiB
Go
607 lines
18 KiB
Go
package models
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"github.com/codinl/go-logger"
|
||
"github.com/xuri/excelize/v2"
|
||
"go-admin/app/admin/models/tools"
|
||
orm "go-admin/common/global"
|
||
"gorm.io/gorm"
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
type CategoryExcel struct {
|
||
FirstCategory string `json:"first_category" binding:"required"` // 一级分类
|
||
SecondCategory string `json:"second_category"` // 二级分类
|
||
ThreeCategory string `json:"three_category"` // 三级分类
|
||
}
|
||
|
||
type CommodityExcel struct {
|
||
Category string `json:"category" binding:"required"` // 商品所属分类
|
||
Name string `json:"name" binding:"required"` // 商品名称
|
||
IMEIType string `json:"imei_type" binding:"required"` // 是否串码 1-无串码 2-串码(系统生成) 3-串码(手动添加)
|
||
SysGenerate string `json:"sys_generate" binding:"required"` // 系统生成串码
|
||
SupplierName string `json:"supplier_name" binding:"required"` // 供应商名称
|
||
SellBrokerage string `json:"sell_brokerage" binding:"required"` // 销售毛利提成
|
||
StaffBrokerage string `json:"staff_brokerage" binding:"required"` // 员工毛利提成
|
||
MemberDiscount string `json:"member_discount" binding:"required"` // 会员优惠
|
||
RetailPrice string `json:"retail_price" binding:"required"` // 指导零售价
|
||
MinRetailPrice string `json:"min_retail_price" binding:"required"` // 最低零售价
|
||
WholesalePrice string `json:"wholesale_price" binding:"required"` // 指导采购价
|
||
StaffCostPrice string `json:"staff_cost_price" binding:"required"` // 员工成本价加价
|
||
Origin string `json:"origin"` // 产地
|
||
Remark string `json:"remark"` // 备注
|
||
}
|
||
|
||
// 获取struct的tag标签,匹配excel导入数据
|
||
func getJSONTagNames(s interface{}) []string {
|
||
valueType := reflect.TypeOf(s)
|
||
if valueType.Kind() != reflect.Struct {
|
||
fmt.Println("传入的不是结构体")
|
||
return nil
|
||
}
|
||
|
||
var tagNames []string
|
||
for i := 0; i < valueType.NumField(); i++ {
|
||
field := valueType.Field(i)
|
||
tag := field.Tag.Get("json")
|
||
if tag != "" {
|
||
tagNames = append(tagNames, tag)
|
||
}
|
||
}
|
||
|
||
return tagNames
|
||
}
|
||
|
||
// FileExcelReader 预览excel数据(不做必填项校验)
|
||
func FileExcelReader(d []byte, cols []string) ([]byte, []map[string]interface{}, error) {
|
||
reader, err := excelize.OpenReader(bytes.NewReader(d))
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("open reader error: %v", err)
|
||
}
|
||
|
||
sheetList := reader.GetSheetList()
|
||
if len(sheetList) == 0 {
|
||
return nil, nil, errors.New("sheet list is empty")
|
||
}
|
||
|
||
rows, err := reader.Rows(sheetList[0])
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("reader rows error: %v", err)
|
||
}
|
||
|
||
sheetCols, err := reader.GetCols(sheetList[0])
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("reader get cols error: %v", err)
|
||
}
|
||
if len(sheetCols) == 0 {
|
||
return nil, nil, errors.New("get cols is empty")
|
||
}
|
||
|
||
var colsMap []map[string]interface{}
|
||
if len(cols) == 0 {
|
||
switch sheetList[0] {
|
||
case "导分类":
|
||
cols = getJSONTagNames(CategoryExcel{})
|
||
case "导商品":
|
||
cols = getJSONTagNames(CommodityExcel{})
|
||
default:
|
||
cols = make([]string, len(sheetCols))
|
||
for i := range sheetCols {
|
||
cols[i] = fmt.Sprintf("c%02d", i)
|
||
}
|
||
}
|
||
} else if len(cols) != len(sheetCols) {
|
||
return nil, nil, errors.New("cols length does not match the number of columns")
|
||
}
|
||
|
||
for rows.Next() {
|
||
columns, err := rows.Columns()
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("rows columns error: %v", err)
|
||
}
|
||
|
||
columnMap := make(map[string]interface{}, len(cols))
|
||
for i, col := range cols {
|
||
if i < len(columns) {
|
||
columnMap[col] = columns[i]
|
||
} else {
|
||
columnMap[col] = ""
|
||
}
|
||
}
|
||
|
||
logger.Info("columnMap:", columnMap)
|
||
colsMap = append(colsMap, columnMap)
|
||
}
|
||
|
||
logger.Info("colsMap:", colsMap)
|
||
mCols, err := json.Marshal(colsMap)
|
||
if err != nil {
|
||
return mCols, nil, fmt.Errorf("marshal error: %v", err)
|
||
}
|
||
|
||
return mCols, colsMap, nil
|
||
}
|
||
|
||
// FileExcelImport 导入excel数据(校验必填项)
|
||
func FileExcelImport(d []byte, cols []string, nType int) ([]byte, []map[string]interface{}, error) {
|
||
reader, err := excelize.OpenReader(bytes.NewReader(d))
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("open reader error: %v", err)
|
||
}
|
||
|
||
sheetList := reader.GetSheetList()
|
||
if len(sheetList) == 0 {
|
||
return nil, nil, errors.New("sheet list is empty")
|
||
}
|
||
|
||
rows, err := reader.Rows(sheetList[0])
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("reader rows err: %v", err)
|
||
}
|
||
|
||
sheetCols, err := reader.GetCols(sheetList[0])
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("reader get cols err: %v", err)
|
||
}
|
||
|
||
if len(sheetCols) == 0 {
|
||
return nil, nil, errors.New("get cols is empty")
|
||
}
|
||
|
||
var colsMap []map[string]interface{}
|
||
if len(cols) == 0 {
|
||
switch nType { // 导入类型:1 商品分类 2 商品资料
|
||
case 1:
|
||
if sheetList[0] != "导分类" {
|
||
return nil, nil, errors.New("格式错误,不是分类模版excel")
|
||
}
|
||
if err := checkCategoryExcel(sheetCols); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
cols = getJSONTagNames(CategoryExcel{})
|
||
case 2:
|
||
if sheetList[0] != "导商品" {
|
||
return nil, nil, errors.New("格式错误,不是商品模版excel")
|
||
}
|
||
if err := checkCommodityExcel(sheetCols); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
cols = getJSONTagNames(CommodityExcel{})
|
||
default:
|
||
return nil, nil, errors.New("格式错误,不是商品分类或资料模版excel")
|
||
}
|
||
} else if len(cols) != len(sheetCols) {
|
||
return nil, nil, errors.New("cols length does not match the number of columns")
|
||
}
|
||
|
||
for rows.Next() {
|
||
columns, err := rows.Columns()
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("rows columns err: %v", err)
|
||
}
|
||
|
||
columnMap := make(map[string]interface{}, len(cols))
|
||
for i, col := range cols {
|
||
if i < len(columns) {
|
||
columnMap[col] = columns[i]
|
||
} else {
|
||
columnMap[col] = ""
|
||
}
|
||
}
|
||
|
||
logger.Info("columnMap:", columnMap)
|
||
colsMap = append(colsMap, columnMap)
|
||
}
|
||
|
||
logger.Info("colsMap:", colsMap)
|
||
mCols, err := json.Marshal(colsMap)
|
||
if err != nil {
|
||
return mCols, nil, fmt.Errorf("marshal err: %v", err)
|
||
}
|
||
|
||
return mCols, colsMap, nil
|
||
}
|
||
|
||
// 校验商品分类导入规则
|
||
// 导入商品分类校验报错: (1)只有3级,没有2级或1级 (2)有2,3级,但没有1级
|
||
func checkCategoryExcel(sheetCols [][]string) error {
|
||
if len(sheetCols) != 3 {
|
||
return errors.New("模版错误,请检查文件")
|
||
}
|
||
|
||
if len(sheetCols[0]) < len(sheetCols[1]) || len(sheetCols[0]) < len(sheetCols[2]) {
|
||
return errors.New("格式错误,缺少一级分类")
|
||
} else if len(sheetCols[1]) < len(sheetCols[2]) {
|
||
return errors.New("格式错误,缺少二级分类")
|
||
}
|
||
|
||
for i := 1; i < len(sheetCols[2]); i++ {
|
||
if sheetCols[0][i] == "" && (sheetCols[1][i] != "" || sheetCols[2][i] != "") {
|
||
return errors.New("格式错误,缺少一级分类")
|
||
} else if sheetCols[2][i] != "" && sheetCols[1][i] == "" {
|
||
return errors.New("格式错误,缺少二级分类")
|
||
}
|
||
}
|
||
|
||
if duplicateName, nFlag := hasDuplicateNames(sheetCols); nFlag {
|
||
return fmt.Errorf("分类名称不允许重复,请检查:[%v]", duplicateName)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 校验名称是否相同,有则false
|
||
func hasDuplicateNames(sheetCols [][]string) (string, bool) {
|
||
nameMap := make(map[string]struct{})
|
||
|
||
for _, col := range sheetCols {
|
||
for _, name := range col {
|
||
if _, exists := nameMap[name]; exists && name != "" {
|
||
// 有重复的名称
|
||
return name, true
|
||
}
|
||
nameMap[name] = struct{}{}
|
||
}
|
||
}
|
||
// 没有重复的名称
|
||
return "", false
|
||
}
|
||
|
||
// 校验商品资料导入规则
|
||
// 导入商品资料校验报错:必填项缺少数据(除了产地、备注,其他都是必填项)
|
||
func checkCommodityExcel(sheetCols [][]string) error {
|
||
if len(sheetCols) != 14 {
|
||
return errors.New("模版错误,请检查文件")
|
||
}
|
||
|
||
for i := 0; i < len(sheetCols)-2; i++ {
|
||
if len(sheetCols[i]) < len(sheetCols[i+1]) {
|
||
return errors.New("格式错误,有必填项未录入")
|
||
}
|
||
for _, v := range sheetCols[i] {
|
||
if v == "" {
|
||
return errors.New("格式错误,有必填项未录入")
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 将读取的excel数据转换成CategoryExcel struct
|
||
func transCategoryData(colsMap []map[string]interface{}) []CategoryExcel {
|
||
var categories []CategoryExcel
|
||
|
||
// 遍历 colsMap 进行类型断言和转换
|
||
for _, col := range colsMap {
|
||
var category CategoryExcel
|
||
|
||
for key, value := range col {
|
||
switch key {
|
||
case "first_category":
|
||
if name, ok := value.(string); ok {
|
||
category.FirstCategory = name
|
||
}
|
||
case "second_category":
|
||
if name, ok := value.(string); ok {
|
||
category.SecondCategory = name
|
||
}
|
||
case "three_category":
|
||
if name, ok := value.(string); ok {
|
||
category.ThreeCategory = name
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将处理后的数据追加到 categories 中
|
||
categories = append(categories, category)
|
||
}
|
||
|
||
return categories
|
||
}
|
||
|
||
// ImportCategoryData 导入商品分类到数据库
|
||
// 规则:分类只有3级,所有分类不允许名称完全相同
|
||
func ImportCategoryData(colsMap []map[string]interface{}, businessId uint32) error {
|
||
data := transCategoryData(colsMap)
|
||
|
||
if duplicateName, nFlag := hasDuplicateInDb(data); nFlag {
|
||
return fmt.Errorf("数据库已有此分类名称,不允许重复,请检查:[%v]", duplicateName)
|
||
}
|
||
|
||
for _, item := range data {
|
||
var firstCategory, secondCategory, threeCategory Category
|
||
if item.FirstCategory == "" { // 一级分类名称为空则跳过,继续遍历
|
||
continue
|
||
}
|
||
|
||
// 插入或获取一级分类
|
||
err := orm.Eloquent.Debug().FirstOrCreate(&firstCategory,
|
||
Category{
|
||
Name: item.FirstCategory,
|
||
Pid: 0,
|
||
Display: 1,
|
||
CooperativeBusinessId: businessId,
|
||
}).Error
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 插入或获取二级分类
|
||
if item.SecondCategory != "" {
|
||
err = orm.Eloquent.Debug().FirstOrCreate(&secondCategory,
|
||
Category{
|
||
Name: item.SecondCategory,
|
||
Pid: firstCategory.ID,
|
||
Display: 1,
|
||
CooperativeBusinessId: businessId,
|
||
}).Error
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
}
|
||
|
||
// 插入三级分类
|
||
if item.ThreeCategory != "" {
|
||
err = orm.Eloquent.Debug().FirstOrCreate(&threeCategory,
|
||
Category{
|
||
Name: item.ThreeCategory,
|
||
Pid: secondCategory.ID,
|
||
Display: 1,
|
||
CooperativeBusinessId: businessId,
|
||
}).Error
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 判断是否存在重复数据
|
||
func hasDuplicateInDb(data []CategoryExcel) (string, bool) {
|
||
for _, item := range data {
|
||
if exists := isCategoryExists(item.FirstCategory); exists {
|
||
return item.FirstCategory, true
|
||
}
|
||
|
||
if exists := isCategoryExists(item.SecondCategory); exists {
|
||
return item.SecondCategory, true
|
||
}
|
||
|
||
if exists := isCategoryExists(item.ThreeCategory); exists {
|
||
return item.ThreeCategory, true
|
||
}
|
||
}
|
||
|
||
return "", false
|
||
}
|
||
|
||
// 判断分类是否存在
|
||
func isCategoryExists(categoryName string) bool {
|
||
var count int64
|
||
orm.Eloquent.Debug().Model(&Category{}).
|
||
Where("name = ?", categoryName).
|
||
Count(&count)
|
||
|
||
return count > 0
|
||
}
|
||
|
||
// ImportCommodityData 导入商品资料
|
||
func ImportCommodityData(colsMap []map[string]interface{}, businessId uint32) error {
|
||
data, err := transCommodityData(colsMap)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var erpCommodities []ErpCommodity
|
||
erpCommodities, err = convertToErpCommodities(data)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
err = updateOrInsertCommodities(erpCommodities)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 将读取的excel数据转换成CommodityExcel struct
|
||
func transCommodityData(colsMap []map[string]interface{}) ([]CommodityExcel, error) {
|
||
var commodities []CommodityExcel
|
||
|
||
// 遍历 colsMap 进行类型断言和转换
|
||
for _, col := range colsMap {
|
||
var commodity CommodityExcel
|
||
|
||
// 将 col 转换为 JSON 字符串
|
||
jsonData, err := json.Marshal(col)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to marshal data to JSON: %v", err)
|
||
}
|
||
|
||
// 将 JSON 字符串反序列化为 CommodityExcel
|
||
if err := json.Unmarshal(jsonData, &commodity); err != nil {
|
||
return nil, fmt.Errorf("failed to unmarshal JSON to CommodityExcel: %v", err)
|
||
}
|
||
|
||
// 将处理后的数据追加到 commodities 中
|
||
commodities = append(commodities, commodity)
|
||
}
|
||
|
||
return commodities, nil
|
||
}
|
||
|
||
func convertToErpCommodities(data []CommodityExcel) ([]ErpCommodity, error) {
|
||
var erpCommodities []ErpCommodity
|
||
|
||
productCounter := tools.NewProductCounter()
|
||
for _, item := range data {
|
||
category, err := getCategoryByName(item.Category)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
list, err := GetSupplier(GetSupplierRequest{Name: item.SupplierName})
|
||
if len(list) == 0 || errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, fmt.Errorf("有供应商未导入,请检查:[%v]", item.SupplierName)
|
||
}
|
||
|
||
serialNumber := fmt.Sprintf("%s%04d", category.Number,
|
||
getNumberOnCategory(category.ID)+int64(productCounter.GetNextProductNumber(category.Number)))
|
||
fmt.Println("商品编号:", serialNumber)
|
||
|
||
erpCommodity, err := convertToErpCommodity(item, category, list[0], serialNumber)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
erpCommodities = append(erpCommodities, erpCommodity)
|
||
}
|
||
|
||
return erpCommodities, nil
|
||
}
|
||
|
||
// 查询商品分类
|
||
func getCategoryByName(name string) (*Category, error) {
|
||
var category Category
|
||
err := orm.Eloquent.Debug().Model(&Category{}).
|
||
Where("name = ?", name).First(&category).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, fmt.Errorf("有商品分类未导入,请检查:[%v]", name)
|
||
}
|
||
return &category, err
|
||
}
|
||
|
||
// 转换格式,匹配Commodity的model
|
||
func convertToErpCommodity(item CommodityExcel, category *Category, supplier *Supplier, serialNumber string) (ErpCommodity, error) {
|
||
brokerage1Float, err := strconv.ParseFloat(strings.TrimRight(item.SellBrokerage, "%"), 64) // 销售毛利
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("销售毛利转换有误:[%v]", err)
|
||
}
|
||
brokerage2Float, err := strconv.ParseFloat(strings.TrimRight(item.StaffBrokerage, "%"), 64) // 员工毛利提成
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("员工毛利提成转换有误:[%v]", err)
|
||
}
|
||
memberDiscountFloat, err := strconv.ParseFloat(item.MemberDiscount, 64) // 会员优惠
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("会员优惠转换有误:[%v]", err)
|
||
}
|
||
nRetailPrice, err := strconv.ParseUint(item.RetailPrice, 10, 32) // 指导零售价
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("指导零售价转换有误:[%v]", err)
|
||
}
|
||
nMinRetailPrice, err := strconv.ParseUint(item.MinRetailPrice, 10, 32) // 最低零售价
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("最低零售价转换有误:[%v]", err)
|
||
}
|
||
nStaffCostPrice, err := strconv.ParseUint(item.StaffCostPrice, 10, 32) // 员工成本价加价
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("员工成本价加价转换有误:[%v]", err)
|
||
}
|
||
nWholesalePrice, err := strconv.ParseUint(item.WholesalePrice, 10, 32) // 指导采购价
|
||
if err != nil {
|
||
return ErpCommodity{}, fmt.Errorf("指导采购价转换有误:[%v]", err)
|
||
}
|
||
|
||
//将是否串码转换为数字 1-无串码 2-串码(系统生成) 3-串码(手动添加)
|
||
var nIMEIType uint32
|
||
switch item.IMEIType {
|
||
case "串码类":
|
||
if item.SysGenerate == "是" {
|
||
nIMEIType = 2
|
||
} else if item.SysGenerate == "否" {
|
||
nIMEIType = 3
|
||
} else {
|
||
return ErpCommodity{}, errors.New("[系统生成串码]列有非法字段")
|
||
}
|
||
case "非串码":
|
||
nIMEIType = 1
|
||
default:
|
||
return ErpCommodity{}, errors.New("[是否串码]列有非法字段")
|
||
}
|
||
|
||
//serialNumber := fmt.Sprintf("%s%04d", category.Number, getNumberOnCategory(category.ID)+1)
|
||
//fmt.Println("商品编号:", serialNumber)
|
||
|
||
return ErpCommodity{
|
||
SerialNumber: serialNumber,
|
||
Name: item.Name,
|
||
ErpCategoryId: category.ID,
|
||
ErpCategoryName: item.Category,
|
||
IMEIType: nIMEIType,
|
||
ErpSupplierId: supplier.ID,
|
||
ErpSupplierName: item.SupplierName,
|
||
RetailPrice: uint32(nRetailPrice),
|
||
MinRetailPrice: uint32(nMinRetailPrice),
|
||
StaffCostPrice: uint32(nStaffCostPrice),
|
||
WholesalePrice: uint32(nWholesalePrice),
|
||
Brokerage1: brokerage1Float,
|
||
Brokerage2: brokerage2Float,
|
||
MemberDiscount: memberDiscountFloat,
|
||
Origin: item.Origin,
|
||
Remark: item.Remark,
|
||
}, nil
|
||
}
|
||
|
||
// 操作数据库,通过商品名称查找,没有则插入,有则更新
|
||
func updateOrInsertCommodities(erpCommodities []ErpCommodity) error {
|
||
for _, erpCommodity := range erpCommodities {
|
||
var existingCommodity ErpCommodity
|
||
result := orm.Eloquent.Where("name = ?", erpCommodity.Name).First(&existingCommodity)
|
||
|
||
if result.Error != nil {
|
||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||
orm.Eloquent.Create(&erpCommodity)
|
||
fmt.Println("Inserted:", erpCommodity.Name)
|
||
} else {
|
||
fmt.Println("Error:", result.Error)
|
||
return result.Error
|
||
}
|
||
} else {
|
||
orm.Eloquent.Model(&existingCommodity).Updates(&erpCommodity)
|
||
fmt.Println("Updated:", erpCommodity.Name)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 查询商品列表中同一类商品分类下商品的个数
|
||
func getNumberOnCategory(categoryId uint32) int64 {
|
||
var count int64
|
||
orm.Eloquent.Debug().Model(&ErpCommodity{}).
|
||
Where("erp_category_id = ?", categoryId).
|
||
Count(&count)
|
||
|
||
return count
|
||
}
|
||
|
||
// GenerateSerialNumber 生成商品编号
|
||
func GenerateSerialNumber(categoryId uint32) (string, error) {
|
||
var category Category
|
||
err := orm.Eloquent.Debug().Model(&Category{}).
|
||
Where("id = ?", categoryId).First(&category).Error
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return "", errors.New("未查到商品分类")
|
||
}
|
||
|
||
var count int64
|
||
orm.Eloquent.Debug().Model(&ErpCommodity{}).
|
||
Where("erp_category_id = ?", category.ID).
|
||
Count(&count)
|
||
|
||
serialNumber := fmt.Sprintf("%s%04d", category.Number, count+1)
|
||
fmt.Println("商品编号:", serialNumber)
|
||
return serialNumber, nil
|
||
}
|