7a78448a25
Deploy Server / release-deploy-server (push) Successful in 56s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
273 lines
8.5 KiB
Go
273 lines
8.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/wangjia/jiu/backend/internal/middleware"
|
|
"github.com/wangjia/jiu/backend/internal/model"
|
|
"github.com/wangjia/jiu/backend/internal/service"
|
|
"github.com/wangjia/jiu/backend/internal/util"
|
|
)
|
|
|
|
func timeNow() *time.Time {
|
|
t := time.Now()
|
|
return &t
|
|
}
|
|
|
|
type StockInHandler struct {
|
|
db *gorm.DB
|
|
stockSvc *service.StockService
|
|
}
|
|
|
|
func NewStockInHandler(db *gorm.DB, svc *service.StockService) *StockInHandler {
|
|
return &StockInHandler{db: db, stockSvc: svc}
|
|
}
|
|
|
|
// List GET /api/v1/stock-in/orders
|
|
func (h *StockInHandler) List(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
pageSize = util.ValidatePageSize(pageSize, 20, 200)
|
|
|
|
query := h.db.Model(&model.StockInOrder{}).
|
|
Where("shop_id = ? AND deleted_at IS NULL", shopID)
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
query = query.Where("status IN ?", strings.Split(status, ","))
|
|
}
|
|
if startDate := c.Query("start_date"); startDate != "" {
|
|
query = query.Where("order_date >= ?", startDate)
|
|
}
|
|
if endDate := c.Query("end_date"); endDate != "" {
|
|
query = query.Where("order_date <= ?", endDate)
|
|
}
|
|
if kw := strings.TrimSpace(c.Query("keyword")); kw != "" {
|
|
like := "%" + kw + "%"
|
|
query = query.Where(
|
|
"order_no LIKE ? OR partner_id IN (SELECT id FROM partners WHERE shop_id = ? AND name LIKE ?)",
|
|
like, shopID, like,
|
|
)
|
|
}
|
|
|
|
var total int64
|
|
query.Count(&total)
|
|
|
|
orders := make([]model.StockInOrder, 0)
|
|
query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
|
|
Offset((page - 1) * pageSize).Limit(pageSize).
|
|
Order("order_date DESC, id DESC").Find(&orders)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": orders, "total": total, "page": page, "page_size": pageSize})
|
|
}
|
|
|
|
// Get GET /api/v1/stock-in/orders/:id
|
|
func (h *StockInHandler) Get(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
var order model.StockInOrder
|
|
if err := h.db.Preload("Items.Product").Preload("Items.Product.Origin").
|
|
Preload("Items.Product.ShelfLife").Preload("Items.Product.Storage").
|
|
Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
|
|
Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
|
|
First(&order).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
util.RespondSuccess(c, order)
|
|
}
|
|
|
|
// Create POST /api/v1/stock-in/orders
|
|
func (h *StockInHandler) Create(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
operatorID := middleware.GetUserID(c)
|
|
|
|
var req model.StockInOrder
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
req.ShopID = shopID
|
|
req.OperatorID = operatorID
|
|
req.Status = "draft"
|
|
|
|
// 生成单号
|
|
orderNo, err := h.stockSvc.GenerateOrderNo(shopID, "stock_in")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
req.OrderNo = orderNo
|
|
|
|
// 事务内:每条明细新建一个独立产品(特有产品/序列号,不按名称复用),回填 product_id,再建单。
|
|
err = h.db.Transaction(func(tx *gorm.DB) error {
|
|
var total float64
|
|
for i := range req.Items {
|
|
it := &req.Items[i]
|
|
it.ShopID = shopID
|
|
if it.BatchNo == "" {
|
|
it.BatchNo = fmt.Sprintf("%s-%02d", req.OrderNo, i+1)
|
|
}
|
|
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
it.ProductID = prod.ID
|
|
it.ProductCode = prod.Code // 保留快照,兼容现有查询(瘦身阶段再去)
|
|
it.TotalPrice = it.Quantity * it.UnitPrice
|
|
total += it.TotalPrice
|
|
}
|
|
req.TotalAmount = total
|
|
return tx.Create(&req).Error
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
util.RespondCreated(c, req)
|
|
}
|
|
|
|
// Update PUT /api/v1/stock-in/orders/:id (只允许草稿状态)
|
|
func (h *StockInHandler) Update(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
|
|
var order model.StockInOrder
|
|
if err := h.db.Where("id = ? AND shop_id = ? AND status = 'draft' AND deleted_at IS NULL", c.Param("id"), shopID).
|
|
First(&order).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "入库单不存在或不可修改"})
|
|
return
|
|
}
|
|
|
|
var req model.StockInOrder
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
err := h.db.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Where("order_id = ?", order.ID).Delete(&model.StockInItem{}).Error; err != nil {
|
|
return err
|
|
}
|
|
var total float64
|
|
for i := range req.Items {
|
|
it := &req.Items[i]
|
|
it.ShopID = shopID
|
|
it.OrderID = order.ID
|
|
if it.BatchNo == "" {
|
|
it.BatchNo = fmt.Sprintf("%s-%02d", order.OrderNo, i+1)
|
|
}
|
|
// 编辑草稿:旧明细已删,每条按新模型重建独立产品(旧草稿 product 无库存,暂留待后续清理)
|
|
prod, e := createIndependentProduct(tx, shopID, it.ProductName, it.Series, it.Spec, it.BatchNo, it.ProductionDate, it.UnitPrice)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
it.ProductID = prod.ID
|
|
it.ProductCode = prod.Code
|
|
it.TotalPrice = it.Quantity * it.UnitPrice
|
|
}
|
|
updates := map[string]interface{}{
|
|
"warehouse_id": req.WarehouseID,
|
|
"partner_id": req.PartnerID,
|
|
"order_date": req.OrderDate,
|
|
"remark": req.Remark,
|
|
"total_amount": total,
|
|
}
|
|
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(req.Items) > 0 {
|
|
if err := tx.Create(&req.Items).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "updated"})
|
|
}
|
|
|
|
// Delete DELETE /api/v1/stock-in/orders/:id (只允许草稿状态)
|
|
func (h *StockInHandler) Delete(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
now := timeNow()
|
|
result := h.db.Model(&model.StockInOrder{}).
|
|
Where("id = ? AND shop_id = ? AND status = 'draft' AND deleted_at IS NULL", c.Param("id"), shopID).
|
|
Update("deleted_at", now)
|
|
if result.RowsAffected == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "入库单不存在或不可删除"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
|
}
|
|
|
|
// Submit PUT /api/v1/stock-in/orders/:id/submit (草稿→待审核)
|
|
func (h *StockInHandler) Submit(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
result := h.db.Model(&model.StockInOrder{}).
|
|
Where("id = ? AND shop_id = ? AND status = 'draft'", c.Param("id"), shopID).
|
|
Update("status", "pending")
|
|
if result.RowsAffected == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in draft status"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "submitted"})
|
|
}
|
|
|
|
// Approve PUT /api/v1/stock-in/orders/:id/approve
|
|
func (h *StockInHandler) Approve(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
reviewerID := middleware.GetUserID(c)
|
|
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err := h.stockSvc.ApproveStockIn(shopID, id, reviewerID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "approved"})
|
|
}
|
|
|
|
// Reject PUT /api/v1/stock-in/orders/:id/reject
|
|
func (h *StockInHandler) Reject(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
reviewerID := middleware.GetUserID(c)
|
|
now := timeNow()
|
|
|
|
result := h.db.Model(&model.StockInOrder{}).
|
|
Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID).
|
|
Updates(map[string]interface{}{
|
|
"status": "rejected",
|
|
"reviewer_id": reviewerID,
|
|
"reviewed_at": now,
|
|
})
|
|
if result.RowsAffected == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "rejected"})
|
|
}
|
|
|
|
// Withdraw PUT /api/v1/stock-in/orders/:id/withdraw
|
|
// 审核中(pending)撤回为草稿(draft),供管理员修改后重新提交。仅 pending 可撤回;
|
|
// 已审核(approved)单据只读、库存已变动,不可撤回。
|
|
func (h *StockInHandler) Withdraw(c *gin.Context) {
|
|
shopID := middleware.GetShopID(c)
|
|
result := h.db.Model(&model.StockInOrder{}).
|
|
Where("id = ? AND shop_id = ? AND status = 'pending'", c.Param("id"), shopID).
|
|
Update("status", "draft")
|
|
if result.RowsAffected == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "order not found or not in pending status"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "withdrawn"})
|
|
}
|