1113 lines
31 KiB
Go
1113 lines
31 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/pkg/errors"
|
||
"psi/database"
|
||
"psi/models"
|
||
systemReq "psi/models/request"
|
||
systemRes "psi/models/response"
|
||
)
|
||
|
||
type LocationService struct{}
|
||
|
||
func (s *LocationService) GetLocationList(req systemReq.QueryLocationRequest, db ...*gorm.DB) ([]systemRes.LocationResponse, int64, error) {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
if req.WarehouseID == 0 {
|
||
return nil, 0, errors.New("仓库ID不能为空")
|
||
}
|
||
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize < 1 || req.PageSize > 100 {
|
||
req.PageSize = 20
|
||
}
|
||
|
||
query := databaseConn.Model(&models.Location{}).Where("warehouse_id = ? AND is_del = ?", req.WarehouseID, 0)
|
||
|
||
if req.Code != "" {
|
||
query = query.Where("code LIKE ?", "%"+req.Code+"%")
|
||
}
|
||
|
||
if req.Type != nil {
|
||
query = query.Where("type = ?", *req.Type)
|
||
}
|
||
|
||
if req.Status != nil {
|
||
query = query.Where("status = ?", *req.Status)
|
||
}
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, errors.New("查询库位总数失败")
|
||
}
|
||
|
||
var locations []models.Location
|
||
offset := (req.Page - 1) * req.PageSize
|
||
if err := query.Order("sort ASC,code ASC").Offset(offset).Limit(req.PageSize).Find(&locations).Error; err != nil {
|
||
return nil, 0, errors.New("查询库位列表失败")
|
||
}
|
||
|
||
responses := make([]systemRes.LocationResponse, 0, len(locations))
|
||
for _, loc := range locations {
|
||
resp := systemRes.ConvertLocationToResponse(loc)
|
||
responses = append(responses, resp)
|
||
}
|
||
|
||
return responses, total, nil
|
||
}
|
||
|
||
// GetAllLocationList 查询所有库位列表(仓库ID可选)
|
||
func (s *LocationService) GetAllLocationList(req systemReq.QueryAllLocationRequest, db ...*gorm.DB) ([]systemRes.LocationResponse, int64, error) {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
if req.PageSize < 1 || req.PageSize > 100 {
|
||
req.PageSize = 20
|
||
}
|
||
|
||
query := databaseConn.Model(&models.Location{}).Where("is_del = ?", 0)
|
||
|
||
if req.WarehouseID != nil && *req.WarehouseID > 0 {
|
||
query = query.Where("warehouse_id = ?", *req.WarehouseID)
|
||
}
|
||
if req.Code != "" {
|
||
query = query.Where("code LIKE ?", "%"+req.Code+"%")
|
||
}
|
||
if req.Type != nil {
|
||
query = query.Where("type = ?", *req.Type)
|
||
}
|
||
if req.Status != nil {
|
||
query = query.Where("status = ?", *req.Status)
|
||
}
|
||
|
||
var total int64
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, errors.New("查询库位总数失败")
|
||
}
|
||
|
||
var locations []models.Location
|
||
offset := (req.Page - 1) * req.PageSize
|
||
if err := query.Order("sort ASC,code ASC").Offset(offset).Limit(req.PageSize).Find(&locations).Error; err != nil {
|
||
return nil, 0, errors.New("查询库位列表失败")
|
||
}
|
||
|
||
responses := make([]systemRes.LocationResponse, 0, len(locations))
|
||
for _, loc := range locations {
|
||
resp := systemRes.ConvertLocationToResponse(loc)
|
||
responses = append(responses, resp)
|
||
}
|
||
|
||
return responses, total, nil
|
||
}
|
||
|
||
func (s *LocationService) GetLocationDetail(id int64, db ...*gorm.DB) (*systemRes.LocationResponse, error) {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
var location models.Location
|
||
if err := databaseConn.Where("id = ? AND is_del = ?", id, 0).First(&location).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New("库位不存在")
|
||
}
|
||
return nil, errors.New("查询库位失败")
|
||
}
|
||
|
||
resp := systemRes.ConvertLocationToResponse(location)
|
||
return &resp, nil
|
||
}
|
||
|
||
func (s *LocationService) GetLocationInfo(code, warehouseCode string, db ...*gorm.DB) (*systemRes.LocationResponse, error) {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
var warehouse models.Warehouse
|
||
if err := databaseConn.Where("code = ? AND is_del = ?", warehouseCode, 0).First(&warehouse).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New("仓库不存在")
|
||
}
|
||
return nil, errors.New("查询仓库失败")
|
||
}
|
||
|
||
var location models.Location
|
||
if err := databaseConn.Where("code = ? AND warehouse_id = ? AND is_del = ?", code, warehouse.ID, 0).First(&location).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New("库位不存在")
|
||
}
|
||
return nil, errors.New("查询库位失败")
|
||
}
|
||
|
||
resp := systemRes.ConvertLocationToResponse(location)
|
||
return &resp, nil
|
||
}
|
||
|
||
func (s *LocationService) CreateLocation(req systemReq.CreateLocationRequest, db ...*gorm.DB) (int64, error) {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
var warehouse models.Warehouse
|
||
if err := databaseConn.Where("id = ? AND is_del = ?", req.WarehouseID, 0).First(&warehouse).Error; err != nil {
|
||
return 0, errors.New("仓库不存在")
|
||
}
|
||
|
||
if len(req.Code) > 50 {
|
||
return 0, errors.New("库位编码长度不能超过50")
|
||
}
|
||
|
||
var existingLocation models.Location
|
||
err := databaseConn.Where("warehouse_id = ? AND code = ?", req.WarehouseID, req.Code).First(&existingLocation).Error
|
||
|
||
if err == nil {
|
||
if existingLocation.IsDel == 1 {
|
||
now := time.Now().Unix()
|
||
existingLocation.IsDel = 0
|
||
existingLocation.Type = req.Type
|
||
existingLocation.Capacity = req.Capacity
|
||
existingLocation.Status = req.Status
|
||
existingLocation.UpdatedAt = now
|
||
|
||
if updateErr := databaseConn.Save(&existingLocation).Error; updateErr != nil {
|
||
return 0, errors.New("恢复库位失败")
|
||
}
|
||
|
||
return existingLocation.ID, nil
|
||
} else {
|
||
return 0, errors.New("库位编码已存在")
|
||
}
|
||
} else if err == gorm.ErrRecordNotFound {
|
||
location := models.Location{
|
||
WarehouseID: req.WarehouseID,
|
||
Code: req.Code,
|
||
Type: req.Type,
|
||
Capacity: req.Capacity,
|
||
Status: req.Status,
|
||
CreatedAt: time.Now().Unix(),
|
||
UpdatedAt: time.Now().Unix(),
|
||
IsDel: 0,
|
||
}
|
||
|
||
if createErr := databaseConn.Create(&location).Error; createErr != nil {
|
||
return 0, errors.New("创建库位失败")
|
||
}
|
||
|
||
return location.ID, nil
|
||
} else {
|
||
return 0, errors.New("查询库位失败")
|
||
}
|
||
}
|
||
|
||
func (s *LocationService) BatchGenerateLocations(req systemReq.BatchGenerateLocationRequest, db ...*gorm.DB) (*systemRes.BatchGenerateResult, error) {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
result := &systemRes.BatchGenerateResult{
|
||
SuccessIDs: make([]int64, 0),
|
||
FailCodes: make([]string, 0),
|
||
}
|
||
|
||
var warehouse models.Warehouse
|
||
if err := databaseConn.Where("id = ? AND is_del = ?", req.WarehouseID, 0).First(&warehouse).Error; err != nil {
|
||
return result, errors.New("仓库不存在")
|
||
}
|
||
|
||
if len(req.Groups) > 5 {
|
||
return result, errors.New("最多支持5组")
|
||
}
|
||
|
||
now := time.Now().Unix()
|
||
allCodes := make([]string, 0)
|
||
locations := make([]models.Location, 0)
|
||
failedCodes := make([]string, 0)
|
||
existingCodes := make(map[string]bool)
|
||
|
||
groupValues := make([][]string, len(req.Groups))
|
||
for i, group := range req.Groups {
|
||
values, err := generateLevelValues(systemReq.GroupConfig{
|
||
FormatType: group.FormatType,
|
||
StartValue: group.StartValue,
|
||
EndValue: group.EndValue,
|
||
PaddingLen: group.PaddingLen,
|
||
})
|
||
if err != nil {
|
||
return result, errors.Wrapf(err, "第%d组配置错误", i+1)
|
||
}
|
||
groupValues[i] = values
|
||
}
|
||
|
||
codes := make([]string, 0)
|
||
var generate func(groupIndex int, prefix string)
|
||
generate = func(groupIndex int, prefix string) {
|
||
if groupIndex == len(groupValues) {
|
||
codes = append(codes, prefix)
|
||
return
|
||
}
|
||
|
||
separator := req.Groups[groupIndex].Separator
|
||
|
||
for _, value := range groupValues[groupIndex] {
|
||
newPrefix := prefix + value
|
||
// separator 放在本组值后面,只有下一组存在时才生效
|
||
if groupIndex+1 < len(groupValues) {
|
||
newPrefix += separator
|
||
}
|
||
generate(groupIndex+1, newPrefix)
|
||
}
|
||
}
|
||
|
||
generate(0, "")
|
||
|
||
for _, code := range codes {
|
||
if existingCodes[code] {
|
||
continue
|
||
}
|
||
|
||
var existingLocation models.Location
|
||
err := databaseConn.Where("warehouse_id = ? AND code = ?", req.WarehouseID, code).First(&existingLocation).Error
|
||
|
||
if err == nil {
|
||
existingCodes[code] = true
|
||
if existingLocation.IsDel == 1 {
|
||
failedCodes = append(failedCodes, fmt.Sprintf("%s:已删除可恢复", code))
|
||
} else {
|
||
failedCodes = append(failedCodes, fmt.Sprintf("%s:已存在", code))
|
||
}
|
||
continue
|
||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return result, errors.New("批量生成库位失败")
|
||
}
|
||
|
||
existingCodes[code] = true
|
||
allCodes = append(allCodes, code)
|
||
|
||
location := models.Location{
|
||
WarehouseID: req.WarehouseID,
|
||
Code: code,
|
||
Type: req.Type,
|
||
Capacity: req.Capacity,
|
||
Status: req.Status,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
IsDel: 0,
|
||
}
|
||
|
||
locations = append(locations, location)
|
||
}
|
||
|
||
result.TotalCount = len(allCodes)
|
||
result.GeneratedCode = allCodes
|
||
|
||
if len(allCodes) > 10000 {
|
||
return result, errors.New(fmt.Sprintf("生成的库位数量过多(%d个),请调整范围", len(allCodes)))
|
||
}
|
||
|
||
if len(locations) > 0 {
|
||
if err := databaseConn.Create(&locations).Error; err != nil {
|
||
return result, errors.New("批量创建库位失败")
|
||
}
|
||
|
||
for _, loc := range locations {
|
||
result.SuccessIDs = append(result.SuccessIDs, loc.ID)
|
||
}
|
||
}
|
||
|
||
result.SuccessCount = len(result.SuccessIDs)
|
||
result.FailCount = len(failedCodes)
|
||
result.FailCodes = failedCodes
|
||
|
||
if len(failedCodes) > 0 {
|
||
result.Message = fmt.Sprintf("成功创建%d个,失败%d个", result.SuccessCount, result.FailCount)
|
||
} else {
|
||
result.Message = fmt.Sprintf("成功创建%d个库位", result.SuccessCount)
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
func (s *LocationService) UpdateLocation(req systemReq.UpdateLocationRequest, db ...*gorm.DB) error {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
if len(req.IDs) == 0 {
|
||
return errors.New("请选择要更新的库位")
|
||
}
|
||
|
||
isSingleUpdate := len(req.IDs) == 1
|
||
|
||
updates := make(map[string]interface{})
|
||
updates["updated_at"] = time.Now().Unix()
|
||
|
||
if isSingleUpdate && req.Code != "" {
|
||
if len(req.Code) > 50 {
|
||
return errors.New("库位编码长度不能超过50")
|
||
}
|
||
|
||
var count int64
|
||
databaseConn.Model(&models.Location{}).
|
||
Where("warehouse_id = ? AND code = ? AND id != ? AND is_del = ?", req.WarehouseID, req.Code, req.IDs[0], 0).
|
||
Count(&count)
|
||
if count > 0 {
|
||
return errors.New("库位编码已存在")
|
||
}
|
||
|
||
updates["code"] = req.Code
|
||
}
|
||
|
||
if req.Type != nil {
|
||
if *req.Type < 1 || *req.Type > 5 {
|
||
return errors.New("库位类型无效")
|
||
}
|
||
updates["type"] = *req.Type
|
||
}
|
||
|
||
if req.Capacity != nil {
|
||
if *req.Capacity < 0 {
|
||
return errors.New("库位容量不能为负数")
|
||
}
|
||
|
||
var totalQuantity int64
|
||
databaseConn.Model(&models.InventoryDetail{}).
|
||
Where("location_id IN (?) AND is_del = ?", req.IDs, 0).
|
||
Select("COALESCE(SUM(quantity), 0)").
|
||
Scan(&totalQuantity)
|
||
|
||
if *req.Capacity < totalQuantity {
|
||
return errors.New(fmt.Sprintf("库位容量不能小于当前库存数量(%d)", totalQuantity))
|
||
}
|
||
|
||
updates["capacity"] = *req.Capacity
|
||
}
|
||
|
||
if req.Status != nil {
|
||
if *req.Status != 0 && *req.Status != 1 {
|
||
return errors.New("库位状态无效")
|
||
}
|
||
updates["status"] = *req.Status
|
||
}
|
||
|
||
if req.Sort != nil {
|
||
updates["sort"] = *req.Sort
|
||
}
|
||
|
||
if len(updates) <= 1 {
|
||
return errors.New("没有需要更新的字段")
|
||
}
|
||
|
||
if err := databaseConn.Model(&models.Location{}).
|
||
Where("id IN (?) AND is_del = ?", req.IDs, 0).
|
||
Updates(updates).Error; err != nil {
|
||
return errors.New("更新库位失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (s *LocationService) DeleteLocation(ids []int64, db ...*gorm.DB) error {
|
||
databaseConn := database.OptionalDB(db...)
|
||
|
||
if len(ids) == 0 {
|
||
return errors.New("请选择要删除的库位")
|
||
}
|
||
|
||
var count int64
|
||
databaseConn.Model(&models.InventoryDetail{}).
|
||
Where("location_id IN ? AND is_del = 0 AND available_quantity > 0", ids).
|
||
Count(&count)
|
||
if count > 0 {
|
||
return errors.New("库位下仍有库存商品,无法删除")
|
||
}
|
||
|
||
// 执行真正的物理删除
|
||
if err := databaseConn.Where("id IN ? AND is_del = ?", ids, 0).Delete(&models.Location{}).Error; err != nil {
|
||
return errors.New("删除库位失败")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// SyncLocations 同步库位数据(包含物流模板、仓库、货区)
|
||
func (s *LocationService) SyncLocations(req systemReq.SyncLocationRequest) (*systemRes.SyncLocationResponse, error) {
|
||
databaseConn, err := database.GetTenantDB(req.UserID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取数据库连接失败: %v", err)
|
||
}
|
||
|
||
result := &systemRes.SyncLocationResponse{
|
||
SuccessCodes: make([]string, 0),
|
||
FailCodes: make([]string, 0),
|
||
}
|
||
|
||
if len(req.Data) == 0 {
|
||
return result, errors.New("数据不能为空")
|
||
}
|
||
|
||
now := time.Now().Unix()
|
||
tx := databaseConn.Begin()
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
successCount := 0
|
||
failCount := 0
|
||
var failMessages []string
|
||
|
||
for _, area := range req.Data {
|
||
areaCode := area.Code
|
||
|
||
if areaCode == "" {
|
||
failCount++
|
||
failMessages = append(failMessages, "区域编码为空")
|
||
continue
|
||
}
|
||
|
||
logisticsID, err := s.createOrUpdateLogistics(tx, area.Logistics, req.UserID, now)
|
||
if err != nil {
|
||
failCount++
|
||
failMessages = append(failMessages, fmt.Sprintf("%s: 创建物流模板失败-%v", areaCode, err))
|
||
continue
|
||
}
|
||
|
||
warehouseID, err := s.createOrUpdateWarehouse(tx, area, logisticsID, req.UserID, now)
|
||
if err != nil {
|
||
failCount++
|
||
failMessages = append(failMessages, fmt.Sprintf("%s: 创建仓库失败-%v", areaCode, err))
|
||
continue
|
||
}
|
||
|
||
locationResult, err := s.batchCreateLocations(tx, warehouseID, area.Data, now)
|
||
if err != nil {
|
||
failCount++
|
||
failMessages = append(failMessages, fmt.Sprintf("%s: 创建货区失败-%v", areaCode, err))
|
||
continue
|
||
}
|
||
|
||
successCount += locationResult.SuccessCount
|
||
failCount += locationResult.FailCount
|
||
if len(locationResult.FailCodes) > 0 {
|
||
for _, failCode := range locationResult.FailCodes {
|
||
failMessages = append(failMessages, fmt.Sprintf("%s-%s", areaCode, failCode))
|
||
}
|
||
}
|
||
result.SuccessCodes = append(result.SuccessCodes, locationResult.SuccessCodes...)
|
||
}
|
||
|
||
if failCount > 0 || len(failMessages) > 0 {
|
||
if err := tx.Commit().Error; err != nil {
|
||
return nil, fmt.Errorf("提交事务失败: %v", err)
|
||
}
|
||
result.FailCodes = failMessages
|
||
result.SuccessCount = successCount
|
||
result.FailCount = failCount
|
||
if failCount > 0 {
|
||
result.Message = fmt.Sprintf("同步完成:成功%d个,失败%d个", successCount, failCount)
|
||
} else {
|
||
result.Message = fmt.Sprintf("同步完成:全部成功,共%d个", successCount)
|
||
}
|
||
} else {
|
||
if err := tx.Commit().Error; err != nil {
|
||
return nil, fmt.Errorf("提交事务失败: %v", err)
|
||
}
|
||
result.SuccessCount = successCount
|
||
result.FailCount = failCount
|
||
result.Message = fmt.Sprintf("同步完成:全部成功,共%d个", successCount)
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
func (s *LocationService) createOrUpdateLogistics(tx *gorm.DB, logisticsReq systemReq.SyncLogisticsRequest, userID int64, now int64) (int64, error) {
|
||
var logistics models.Logistics
|
||
|
||
err := tx.Where("id = ?", logisticsReq.Id).First(&logistics).Error
|
||
|
||
if err == gorm.ErrRecordNotFound {
|
||
createTime, _ := time.Parse(time.RFC3339, logisticsReq.CreateTime)
|
||
updateTime, _ := time.Parse(time.RFC3339, logisticsReq.UpdateTime)
|
||
|
||
newLogistics := models.Logistics{
|
||
Id: uint64(logisticsReq.Id),
|
||
TemplateName: logisticsReq.TemplateName,
|
||
DeliveryProvince: logisticsReq.DeliveryProvince,
|
||
DeliveryCity: logisticsReq.DeliveryCity,
|
||
DeliveryArea: logisticsReq.DeliveryArea,
|
||
DeliveryAddress: logisticsReq.DeliveryAddress,
|
||
PricingMethod: logisticsReq.PricingMethod,
|
||
Shipping: logisticsReq.Shipping,
|
||
FirWbv: logisticsReq.FirWbv,
|
||
FirPrice: logisticsReq.FirPrice,
|
||
ContinueWbv: logisticsReq.ContinueWbv,
|
||
ContinuePrice: logisticsReq.ContinuePrice,
|
||
CreateBy: uint64(userID),
|
||
CreateTime: &createTime,
|
||
UpdateBy: uint64(userID),
|
||
UpdateTime: &updateTime,
|
||
Status: logisticsReq.Status,
|
||
DelFlag: "0",
|
||
TenantId: logisticsReq.TenantId,
|
||
CreateDept: uint64(logisticsReq.CreateDept),
|
||
ShippingRange: logisticsReq.ShippingRange,
|
||
WarehouseId: 0,
|
||
Remark: logisticsReq.Remark,
|
||
PhoneNumber: uint64(logisticsReq.PhoneNumber),
|
||
Contact: logisticsReq.Contact,
|
||
FullAddress: logisticsReq.FullAddress,
|
||
}
|
||
|
||
if err := tx.Create(&newLogistics).Error; err != nil {
|
||
return 0, errors.New("创建物流模板失败")
|
||
}
|
||
|
||
return int64(newLogistics.Id), nil
|
||
} else if err != nil {
|
||
return 0, errors.New("查询物流模板失败")
|
||
} else {
|
||
updateTime, _ := time.Parse(time.RFC3339, logisticsReq.UpdateTime)
|
||
|
||
logistics.TemplateName = logisticsReq.TemplateName
|
||
logistics.DeliveryProvince = logisticsReq.DeliveryProvince
|
||
logistics.DeliveryCity = logisticsReq.DeliveryCity
|
||
logistics.DeliveryArea = logisticsReq.DeliveryArea
|
||
logistics.DeliveryAddress = logisticsReq.DeliveryAddress
|
||
logistics.PricingMethod = logisticsReq.PricingMethod
|
||
logistics.Shipping = logisticsReq.Shipping
|
||
logistics.FirWbv = logisticsReq.FirWbv
|
||
logistics.FirPrice = logisticsReq.FirPrice
|
||
logistics.ContinueWbv = logisticsReq.ContinueWbv
|
||
logistics.ContinuePrice = logisticsReq.ContinuePrice
|
||
logistics.UpdateBy = uint64(userID)
|
||
logistics.UpdateTime = &updateTime
|
||
logistics.Status = logisticsReq.Status
|
||
logistics.ShippingRange = logisticsReq.ShippingRange
|
||
logistics.Remark = logisticsReq.Remark
|
||
logistics.PhoneNumber = uint64(logisticsReq.PhoneNumber)
|
||
logistics.Contact = logisticsReq.Contact
|
||
logistics.FullAddress = logisticsReq.FullAddress
|
||
|
||
if err := tx.Save(&logistics).Error; err != nil {
|
||
return 0, errors.New("更新物流模板失败")
|
||
}
|
||
|
||
return int64(logistics.Id), nil
|
||
}
|
||
}
|
||
|
||
func (s *LocationService) createOrUpdateWarehouse(tx *gorm.DB, area systemReq.SyncLocationAreaRequest, logisticsID int64, userID int64, now int64) (int64, error) {
|
||
var warehouse models.Warehouse
|
||
|
||
err := tx.Where("code = ? AND is_del = ?", area.Code, 0).First(&warehouse).Error
|
||
|
||
if err == gorm.ErrRecordNotFound {
|
||
newWarehouse := models.Warehouse{
|
||
LogisticsID: logisticsID,
|
||
Code: area.Code,
|
||
Name: area.Name,
|
||
Type: 1,
|
||
ContactPerson: area.Logistics.Contact,
|
||
ContactPhone: fmt.Sprintf("%d", area.Logistics.PhoneNumber),
|
||
Province: area.Logistics.DeliveryProvince,
|
||
City: area.Logistics.DeliveryCity,
|
||
District: area.Logistics.DeliveryArea,
|
||
Address: area.Logistics.FullAddress,
|
||
Status: 1,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
IsDel: 0,
|
||
}
|
||
|
||
if err := tx.Create(&newWarehouse).Error; err != nil {
|
||
return 0, errors.New("创建仓库失败")
|
||
}
|
||
|
||
return newWarehouse.ID, nil
|
||
} else if err != nil {
|
||
return 0, errors.New("查询仓库失败")
|
||
} else {
|
||
warehouse.LogisticsID = logisticsID
|
||
warehouse.Name = area.Name
|
||
warehouse.ContactPerson = area.Logistics.Contact
|
||
warehouse.ContactPhone = fmt.Sprintf("%d", area.Logistics.PhoneNumber)
|
||
warehouse.Province = area.Logistics.DeliveryProvince
|
||
warehouse.City = area.Logistics.DeliveryCity
|
||
warehouse.District = area.Logistics.DeliveryArea
|
||
warehouse.Address = area.Logistics.FullAddress
|
||
warehouse.UpdatedAt = now
|
||
|
||
if err := tx.Save(&warehouse).Error; err != nil {
|
||
return 0, errors.New("更新仓库失败")
|
||
}
|
||
|
||
return warehouse.ID, nil
|
||
}
|
||
}
|
||
|
||
func (s *LocationService) batchCreateLocations(tx *gorm.DB, warehouseID int64, items []systemReq.SyncLocationItemRequest, now int64) (*systemRes.SyncLocationResponse, error) {
|
||
result := &systemRes.SyncLocationResponse{
|
||
SuccessCodes: make([]string, 0),
|
||
FailCodes: make([]string, 0),
|
||
}
|
||
|
||
if len(items) == 0 {
|
||
return result, nil
|
||
}
|
||
|
||
codes := make([]string, 0, len(items))
|
||
codeMap := make(map[string]systemReq.SyncLocationItemRequest)
|
||
|
||
for _, item := range items {
|
||
if item.Code == "" {
|
||
result.FailCodes = append(result.FailCodes, "编码为空")
|
||
continue
|
||
}
|
||
codes = append(codes, item.Code)
|
||
codeMap[item.Code] = item
|
||
}
|
||
|
||
if len(codes) == 0 {
|
||
result.SuccessCount = 0
|
||
result.FailCount = len(result.FailCodes)
|
||
return result, nil
|
||
}
|
||
|
||
var existingLocations []models.Location
|
||
if err := tx.Where("warehouse_id = ? AND code IN ?", warehouseID, codes).Find(&existingLocations).Error; err != nil {
|
||
return result, errors.New("查询库位失败")
|
||
}
|
||
|
||
existingCodeMap := make(map[string]models.Location)
|
||
for _, loc := range existingLocations {
|
||
existingCodeMap[loc.Code] = loc
|
||
}
|
||
|
||
newLocations := make([]models.Location, 0)
|
||
updateLocations := make([]models.Location, 0)
|
||
successCodes := make([]string, 0)
|
||
|
||
for _, code := range codes {
|
||
item := codeMap[code]
|
||
|
||
if existingLoc, exists := existingCodeMap[code]; exists {
|
||
existingLoc.Capacity = item.SheQuantityMax
|
||
existingLoc.UpdatedAt = now
|
||
updateLocations = append(updateLocations, existingLoc)
|
||
successCodes = append(successCodes, code)
|
||
} else {
|
||
newLocation := models.Location{
|
||
WarehouseID: warehouseID,
|
||
Code: item.Code,
|
||
Type: 1,
|
||
Capacity: item.SheQuantityMax,
|
||
Status: 1,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
IsDel: 0,
|
||
}
|
||
newLocations = append(newLocations, newLocation)
|
||
successCodes = append(successCodes, code)
|
||
}
|
||
}
|
||
|
||
if len(newLocations) > 0 {
|
||
if err := tx.Create(&newLocations).Error; err != nil {
|
||
return result, errors.New("批量创建库位失败")
|
||
}
|
||
}
|
||
|
||
if len(updateLocations) > 0 {
|
||
for i := range updateLocations {
|
||
if err := tx.Save(&updateLocations[i]).Error; err != nil {
|
||
result.FailCodes = append(result.FailCodes, updateLocations[i].Code+":更新失败")
|
||
for j, code := range successCodes {
|
||
if code == updateLocations[i].Code {
|
||
successCodes = append(successCodes[:j], successCodes[j+1:]...)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
result.SuccessCodes = successCodes
|
||
result.SuccessCount = len(successCodes)
|
||
result.FailCount = len(result.FailCodes)
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// SyncGoods 同步商品数据并创建库存
|
||
func (s *LocationService) SyncGoods(req systemReq.SyncGoodsRequest) (*systemRes.SyncGoodsResponse, error) {
|
||
databaseConn, err := database.GetTenantDB(req.UserID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取数据库连接失败: %v", err)
|
||
}
|
||
|
||
result := &systemRes.SyncGoodsResponse{
|
||
SuccessCount: 0,
|
||
FailCount: 0,
|
||
Message: "",
|
||
}
|
||
|
||
if len(req.Data) == 0 {
|
||
result.Message = "数据为空"
|
||
return result, nil
|
||
}
|
||
|
||
now := time.Now().Unix()
|
||
successCount := 0
|
||
failCount := 0
|
||
var failMessages []string
|
||
|
||
tx := databaseConn.Begin()
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
locationCodes := make([]string, 0, len(req.Data))
|
||
for _, item := range req.Data {
|
||
locationCodes = append(locationCodes, item.LocationCode)
|
||
}
|
||
|
||
var locations []models.Location
|
||
if err := tx.Where("code IN ? AND is_del = ?", locationCodes, 0).Find(&locations).Error; err != nil {
|
||
tx.Rollback()
|
||
return nil, fmt.Errorf("查询库位失败: %v", err)
|
||
}
|
||
|
||
locationMap := make(map[string]models.Location)
|
||
for _, loc := range locations {
|
||
locationMap[loc.Code] = loc
|
||
}
|
||
|
||
newProducts := make([]models.Product, 0, len(req.Data))
|
||
for _, item := range req.Data {
|
||
_, exists := locationMap[item.LocationCode]
|
||
if !exists {
|
||
failCount++
|
||
failMessages = append(failMessages, fmt.Sprintf("%s: 库位%s不存在", item.GoodsName, item.LocationCode))
|
||
continue
|
||
}
|
||
|
||
var liveImage datatypes.JSON
|
||
if len(item.LiveImage) > 0 {
|
||
jsonBytes, _ := json.Marshal(item.LiveImage)
|
||
liveImage = jsonBytes
|
||
} else {
|
||
liveImage = datatypes.JSON("[]")
|
||
}
|
||
|
||
product := models.Product{
|
||
CategoryID: 1,
|
||
StandardProductID: 1,
|
||
Name: item.GoodsName,
|
||
Appearance: s.parseAppearance(item.Appearance),
|
||
Barcode: item.ISBN,
|
||
Price: 0,
|
||
SalePrice: item.Price,
|
||
Cost: 0,
|
||
LiveImage: liveImage,
|
||
IsBatchManaged: 0,
|
||
IsShelfLifeManaged: 0,
|
||
Status: 1,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
IsDel: 0,
|
||
}
|
||
newProducts = append(newProducts, product)
|
||
}
|
||
|
||
if len(newProducts) > 0 {
|
||
if err := tx.Create(&newProducts).Error; err != nil {
|
||
tx.Rollback()
|
||
return nil, fmt.Errorf("批量创建商品失败: %v", err)
|
||
}
|
||
}
|
||
|
||
newInventories := make([]models.Inventory, 0, len(newProducts))
|
||
newInventoryDetails := make([]models.InventoryDetail, 0, len(newProducts))
|
||
|
||
for i, item := range req.Data {
|
||
loc, exists := locationMap[item.LocationCode]
|
||
if !exists {
|
||
failCount++
|
||
continue
|
||
}
|
||
|
||
if i >= len(newProducts) {
|
||
failCount++
|
||
continue
|
||
}
|
||
|
||
product := newProducts[i]
|
||
quantity := item.Inventory
|
||
|
||
inventory := models.Inventory{
|
||
WarehouseID: loc.WarehouseID,
|
||
ProductID: product.ID,
|
||
BatchNo: "",
|
||
ProductionDate: 0,
|
||
ExpiryDate: 0,
|
||
Quantity: quantity,
|
||
LockedQuantity: 0,
|
||
AvailableQuantity: quantity,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
IsDel: 0,
|
||
}
|
||
newInventories = append(newInventories, inventory)
|
||
|
||
inventoryDetail := models.InventoryDetail{
|
||
WarehouseID: loc.WarehouseID,
|
||
LocationID: loc.ID,
|
||
ProductID: product.ID,
|
||
BatchNo: "",
|
||
ProductionDate: 0,
|
||
ExpiryDate: 0,
|
||
Quantity: quantity,
|
||
LockedQuantity: 0,
|
||
AvailableQuantity: quantity,
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
IsDel: 0,
|
||
}
|
||
newInventoryDetails = append(newInventoryDetails, inventoryDetail)
|
||
|
||
successCount++
|
||
}
|
||
|
||
if len(newInventories) > 0 {
|
||
if err := tx.Create(&newInventories).Error; err != nil {
|
||
tx.Rollback()
|
||
return nil, fmt.Errorf("批量创建库存汇总失败: %v", err)
|
||
}
|
||
}
|
||
|
||
if len(newInventoryDetails) > 0 {
|
||
if err := tx.Create(&newInventoryDetails).Error; err != nil {
|
||
tx.Rollback()
|
||
return nil, fmt.Errorf("批量创建库存明细失败: %v", err)
|
||
}
|
||
}
|
||
|
||
if err := tx.Commit().Error; err != nil {
|
||
return nil, fmt.Errorf("提交事务失败: %v", err)
|
||
}
|
||
|
||
result.SuccessCount = successCount
|
||
result.FailCount = failCount
|
||
|
||
if failCount > 0 {
|
||
result.Message = fmt.Sprintf("同步完成:成功%d个,失败%d个", successCount, failCount)
|
||
if len(failMessages) <= 5 {
|
||
result.Message += fmt.Sprintf(",失败详情:%v", failMessages)
|
||
}
|
||
} else {
|
||
result.Message = fmt.Sprintf("同步完成:全部成功,共%d个", successCount)
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// parseAppearance 解析品相字符串为整数
|
||
func (s *LocationService) parseAppearance(appearance string) int64 {
|
||
var result int64
|
||
fmt.Sscanf(appearance, "%d", &result)
|
||
return result
|
||
}
|
||
|
||
func generateLevelValues(config systemReq.GroupConfig) ([]string, error) {
|
||
values := make([]string, 0)
|
||
|
||
switch config.FormatType {
|
||
case 1:
|
||
startChar := strings.ToUpper(config.StartValue)
|
||
endChar := strings.ToUpper(config.EndValue)
|
||
|
||
if len(startChar) != 1 || len(endChar) != 1 {
|
||
return nil, errors.New("字母格式起始值和结束值必须是单个字母")
|
||
}
|
||
|
||
startNum := charToNum(startChar[0])
|
||
endNum := charToNum(endChar[0])
|
||
|
||
if startNum == -1 || endNum == -1 {
|
||
return nil, errors.New("无效的字母范围")
|
||
}
|
||
|
||
if startNum > endNum {
|
||
return nil, errors.New("起始字母不能大于结束字母")
|
||
}
|
||
|
||
for i := startNum; i <= endNum; i++ {
|
||
val := string(numToChar(i))
|
||
paddingLen := config.PaddingLen
|
||
if paddingLen > 0 {
|
||
val = fmt.Sprintf("%0"+strconv.Itoa(paddingLen)+"s", val)
|
||
}
|
||
values = append(values, val)
|
||
}
|
||
|
||
case 2:
|
||
startNum, err := strconv.Atoi(config.StartValue)
|
||
if err != nil {
|
||
return nil, errors.New("数字格式起始值必须是数字")
|
||
}
|
||
|
||
endNum, err := strconv.Atoi(config.EndValue)
|
||
if err != nil {
|
||
return nil, errors.New("数字格式结束值必须是数字")
|
||
}
|
||
|
||
if startNum > endNum {
|
||
return nil, errors.New("起始数字不能大于结束数字")
|
||
}
|
||
|
||
for i := startNum; i <= endNum; i++ {
|
||
val := strconv.Itoa(i)
|
||
paddingLen := config.PaddingLen
|
||
if paddingLen > 0 {
|
||
val = fmt.Sprintf("%0"+strconv.Itoa(paddingLen)+"d", i)
|
||
}
|
||
values = append(values, val)
|
||
}
|
||
|
||
case 3:
|
||
parts := strings.Split(config.StartValue, ",")
|
||
if len(parts) != 2 {
|
||
return nil, errors.New("字母+数字格式起始值格式错误,应为:字母,数字")
|
||
}
|
||
|
||
startChar := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||
startNumStr := strings.TrimSpace(parts[1])
|
||
|
||
parts = strings.Split(config.EndValue, ",")
|
||
if len(parts) != 2 {
|
||
return nil, errors.New("字母+数字格式结束值格式错误,应为:字母,数字")
|
||
}
|
||
|
||
endChar := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||
endNumStr := strings.TrimSpace(parts[1])
|
||
|
||
if len(startChar) != 1 || len(endChar) != 1 {
|
||
return nil, errors.New("字母部分必须是单个字母")
|
||
}
|
||
|
||
startCharNum := charToNum(startChar[0])
|
||
endCharNum := charToNum(endChar[0])
|
||
|
||
if startCharNum == -1 || endCharNum == -1 {
|
||
return nil, errors.New("无效的字母")
|
||
}
|
||
|
||
startNum, err := strconv.Atoi(startNumStr)
|
||
if err != nil {
|
||
return nil, errors.New("起始数字部分必须是数字")
|
||
}
|
||
|
||
endNum, err := strconv.Atoi(endNumStr)
|
||
if err != nil {
|
||
return nil, errors.New("结束数字部分必须是数字")
|
||
}
|
||
|
||
if startCharNum > endCharNum {
|
||
return nil, errors.New("起始字母不能大于结束字母")
|
||
}
|
||
|
||
for c := startCharNum; c <= endCharNum; c++ {
|
||
for n := startNum; n <= endNum; n++ {
|
||
charVal := string(numToChar(c))
|
||
numVal := strconv.Itoa(n)
|
||
if config.PaddingLen > 0 {
|
||
numVal = fmt.Sprintf("%0"+strconv.Itoa(config.PaddingLen)+"d", n)
|
||
}
|
||
values = append(values, charVal+numVal)
|
||
}
|
||
}
|
||
case 4:
|
||
parts := strings.Split(config.StartValue, ",")
|
||
if len(parts) != 2 {
|
||
return nil, errors.New("数字+字母格式起始值格式错误,应为:数字,字母")
|
||
}
|
||
|
||
startNumStr := strings.TrimSpace(parts[0])
|
||
startChar := strings.ToUpper(strings.TrimSpace(parts[1]))
|
||
|
||
parts = strings.Split(config.EndValue, ",")
|
||
if len(parts) != 2 {
|
||
return nil, errors.New("数字+字母格式结束值格式错误,应为:数字,字母")
|
||
}
|
||
|
||
endNumStr := strings.TrimSpace(parts[0])
|
||
endChar := strings.ToUpper(strings.TrimSpace(parts[1]))
|
||
|
||
if len(startChar) != 1 || len(endChar) != 1 {
|
||
return nil, errors.New("字母部分必须是单个字母")
|
||
}
|
||
|
||
startNum, err := strconv.Atoi(startNumStr)
|
||
if err != nil {
|
||
return nil, errors.New("起始数字部分必须是数字")
|
||
}
|
||
|
||
endNum, err := strconv.Atoi(endNumStr)
|
||
if err != nil {
|
||
return nil, errors.New("结束数字部分必须是数字")
|
||
}
|
||
|
||
startCharNum := charToNum(startChar[0])
|
||
endCharNum := charToNum(endChar[0])
|
||
|
||
if startCharNum == -1 || endCharNum == -1 {
|
||
return nil, errors.New("无效的字母")
|
||
}
|
||
|
||
if startNum > endNum {
|
||
return nil, errors.New("起始数字不能大于结束数字")
|
||
}
|
||
|
||
for n := startNum; n <= endNum; n++ {
|
||
for c := startCharNum; c <= endCharNum; c++ {
|
||
numVal := strconv.Itoa(n)
|
||
if config.PaddingLen > 0 {
|
||
numVal = fmt.Sprintf("%0"+strconv.Itoa(config.PaddingLen)+"d", n)
|
||
}
|
||
charVal := string(numToChar(c))
|
||
values = append(values, numVal+charVal)
|
||
}
|
||
}
|
||
|
||
default:
|
||
return nil, errors.New("不支持的格式类型")
|
||
}
|
||
|
||
if len(values) == 0 {
|
||
return nil, errors.New("生成的值为空")
|
||
}
|
||
|
||
return values, nil
|
||
}
|
||
|
||
func charToNum(c byte) int {
|
||
if c >= 'A' && c <= 'Z' {
|
||
return int(c - 'A')
|
||
}
|
||
if c >= 'a' && c <= 'z' {
|
||
return int(c - 'a')
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func numToChar(n int) byte {
|
||
if n >= 0 && n < 26 {
|
||
return byte('A' + n)
|
||
}
|
||
return 0
|
||
}
|