0e42f0e417
- 项目目录结构:backend/ deploy/ schema/ migrations/ - 数据库 Schema:所有建表 SQL,含 hotel_id 多租户隔离 - Go 后端:config、model、handler、service、middleware、router - 认证:账号密码登录 + JWT(Access + Refresh Token) - 许可证:HMAC-SHA256 激活码生成 + 设备绑定验证 - 业务模块:商品、仓库、往来单位、入库、出库、库存、盘点 - 库存事务:入库/出库审核时原子更新库存 + 流水记录 - 数据导入:Excel/CSV 批量导入商品、往来单位 - Docker Compose:本地 MySQL + Adminer Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
4.4 KiB
Go
149 lines
4.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"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"
|
|
)
|
|
|
|
type StockOutHandler struct {
|
|
db *gorm.DB
|
|
stockSvc *service.StockService
|
|
}
|
|
|
|
func NewStockOutHandler(db *gorm.DB, svc *service.StockService) *StockOutHandler {
|
|
return &StockOutHandler{db: db, stockSvc: svc}
|
|
}
|
|
|
|
// List GET /api/v1/stock-out/orders
|
|
func (h *StockOutHandler) List(c *gin.Context) {
|
|
hotelID := middleware.GetHotelID(c)
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
query := h.db.Model(&model.StockOutOrder{}).
|
|
Where("hotel_id = ? AND deleted_at IS NULL", hotelID)
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
query = query.Where("status = ?", 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)
|
|
}
|
|
|
|
var total int64
|
|
query.Count(&total)
|
|
|
|
var orders []model.StockOutOrder
|
|
query.Preload("Warehouse").Preload("Partner").Preload("Operator").
|
|
Offset((page - 1) * pageSize).Limit(pageSize).
|
|
Order("id DESC").Find(&orders)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"data": orders, "total": total, "page": page, "page_size": pageSize})
|
|
}
|
|
|
|
// Get GET /api/v1/stock-out/orders/:id
|
|
func (h *StockOutHandler) Get(c *gin.Context) {
|
|
hotelID := middleware.GetHotelID(c)
|
|
var order model.StockOutOrder
|
|
if err := h.db.Preload("Items.Product").Preload("Warehouse").Preload("Partner").
|
|
Where("id = ? AND hotel_id = ? AND deleted_at IS NULL", c.Param("id"), hotelID).
|
|
First(&order).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": order})
|
|
}
|
|
|
|
// Create POST /api/v1/stock-out/orders
|
|
func (h *StockOutHandler) Create(c *gin.Context) {
|
|
hotelID := middleware.GetHotelID(c)
|
|
operatorID := middleware.GetUserID(c)
|
|
|
|
var req model.StockOutOrder
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
req.HotelID = hotelID
|
|
req.OperatorID = operatorID
|
|
req.Status = "draft"
|
|
|
|
orderNo, err := h.stockSvc.GenerateOrderNo(hotelID, "stock_out")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
req.OrderNo = orderNo
|
|
|
|
var total float64
|
|
for i := range req.Items {
|
|
req.Items[i].HotelID = hotelID
|
|
req.Items[i].TotalPrice = req.Items[i].Quantity * req.Items[i].UnitPrice
|
|
total += req.Items[i].TotalPrice
|
|
}
|
|
req.TotalAmount = total
|
|
|
|
if err := h.db.Create(&req).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, gin.H{"data": req})
|
|
}
|
|
|
|
// Submit PUT /api/v1/stock-out/orders/:id/submit
|
|
func (h *StockOutHandler) Submit(c *gin.Context) {
|
|
hotelID := middleware.GetHotelID(c)
|
|
result := h.db.Model(&model.StockOutOrder{}).
|
|
Where("id = ? AND hotel_id = ? AND status = 'draft'", c.Param("id"), hotelID).
|
|
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-out/orders/:id/approve
|
|
func (h *StockOutHandler) Approve(c *gin.Context) {
|
|
hotelID := middleware.GetHotelID(c)
|
|
reviewerID := middleware.GetUserID(c)
|
|
|
|
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
|
if err := h.stockSvc.ApproveStockOut(hotelID, 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-out/orders/:id/reject
|
|
func (h *StockOutHandler) Reject(c *gin.Context) {
|
|
hotelID := middleware.GetHotelID(c)
|
|
reviewerID := middleware.GetUserID(c)
|
|
now := timeNow()
|
|
|
|
result := h.db.Model(&model.StockOutOrder{}).
|
|
Where("id = ? AND hotel_id = ? AND status = 'pending'", c.Param("id"), hotelID).
|
|
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"})
|
|
}
|