无需发货 手动发货 仓库导入导出
This commit is contained in:
parent
5e638b826d
commit
aa9b357f16
10
.idea/go.imports.xml
generated
Normal file
10
.idea/go.imports.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@ -17,6 +17,7 @@ type Config struct {
|
||||
Log LogConfig `yaml:"log"`
|
||||
ES ESConfig `yaml:"es"`
|
||||
OCR OCRConfig `yaml:"ocr"`
|
||||
Font FontConfig `yaml:"font"`
|
||||
ExternalAPI ExternalAPIConfig `yaml:"external_api"`
|
||||
Wangdian WangdianConfig `yaml:"wangdian"`
|
||||
}
|
||||
@ -67,6 +68,10 @@ type OCRConfig struct {
|
||||
ExeUrl string `yaml:"exe_url"`
|
||||
}
|
||||
|
||||
type FontConfig struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
type ExternalAPIConfig struct {
|
||||
SyncProductURL string `yaml:"sync_product_url"`
|
||||
SyncTaskURL string `yaml:"sync_task_url"`
|
||||
|
||||
@ -2,11 +2,15 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
|
||||
"psi/constant"
|
||||
"psi/database"
|
||||
"psi/models"
|
||||
systemReq "psi/models/request"
|
||||
systemRes "psi/models/response"
|
||||
"psi/service"
|
||||
@ -17,6 +21,79 @@ type LocationApi struct{}
|
||||
|
||||
var locationService = service.LocationService{}
|
||||
|
||||
// LocationToCsv 导出库位到Excel
|
||||
// POST /api/location/to-csv
|
||||
func (r *LocationApi) LocationToCsv(c *gin.Context) {
|
||||
var req systemReq.ExportLocationRequest
|
||||
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelRequest, "导出库位请求参数异常", fmt.Errorf("参数错误: %v", err), c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
fileBytes, fileName, total, err := locationService.ExportLocationToCSV(req, database.GetDB(c))
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "导出库位异常", err, c, req)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": gin.H{"total": total},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Description", "File Transfer")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileBytes)
|
||||
}
|
||||
|
||||
// CsvToLocation 从CSV/Excel导入库位
|
||||
// POST /api/location/csv-to
|
||||
func (r *LocationApi) CsvToLocation(c *gin.Context) {
|
||||
var req systemReq.ImportLocationRequest
|
||||
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelRequest, "导入库位请求参数异常", fmt.Errorf("参数错误: %v", err), c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelRequest, "导入库位未上传文件", fmt.Errorf("未上传文件: %v", err), c, nil)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelRequest, "导入库位读取文件失败", fmt.Errorf("读取文件失败: %v", err), c, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 从JWT中获取当前用户信息
|
||||
employeeObj, exists := c.Get("employee")
|
||||
if !exists {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelRequest, "导入库位未登录", fmt.Errorf("未获取到用户信息"), c, nil)
|
||||
return
|
||||
}
|
||||
currentEmployee := employeeObj.(models.Employee)
|
||||
|
||||
result, err := locationService.ImportLocationFromCSV(currentEmployee.ID, req.WarehouseID, fileBytes)
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "导入库位失败", err, c, req)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "导入完成",
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *LocationApi) GetLocationList(c *gin.Context) {
|
||||
var req systemReq.QueryLocationRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
|
||||
@ -864,6 +864,42 @@ func getPostFormInt64(c *gin.Context, key string) (int64, bool, error) {
|
||||
return val, true, nil
|
||||
}
|
||||
|
||||
// DirectShip 无需发货:直接将订单状态改为已发货
|
||||
func (r *ProcessApi) DirectShip(c *gin.Context) {
|
||||
var req systemReq.DirectShipRequest
|
||||
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
ValidAndFail(constant.LoggerChannelRequest, "无需发货请求参数异常", "参数错误: "+err.Error(), c, err)
|
||||
return
|
||||
}
|
||||
|
||||
err := processService.DirectShip(req, utils.GetUserInfo(c).ID, database.GetDB(c))
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "无需发货异常", err, c, req)
|
||||
return
|
||||
}
|
||||
|
||||
systemRes.OkWithMessage("已标记为已发货", c)
|
||||
}
|
||||
|
||||
// ManualShip 手动填写发货:填写快递单号后直接将订单状态改为已发货
|
||||
func (r *ProcessApi) ManualShip(c *gin.Context) {
|
||||
var req systemReq.ManualShipRequest
|
||||
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
ValidAndFail(constant.LoggerChannelRequest, "手动发货请求参数异常", "参数错误: "+err.Error(), c, err)
|
||||
return
|
||||
}
|
||||
|
||||
err := processService.ManualShip(req, utils.GetUserInfo(c).ID, database.GetDB(c))
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "手动发货异常", err, c, req)
|
||||
return
|
||||
}
|
||||
|
||||
systemRes.OkWithMessage("手动发货成功", c)
|
||||
}
|
||||
|
||||
// logAndFail 业务统一的错误日志和响应函数
|
||||
func logAndFail(channel string, source, errMsg string, c *gin.Context) {
|
||||
utils.ErrorLog(channel, logrus.Fields{
|
||||
|
||||
@ -77,3 +77,25 @@ func (r *ShippingApi) GetShippingOrderDetailList(c *gin.Context) {
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
|
||||
// GetShippingOrderFlatList 获取发货单平铺列表(每条记录对应一个商品明细)
|
||||
func (r *ShippingApi) GetShippingOrderFlatList(c *gin.Context) {
|
||||
var req systemReq.GetShippingOrderFlatListRequest
|
||||
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
ValidAndFail(constant.LoggerChannelRequest, "发货单平铺列表请求参数异常", "参数错误: "+err.Error(), c, err)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := utils.GetUserInfo(c)
|
||||
result, err := shippingService.GetShippingOrderFlatList(req, userInfo.ID, userInfo.Role, database.GetDB(c))
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "发货单平铺列表异常", err, c, req)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,15 +5,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"psi/constant"
|
||||
"psi/database"
|
||||
systemReq "psi/models/request"
|
||||
systemRes "psi/models/response"
|
||||
"psi/service"
|
||||
"psi/utils"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WarehouseApi struct{}
|
||||
@ -137,66 +134,6 @@ func (r *WarehouseApi) DeleteWarehouse(c *gin.Context) {
|
||||
systemRes.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
// LocationToCsv 导出库位
|
||||
func (r *LocationApi) LocationToCsv(c *gin.Context) {
|
||||
var req systemReq.ExportLocationRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
ValidAndFail(constant.LoggerChannelRequest, "导出库位请求参数异常", "参数错误: "+err.Error(), c, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := locationService.ExportLocations(req, database.GetDB(c))
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "导出库位异常", err, c, req)
|
||||
return
|
||||
}
|
||||
|
||||
systemRes.OkWithDetailed(result, "导出成功", c)
|
||||
}
|
||||
|
||||
// CsvToLocation 导入库位
|
||||
func (r *LocationApi) CsvToLocation(c *gin.Context) {
|
||||
var req systemReq.ImportLocationRequest
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
ValidAndFail(constant.LoggerChannelRequest, "导入库位请求参数异常", "参数错误: "+err.Error(), c, err)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
systemRes.FailWithValidateMessage("请上传文件", c)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(strings.ToLower(file.Filename), ".xlsx") && !strings.HasSuffix(strings.ToLower(file.Filename), ".xls") {
|
||||
systemRes.FailWithValidateMessage("只支持Excel文件格式(.xlsx, .xls)", c)
|
||||
return
|
||||
}
|
||||
|
||||
filePath := fmt.Sprintf("excel/import_%s_%d.xlsx", time.Now().Format("20060102150405"), time.Now().UnixNano())
|
||||
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "保存上传文件异常", err, c, gin.H{"filename": file.Filename})
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{
|
||||
"source": "删除临时文件失败",
|
||||
"err_msg": err.Error(),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
result, err := locationService.ImportLocationsFromCSV(req, filePath, database.GetDB(c))
|
||||
if err != nil {
|
||||
utils.FailWithRequestLog(constant.LoggerChannelWork, "导入库位异常", err, c, gin.H{"filename": file.Filename})
|
||||
return
|
||||
}
|
||||
|
||||
systemRes.OkWithDetailed(result, result.Message, c)
|
||||
}
|
||||
|
||||
// GetUserWarehouseMappings 获取用户的仓库映射列表
|
||||
func (r *WarehouseApi) GetUserWarehouseMappings(c *gin.Context) {
|
||||
data, err := warehouseService.GetUserWarehouseMappings()
|
||||
|
||||
@ -119,7 +119,7 @@ func createTenantDB(dbName string) (*gorm.DB, error) {
|
||||
sqlDB.SetConnMaxLifetime(10 * time.Minute) // 单个连接最大存活10分钟
|
||||
sqlDB.SetConnMaxIdleTime(2 * time.Minute) // 空闲连接2分钟后关闭
|
||||
|
||||
//migrateTenantTables(tenantDB)
|
||||
migrateTenantTables(tenantDB)
|
||||
|
||||
return tenantDB, nil
|
||||
}
|
||||
|
||||
@ -161,3 +161,15 @@ type StockCheckReturnRequest struct {
|
||||
SalesOrderItemID int64 `form:"sales_order_item_id" binding:"required"` // 销售订单明细ID
|
||||
Remark string `form:"remark"` // 备注
|
||||
}
|
||||
|
||||
// DirectShipRequest 无需发货请求(按明细行标记)
|
||||
type DirectShipRequest struct {
|
||||
ShippingOrderItemID int64 `form:"shipping_order_item_id" json:"shipping_order_item_id" binding:"required"` // 发货单明细ID
|
||||
}
|
||||
|
||||
// ManualShipRequest 手动填写发货请求(按明细行标记)
|
||||
type ManualShipRequest struct {
|
||||
ShippingOrderItemID int64 `form:"shipping_order_item_id" json:"shipping_order_item_id" binding:"required"` // 发货单明细ID
|
||||
LogisticsCompany string `form:"logistics_company" json:"logistics_company" binding:"required"` // 物流公司编码
|
||||
LogisticsNo string `form:"logistics_no" json:"logistics_no" binding:"required"` // 物流单号
|
||||
}
|
||||
|
||||
@ -36,3 +36,16 @@ type GetShippingOrderDetailListRequest struct {
|
||||
WarehouseID int64 `form:"warehouse_id" json:"warehouse_id"`
|
||||
LocationID int64 `form:"location_id" json:"location_id"`
|
||||
}
|
||||
|
||||
// GetShippingOrderFlatListRequest 获取发货单平铺列表请求
|
||||
type GetShippingOrderFlatListRequest struct {
|
||||
Page int `form:"page" json:"page" binding:"omitempty,min=1"`
|
||||
PageSize int `form:"page_size" json:"page_size" binding:"omitempty,min=1,max=100"`
|
||||
Status int8 `form:"status" json:"status"`
|
||||
ShippingNo string `form:"shipping_no" json:"shipping_no"`
|
||||
ShopType int `form:"shop_type" json:"shop_type"`
|
||||
AssociationOrderNo string `form:"association_order_no" json:"association_order_no"`
|
||||
LogisticsNo string `form:"logistics_no" json:"logistics_no"`
|
||||
WarehouseID int64 `form:"warehouse_id" json:"warehouse_id"`
|
||||
LocationID int64 `form:"location_id" json:"location_id"`
|
||||
}
|
||||
|
||||
@ -215,6 +215,38 @@ func ConvertShippingOrderToDetailListItem(order models.ShippingOrder, customerNa
|
||||
}
|
||||
}
|
||||
|
||||
// ShippingOrderFlatItem 发货单平铺记录(每条记录对应一个商品明细)
|
||||
type ShippingOrderFlatItem struct {
|
||||
ItemID int64 `json:"item_id"` // shipping_order_item.id,用于操作按钮
|
||||
ShippingOrderID int64 `json:"shipping_order_id"` // 发货单ID
|
||||
ShippingNo string `json:"shipping_no"` // 发货单号
|
||||
ShopType int8 `json:"shop_type"` // 平台类型
|
||||
ShopTypeText string `json:"shop_type_text"` // 平台类型文本
|
||||
LogisticsCompany string `json:"logistics_company"` // 快递公司(来自 sales_order_item)
|
||||
Status int8 `json:"status"` // 发货单状态
|
||||
StatusText string `json:"status_text"` // 发货单状态文本
|
||||
ProductName string `json:"product_name"` // 书名
|
||||
ProductCode string `json:"product_code"` // ISBN
|
||||
ReceiverAddress string `json:"receiver_address"` // 收件人地址
|
||||
SalesOrderNo string `json:"sales_order_no"` // 销售单号
|
||||
OutboundOrderNo string `json:"outbound_order_no"` // 出库单号
|
||||
AssociationOrderNo string `json:"association_order_no"` // 第三方订单编号
|
||||
LogisticsNo string `json:"logistics_no"` // 快递单号
|
||||
SalesOrderCreatedAt int64 `json:"sales_order_created_at"` // 销售时间
|
||||
UnitPrice int64 `json:"unit_price"` // 售价(分)
|
||||
Quantity int64 `json:"quantity"` // 出库数量
|
||||
SalesOrderItemID int64 `json:"sales_order_item_id"` // sales_order_item.id,打印流程需要
|
||||
ProductID int64 `json:"product_id"` // 商品ID,左侧详情展示需要
|
||||
}
|
||||
|
||||
// ShippingOrderFlatListResponse 发货单平铺列表响应
|
||||
type ShippingOrderFlatListResponse struct {
|
||||
List []ShippingOrderFlatItem `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// GetShippingOrderStatusText 获取发货单状态文本
|
||||
func GetShippingOrderStatusText(status int8) string {
|
||||
statusMap := map[int8]string{
|
||||
|
||||
@ -219,6 +219,8 @@ func initRouter() (r *gin.Engine) {
|
||||
auth.GET("/outbound/detail/:id", processApi.GetOutboundDetail) // 获取出库单详情
|
||||
auth.POST("/shipping-order/create", processApi.CreateShippingOrder) // 创建发货单
|
||||
auth.POST("/shipping-order/update", processApi.UpdateShippingLogistics) // 更新发货单物流信息
|
||||
auth.POST("/shipping-order/direct-ship", processApi.DirectShip) // 无需发货
|
||||
auth.POST("/shipping-order/manual-ship", processApi.ManualShip) // 手动填写发货
|
||||
auth.POST("/sales-order/cancel", processApi.CancelSalesOrder) // 取消销售订
|
||||
//auth.POST("/sales-order/unlock-inventory", processApi.UnlockSalesOrderInventory) // 解锁销售订单库存
|
||||
|
||||
@ -245,6 +247,7 @@ func initRouter() (r *gin.Engine) {
|
||||
auth.GET("/shipping-order/list", shippingApi.GetShippingOrderList) // 获取发货单列表
|
||||
auth.GET("/shipping-order/detail", shippingApi.GetShippingOrderDetail) // 获取发货单详情
|
||||
auth.GET("/shipping-order/detaillist", shippingApi.GetShippingOrderDetailList) // 获取发货单详情列表(按状态)
|
||||
auth.GET("/shipping-order/flat-list", shippingApi.GetShippingOrderFlatList) // 获取发货单平铺列表
|
||||
// 波次任务管理
|
||||
auth.GET("/wave/task/list", waveApi.GetWaveTaskList) // 获取波次任务列表
|
||||
auth.GET("/wave/task/detail", waveApi.GetWaveTaskDetail) // 获取波次任务详情
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
systemRes "psi/models/response"
|
||||
"strings"
|
||||
|
||||
@ -35,11 +36,10 @@ type Config struct {
|
||||
// GenerateBarcode 根据货号生成条形码图片(Base64)
|
||||
func (s *BarcodeService) GenerateBarcode(content string) (systemRes.BarcodeResponse, error) {
|
||||
// 配置参数
|
||||
config := &Config{
|
||||
cfg := &Config{
|
||||
BarcodeHeight: 120,
|
||||
ModuleWidth: 2,
|
||||
Padding: 20,
|
||||
FontPath: "fonts/youaimoshouheiti-regular.ttf",
|
||||
FontSize: 50,
|
||||
BgColor: color.White,
|
||||
BarColor: color.Black,
|
||||
@ -47,11 +47,11 @@ func (s *BarcodeService) GenerateBarcode(content string) (systemRes.BarcodeRespo
|
||||
|
||||
// 若内容包含isbn,则使用较小字号且不加粗
|
||||
if strings.Contains(strings.ToLower(content), "9787") {
|
||||
config.FontSize = 35
|
||||
cfg.FontSize = 35
|
||||
}
|
||||
|
||||
// 生成条形码图片的Base64
|
||||
base64Image, err := generateBase64Barcode(content, config)
|
||||
base64Image, err := generateBase64Barcode(content, cfg)
|
||||
if err != nil {
|
||||
return systemRes.BarcodeResponse{}, fmt.Errorf("生成条形码失败: %w", err)
|
||||
}
|
||||
@ -62,9 +62,9 @@ func (s *BarcodeService) GenerateBarcode(content string) (systemRes.BarcodeRespo
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateBase64Barcode(content string, config *Config) (string, error) {
|
||||
func generateBase64Barcode(content string, cfg *Config) (string, error) {
|
||||
// 1. 生成条形码图像
|
||||
barcodeImg, err := generateBarcodeImage(content, config)
|
||||
barcodeImg, err := generateBarcodeImage(content, cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -73,42 +73,46 @@ func generateBase64Barcode(content string, config *Config) (string, error) {
|
||||
barcodeWidth := barcodeBounds.Dx()
|
||||
barcodeHeight := barcodeBounds.Dy()
|
||||
|
||||
// 2. 加载字体
|
||||
face, err := loadFont(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 2. 尝试加载字体(自定义字体 -> 系统字体 -> 跳过文字)
|
||||
face, fontErr := loadFont(cfg.FontSize)
|
||||
var textHeight, ascent, descent int
|
||||
var textWidth int
|
||||
if fontErr == nil && face != nil {
|
||||
defer face.Close()
|
||||
|
||||
// 3. 获取文字度量信息
|
||||
metrics := face.Metrics()
|
||||
ascent := metrics.Ascent.Ceil()
|
||||
descent := metrics.Descent.Ceil()
|
||||
textHeight := ascent + descent
|
||||
ascent = metrics.Ascent.Ceil()
|
||||
descent = metrics.Descent.Ceil()
|
||||
textHeight = ascent + descent
|
||||
|
||||
// 计算文字宽度
|
||||
drawer := &font.Drawer{Face: face}
|
||||
advance := drawer.MeasureString(content)
|
||||
textWidth := advance.Ceil()
|
||||
textWidth = advance.Ceil()
|
||||
}
|
||||
|
||||
// 4. 计算最终图片尺寸
|
||||
gapBetween := 12
|
||||
textBottomPadding := 12
|
||||
|
||||
finalWidth := barcodeWidth + cfg.Padding*2
|
||||
finalHeight := barcodeHeight + cfg.Padding*2
|
||||
if face != nil {
|
||||
maxContentWidth := barcodeWidth
|
||||
if textWidth > maxContentWidth {
|
||||
maxContentWidth = textWidth
|
||||
}
|
||||
finalWidth := maxContentWidth + config.Padding*2
|
||||
finalHeight := barcodeHeight + gapBetween + textHeight + config.Padding*2 + textBottomPadding
|
||||
finalWidth = maxContentWidth + cfg.Padding*2
|
||||
finalHeight = barcodeHeight + gapBetween + textHeight + cfg.Padding*2 + textBottomPadding
|
||||
}
|
||||
|
||||
// 5. 创建最终图片
|
||||
finalImg := image.NewRGBA(image.Rect(0, 0, finalWidth, finalHeight))
|
||||
draw.Draw(finalImg, finalImg.Bounds(), &image.Uniform{config.BgColor}, image.Point{}, draw.Src)
|
||||
draw.Draw(finalImg, finalImg.Bounds(), &image.Uniform{cfg.BgColor}, image.Point{}, draw.Src)
|
||||
|
||||
// 6. 绘制条形码(居中)
|
||||
barcodeStartX := (finalWidth - barcodeWidth) / 2
|
||||
barcodeStartY := config.Padding
|
||||
barcodeStartY := cfg.Padding
|
||||
draw.Draw(finalImg,
|
||||
image.Rect(barcodeStartX, barcodeStartY, barcodeStartX+barcodeWidth, barcodeStartY+barcodeHeight),
|
||||
barcodeImg,
|
||||
@ -116,18 +120,19 @@ func generateBase64Barcode(content string, config *Config) (string, error) {
|
||||
draw.Over)
|
||||
|
||||
// 7. 绘制货号文字(居中,加粗字体)
|
||||
if face != nil {
|
||||
textAreaTop := barcodeStartY + barcodeHeight + gapBetween
|
||||
textAreaHeight := textHeight + textBottomPadding
|
||||
baselineY := textAreaTop + (textAreaHeight / 2) + (ascent / 2) - 2
|
||||
|
||||
textX := (finalWidth - textWidth) / 2
|
||||
if textX < 0 {
|
||||
textX = config.Padding
|
||||
textX = cfg.Padding
|
||||
}
|
||||
|
||||
textDrawer := &font.Drawer{
|
||||
Dst: finalImg,
|
||||
Src: image.NewUniform(config.BarColor),
|
||||
Src: image.NewUniform(cfg.BarColor),
|
||||
Face: face,
|
||||
Dot: fixed.Point26_6{
|
||||
X: fixed.I(textX),
|
||||
@ -135,6 +140,7 @@ func generateBase64Barcode(content string, config *Config) (string, error) {
|
||||
},
|
||||
}
|
||||
textDrawer.DrawString(content)
|
||||
}
|
||||
|
||||
// 8. 转换为Base64
|
||||
buf := new(bytes.Buffer)
|
||||
@ -161,25 +167,45 @@ func generateBarcodeImage(content string, config *Config) (image.Image, error) {
|
||||
return barcode.Scale(code128Barcode, targetWidth, targetHeight)
|
||||
}
|
||||
|
||||
// loadFont 加载加粗TrueType字体
|
||||
func loadFont(config *Config) (font.Face, error) {
|
||||
fontBytes, err := os.ReadFile(config.FontPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取字体文件失败: %w", err)
|
||||
// loadFont 尝试加载字体,按优先级:项目字体目录 -> Windows系统字体 -> Linux系统字体
|
||||
// 如果都失败,返回 nil(调用方会跳过文字绘制)
|
||||
func loadFont(fontSize float64) (font.Face, error) {
|
||||
// 按优先级尝试的字体路径列表
|
||||
fontPaths := []string{
|
||||
"fonts/youaimoshouheiti-regular.ttf", // 项目自定义字体
|
||||
"C:\\Windows\\Fonts\\arial.ttf", // Windows Arial
|
||||
"C:\\Windows\\Fonts\\simhei.ttf", // Windows 黑体(支持中文)
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", // Linux DejaVu
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", // Linux Liberation
|
||||
}
|
||||
|
||||
// 如果是相对路径,尝试基于可执行文件目录解析
|
||||
exePath, exeErr := os.Executable()
|
||||
for i, p := range fontPaths {
|
||||
if !filepath.IsAbs(p) && exeErr == nil {
|
||||
fontPaths[i] = filepath.Join(filepath.Dir(exePath), p)
|
||||
}
|
||||
}
|
||||
|
||||
for _, fontPath := range fontPaths {
|
||||
fontBytes, err := os.ReadFile(fontPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
parsedFont, err := opentype.Parse(fontBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析字体失败: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
face, err := opentype.NewFace(parsedFont, &opentype.FaceOptions{
|
||||
Size: config.FontSize,
|
||||
Size: fontSize,
|
||||
DPI: 96,
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建字体失败: %w", err)
|
||||
continue
|
||||
}
|
||||
return face, nil
|
||||
}
|
||||
|
||||
return nil, nil // 所有字体都加载失败,返回 nil(不绘制文字)
|
||||
}
|
||||
|
||||
@ -3,17 +3,22 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"psi/constant"
|
||||
"psi/database"
|
||||
"psi/models"
|
||||
systemReq "psi/models/request"
|
||||
systemRes "psi/models/response"
|
||||
"psi/utils"
|
||||
)
|
||||
|
||||
type LocationService struct{}
|
||||
@ -1125,3 +1130,143 @@ func numToChar(n int) byte {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ExportLocationToCSV 导出库位到Excel文件
|
||||
func (s *LocationService) ExportLocationToCSV(req systemReq.ExportLocationRequest, db ...*gorm.DB) ([]byte, string, int64, error) {
|
||||
databaseConn := database.OptionalDB(db...)
|
||||
|
||||
var total int64
|
||||
query := databaseConn.Model(&models.Location{}).Where("is_del = ?", 0).Where("warehouse_id = ?", req.WarehouseID)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, "", 0, fmt.Errorf("查询库位总数失败: %v", err)
|
||||
}
|
||||
|
||||
if req.Type == 0 {
|
||||
return nil, "", total, nil
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
return nil, "", 0, fmt.Errorf("没有符合条件的库位数据")
|
||||
}
|
||||
|
||||
var locations []models.Location
|
||||
if err := query.Order("sort ASC, code ASC").Find(&locations).Error; err != nil {
|
||||
return nil, "", 0, fmt.Errorf("查询库位数据失败: %v", err)
|
||||
}
|
||||
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
utils.ErrorLog(constant.LoggerChannelWork, logrus.Fields{
|
||||
"source": "关闭Excel文件",
|
||||
"error": fmt.Sprintf("关闭失败: %v", err),
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
sheetName := "Sheet1"
|
||||
f.SetSheetName("Sheet1", sheetName)
|
||||
|
||||
headers := []string{"库位编码", "库位类型", "容量", "排序值", "状态", "创建时间", "更新时间"}
|
||||
for i, header := range headers {
|
||||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||||
f.SetCellValue(sheetName, cell, header)
|
||||
}
|
||||
|
||||
headerStyle, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{
|
||||
Bold: true,
|
||||
Size: 12,
|
||||
},
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "center",
|
||||
Vertical: "center",
|
||||
},
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Color: []string{"#E0E0E0"},
|
||||
Pattern: 1,
|
||||
},
|
||||
})
|
||||
f.SetCellStyle(sheetName, "A1", "G1", headerStyle)
|
||||
|
||||
typeMap := map[int8]string{
|
||||
1: "存储库位",
|
||||
2: "拣货库位",
|
||||
3: "收货库位",
|
||||
4: "发货库位",
|
||||
5: "退货库位",
|
||||
}
|
||||
statusMap := map[int8]string{
|
||||
0: "禁用",
|
||||
1: "启用",
|
||||
}
|
||||
|
||||
for idx, loc := range locations {
|
||||
row := idx + 2
|
||||
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), loc.Code)
|
||||
|
||||
typeName := typeMap[loc.Type]
|
||||
if typeName == "" {
|
||||
typeName = fmt.Sprintf("类型%d", loc.Type)
|
||||
}
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), typeName)
|
||||
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), loc.Capacity)
|
||||
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), loc.Sort)
|
||||
|
||||
statusName := statusMap[loc.Status]
|
||||
if statusName == "" {
|
||||
statusName = fmt.Sprintf("未知(%d)", loc.Status)
|
||||
}
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), statusName)
|
||||
|
||||
createdAtStr := time.Unix(loc.CreatedAt, 0).Format("2006-01-02 15:04:05")
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), createdAtStr)
|
||||
|
||||
updatedAtStr := time.Unix(loc.UpdatedAt, 0).Format("2006-01-02 15:04:05")
|
||||
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), updatedAtStr)
|
||||
}
|
||||
|
||||
colWidths := map[string]float64{
|
||||
"A": 20,
|
||||
"B": 14,
|
||||
"C": 10,
|
||||
"D": 10,
|
||||
"E": 10,
|
||||
"F": 20,
|
||||
"G": 20,
|
||||
}
|
||||
for col, width := range colWidths {
|
||||
f.SetColWidth(sheetName, col, col, width)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
fileName := fmt.Sprintf("location_export_%s.xlsx", now.Format("20060102150405"))
|
||||
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, "", 0, fmt.Errorf("生成Excel文件失败: %v", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), fileName, total, nil
|
||||
}
|
||||
|
||||
// ImportLocationFromCSV 从CSV/Excel文件导入库位
|
||||
func (s *LocationService) ImportLocationFromCSV(userID, warehouseID int64, fileBytes []byte) (*LocationImportResult, error) {
|
||||
importSvc := &LocationImportService{}
|
||||
rows, err := importSvc.ParseLocationImportExcel(fileBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析文件失败: %v", err)
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return &LocationImportResult{
|
||||
Message: "文件中没有有效的库位编码",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return importSvc.ImportLocationsFromExcel(userID, warehouseID, rows)
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ func getCell(row []string, idx int) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ImportLocationsFromExcel 从Excel导入库位(事务内批量创建)
|
||||
// ImportLocationsFromExcel 从Excel导入库位(覆盖模式:事务内先删后插,保证原子性)
|
||||
func (s *LocationImportService) ImportLocationsFromExcel(userID, warehouseID int64, rows []request.LocationImportRow) (*LocationImportResult, error) {
|
||||
databaseConn, err := database.GetTenantDB(userID)
|
||||
if err != nil {
|
||||
@ -89,6 +89,13 @@ func (s *LocationImportService) ImportLocationsFromExcel(userID, warehouseID int
|
||||
err = databaseConn.Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now().Unix()
|
||||
|
||||
// 覆盖模式:物理删除当前仓库的所有库位,释放唯一索引 uk_warehouse_code
|
||||
if err := tx.Where("warehouse_id = ?", warehouseID).Delete(&models.Location{}).Error; err != nil {
|
||||
return fmt.Errorf("清空旧库位失败: %v", err)
|
||||
}
|
||||
|
||||
rowIndex := 0
|
||||
|
||||
for _, row := range rows {
|
||||
code := strings.TrimSpace(row.Code)
|
||||
if code == "" {
|
||||
@ -96,19 +103,13 @@ func (s *LocationImportService) ImportLocationsFromExcel(userID, warehouseID int
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查同仓库下是否已存在
|
||||
var existing models.Location
|
||||
if err := tx.Where("warehouse_id = ? AND code = ? AND is_del = ?", warehouseID, code, 0).First(&existing).Error; err == nil {
|
||||
result.AddFail(fmt.Sprintf("%s: 库位已存在", code))
|
||||
continue
|
||||
}
|
||||
|
||||
rowIndex++
|
||||
loc := models.Location{
|
||||
WarehouseID: warehouseID,
|
||||
Code: code,
|
||||
Type: 1, // 存储库位
|
||||
Capacity: 255,
|
||||
Sort: 0,
|
||||
Sort: rowIndex,
|
||||
Status: 1, // 启用
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
||||
@ -4708,3 +4708,164 @@ func parseImageList(liveImage datatypes.JSON) []string {
|
||||
// 都不成功,返回空数组
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// DirectShip 无需发货:更新指定明细行的物流信息,并同步更新订单状态
|
||||
func (s *ProcessService) DirectShip(req systemReq.DirectShipRequest, operatorID int64, db ...*gorm.DB) error {
|
||||
databaseConn := database.OptionalDB(db...)
|
||||
now := time.Now().Unix()
|
||||
|
||||
return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error {
|
||||
// 查找发货单明细
|
||||
var shippingItem models.ShippingOrderItem
|
||||
if err := tx.Where("id = ? AND is_del = 0", req.ShippingOrderItemID).First(&shippingItem).Error; err != nil {
|
||||
return utils.NewError("发货单明细不存在")
|
||||
}
|
||||
|
||||
// 校验发货单存在
|
||||
var shippingOrder models.ShippingOrder
|
||||
if err := tx.Where("id = ? AND is_del = 0", shippingItem.ShippingOrderID).First(&shippingOrder).Error; err != nil {
|
||||
return utils.NewError("发货单不存在")
|
||||
}
|
||||
|
||||
if shippingItem.OutboundOrderItemID == nil {
|
||||
return utils.NewError("发货单明细无关联出库单")
|
||||
}
|
||||
|
||||
var outboundItem models.OutboundOrderItem
|
||||
if err := tx.Where("id = ? AND is_del = 0", *shippingItem.OutboundOrderItemID).First(&outboundItem).Error; err != nil {
|
||||
return utils.NewError("出库单明细不存在")
|
||||
}
|
||||
|
||||
// 更新 sales_order_item:物流公司=无需发货,已出库
|
||||
if outboundItem.SalesOrderID > 0 {
|
||||
if err := tx.Model(&models.SalesOrderItem{}).
|
||||
Where("sales_order_id = ? AND product_id = ? AND is_del = 0", outboundItem.SalesOrderID, outboundItem.ProductID).
|
||||
Updates(map[string]interface{}{
|
||||
"shipped_quantity": 1,
|
||||
"logistics_company": "无需发货",
|
||||
"logistics_no": "",
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return utils.NewError("更新销售订单明细失败")
|
||||
}
|
||||
|
||||
// 更新销售订单状态为已发货
|
||||
if err := tx.Model(&models.SalesOrder{}).
|
||||
Where("id = ? AND is_del = ?", outboundItem.SalesOrderID, 0).
|
||||
Update("status", constant.SalesStatusShipped).Error; err != nil {
|
||||
return utils.NewError("更新销售订单状态失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查该发货单所有明细是否全部发货,若是则更新出库单和发货单状态
|
||||
var allShippingItems []models.ShippingOrderItem
|
||||
if err := tx.Where("shipping_order_id = ? AND is_del = 0", shippingItem.ShippingOrderID).Find(&allShippingItems).Error; err != nil {
|
||||
return utils.NewError("查询发货单明细失败")
|
||||
}
|
||||
|
||||
// 收集关联的出库单明细ID
|
||||
outboundOrderItemIDs := make([]int64, 0, len(allShippingItems))
|
||||
for _, item := range allShippingItems {
|
||||
if item.OutboundOrderItemID != nil && *item.OutboundOrderItemID > 0 {
|
||||
outboundOrderItemIDs = append(outboundOrderItemIDs, *item.OutboundOrderItemID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(outboundOrderItemIDs) > 0 {
|
||||
// 查询所有关联的销售订单明细,判断是否全部已发货
|
||||
var relatedSalesOrderItems []models.SalesOrderItem
|
||||
if err := tx.Where("sales_order_id IN (?) AND is_del = 0",
|
||||
tx.Table("outbound_order_item").Select("sales_order_id").Where("id IN ? AND is_del = 0", outboundOrderItemIDs).QueryExpr()).
|
||||
Find(&relatedSalesOrderItems).Error; err != nil {
|
||||
return utils.NewError("查询销售订单明细失败")
|
||||
}
|
||||
|
||||
allShipped := len(relatedSalesOrderItems) > 0
|
||||
for _, item := range relatedSalesOrderItems {
|
||||
if item.ShippedQuantity == 0 {
|
||||
allShipped = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allShipped {
|
||||
// 更新出库单状态为已发货
|
||||
var outboundOrderIDs []int64
|
||||
if err := tx.Model(&models.OutboundOrderItem{}).
|
||||
Where("id IN ? AND is_del = 0", outboundOrderItemIDs).
|
||||
Pluck("DISTINCT out_order_id", &outboundOrderIDs).Error; err != nil {
|
||||
return utils.NewError("查询出库单ID失败")
|
||||
}
|
||||
|
||||
if len(outboundOrderIDs) > 0 {
|
||||
if err := tx.Model(&models.OutboundOrder{}).
|
||||
Where("id IN ? AND is_del = 0", outboundOrderIDs).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constant.OutboundStatusShipped,
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return utils.NewError("更新出库单状态失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新发货单状态为已发货
|
||||
if err := tx.Model(&models.ShippingOrder{}).
|
||||
Where("id = ? AND is_del = ?", shippingItem.ShippingOrderID, 0).
|
||||
Updates(map[string]interface{}{
|
||||
"status": constant.ShippingStatusShipped,
|
||||
"operator": operatorID,
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return utils.NewError("更新发货单状态失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ManualShip 手动填写发货:仅更新指定明细行的物流信息
|
||||
func (s *ProcessService) ManualShip(req systemReq.ManualShipRequest, operatorID int64, db ...*gorm.DB) error {
|
||||
databaseConn := database.OptionalDB(db...)
|
||||
now := time.Now().Unix()
|
||||
|
||||
return executeInTransactionWithDB(databaseConn, func(tx *gorm.DB) error {
|
||||
// 查找发货单明细
|
||||
var shippingItem models.ShippingOrderItem
|
||||
if err := tx.Where("id = ? AND is_del = 0", req.ShippingOrderItemID).First(&shippingItem).Error; err != nil {
|
||||
return utils.NewError("发货单明细不存在")
|
||||
}
|
||||
|
||||
// 校验发货单存在
|
||||
var shippingOrder models.ShippingOrder
|
||||
if err := tx.Where("id = ? AND is_del = 0", shippingItem.ShippingOrderID).First(&shippingOrder).Error; err != nil {
|
||||
return utils.NewError("发货单不存在")
|
||||
}
|
||||
|
||||
if shippingItem.OutboundOrderItemID == nil {
|
||||
return utils.NewError("发货单明细无关联出库单")
|
||||
}
|
||||
|
||||
var outboundItem models.OutboundOrderItem
|
||||
if err := tx.Where("id = ? AND is_del = 0", *shippingItem.OutboundOrderItemID).First(&outboundItem).Error; err != nil {
|
||||
return utils.NewError("出库单明细不存在")
|
||||
}
|
||||
|
||||
// 更新 sales_order_item:物流公司、物流单号,已出库
|
||||
if outboundItem.SalesOrderID > 0 {
|
||||
if err := tx.Model(&models.SalesOrderItem{}).
|
||||
Where("sales_order_id = ? AND product_id = ? AND is_del = 0", outboundItem.SalesOrderID, outboundItem.ProductID).
|
||||
Updates(map[string]interface{}{
|
||||
"shipped_quantity": 1,
|
||||
"logistics_company": req.LogisticsCompany,
|
||||
"logistics_no": req.LogisticsNo,
|
||||
"updated_at": now,
|
||||
}).Error; err != nil {
|
||||
return utils.NewError("更新销售订单明细失败")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -538,3 +538,146 @@ func (s *ShippingService) GetShippingOrderDetailList(req systemReq.GetShippingOr
|
||||
PageSize: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetShippingOrderFlatList 获取发货单平铺列表(每条记录对应一个商品明细)
|
||||
func (s *ShippingService) GetShippingOrderFlatList(req systemReq.GetShippingOrderFlatListRequest, creatorID int64, role int64, db ...*gorm.DB) (*systemRes.ShippingOrderFlatListResponse, error) {
|
||||
databaseConn := database.OptionalDB(db...)
|
||||
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 || req.PageSize > 100 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
// 基础查询:以 shipping_order_item 为主表
|
||||
query := databaseConn.Table("shipping_order_item soi").
|
||||
Select(`soi.id as item_id,
|
||||
so.id as shipping_order_id,
|
||||
so.shipping_no,
|
||||
so.status,
|
||||
p.name as product_name,
|
||||
p.barcode as product_code,
|
||||
soi2.receiver_address,
|
||||
so2.so_no as sales_order_no,
|
||||
oo.out_no as outbound_order_no,
|
||||
so2.association_order_no,
|
||||
soi2.logistics_no,
|
||||
soi2.logistics_company,
|
||||
so2.created_at as sales_order_created_at,
|
||||
ooi.unit_price,
|
||||
ooi.quantity,
|
||||
so2.shop_type,
|
||||
soi2.id as sales_order_item_id,
|
||||
p.id as product_id`).
|
||||
Joins("INNER JOIN shipping_order so ON soi.shipping_order_id = so.id AND so.is_del = 0").
|
||||
Joins("INNER JOIN outbound_order_item ooi ON soi.outbound_order_item_id = ooi.id AND ooi.is_del = 0").
|
||||
Joins("INNER JOIN outbound_order oo ON ooi.out_order_id = oo.id AND oo.is_del = 0").
|
||||
Joins("INNER JOIN product p ON ooi.product_id = p.id AND p.is_del = 0").
|
||||
Joins("LEFT JOIN sales_order so2 ON ooi.sales_order_id = so2.id AND so2.is_del = 0").
|
||||
Joins("LEFT JOIN sales_order_item soi2 ON ooi.sales_order_id = soi2.sales_order_id AND ooi.product_id = soi2.product_id AND soi2.is_del = 0").
|
||||
Where("soi.is_del = 0")
|
||||
|
||||
// 筛选条件
|
||||
if req.Status > 0 {
|
||||
query = query.Where("so.status = ?", req.Status)
|
||||
}
|
||||
if req.ShippingNo != "" {
|
||||
query = query.Where("so.shipping_no LIKE ?", "%"+req.ShippingNo+"%")
|
||||
}
|
||||
if req.ShopType > 0 {
|
||||
query = query.Where("so2.shop_type = ?", req.ShopType)
|
||||
}
|
||||
if req.AssociationOrderNo != "" {
|
||||
query = query.Where("so2.association_order_no LIKE ?", "%"+req.AssociationOrderNo+"%")
|
||||
}
|
||||
if req.LogisticsNo != "" {
|
||||
query = query.Where("soi2.logistics_no LIKE ?", "%"+req.LogisticsNo+"%")
|
||||
}
|
||||
if req.WarehouseID > 0 {
|
||||
query = query.Joins("INNER JOIN location l ON ooi.location_id = l.id AND l.is_del = 0").
|
||||
Where("l.warehouse_id = ?", req.WarehouseID)
|
||||
}
|
||||
if req.LocationID > 0 {
|
||||
query = query.Where("ooi.location_id = ?", req.LocationID)
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, utils.NewError("查询平铺列表总数失败")
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
return &systemRes.ShippingOrderFlatListResponse{
|
||||
List: []systemRes.ShippingOrderFlatItem{},
|
||||
Total: 0,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 查询分页数据
|
||||
type flatRow struct {
|
||||
ItemID int64 `gorm:"column:item_id"`
|
||||
ShippingOrderID int64 `gorm:"column:shipping_order_id"`
|
||||
ShippingNo string `gorm:"column:shipping_no"`
|
||||
Status int8 `gorm:"column:status"`
|
||||
ProductName string `gorm:"column:product_name"`
|
||||
ProductCode string `gorm:"column:product_code"`
|
||||
ReceiverAddress string `gorm:"column:receiver_address"`
|
||||
SalesOrderNo string `gorm:"column:sales_order_no"`
|
||||
OutboundOrderNo string `gorm:"column:outbound_order_no"`
|
||||
AssociationOrderNo string `gorm:"column:association_order_no"`
|
||||
LogisticsNo string `gorm:"column:logistics_no"`
|
||||
LogisticsCompany string `gorm:"column:logistics_company"`
|
||||
SalesOrderCreatedAt int64 `gorm:"column:sales_order_created_at"`
|
||||
UnitPrice int64 `gorm:"column:unit_price"`
|
||||
Quantity int64 `gorm:"column:quantity"`
|
||||
ShopType int8 `gorm:"column:shop_type"`
|
||||
SalesOrderItemID int64 `gorm:"column:sales_order_item_id"`
|
||||
ProductID int64 `gorm:"column:product_id"`
|
||||
}
|
||||
|
||||
var rows []flatRow
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("so.created_at DESC, soi.id ASC").
|
||||
Offset(offset).
|
||||
Limit(req.PageSize).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, utils.NewError("查询平铺列表失败")
|
||||
}
|
||||
|
||||
items := make([]systemRes.ShippingOrderFlatItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, systemRes.ShippingOrderFlatItem{
|
||||
ItemID: row.ItemID,
|
||||
ShippingOrderID: row.ShippingOrderID,
|
||||
ShippingNo: row.ShippingNo,
|
||||
ShopType: row.ShopType,
|
||||
ShopTypeText: systemRes.GetShopTypeText(row.ShopType),
|
||||
LogisticsCompany: row.LogisticsCompany,
|
||||
Status: row.Status,
|
||||
StatusText: systemRes.GetShippingOrderStatusText(row.Status),
|
||||
ProductName: row.ProductName,
|
||||
ProductCode: row.ProductCode,
|
||||
ReceiverAddress: row.ReceiverAddress,
|
||||
SalesOrderNo: row.SalesOrderNo,
|
||||
OutboundOrderNo: row.OutboundOrderNo,
|
||||
AssociationOrderNo: row.AssociationOrderNo,
|
||||
LogisticsNo: row.LogisticsNo,
|
||||
SalesOrderCreatedAt: row.SalesOrderCreatedAt,
|
||||
UnitPrice: row.UnitPrice,
|
||||
Quantity: row.Quantity,
|
||||
SalesOrderItemID: row.SalesOrderItemID,
|
||||
ProductID: row.ProductID,
|
||||
})
|
||||
}
|
||||
|
||||
return &systemRes.ShippingOrderFlatListResponse{
|
||||
List: items,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user