933 lines
25 KiB
Go
933 lines
25 KiB
Go
package routers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"ops/models"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// ---------- 数据表结构 ----------
|
||
|
||
type TabWarehouseContainer struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
Title string `gorm:"size:255;not null;comment:容器名"`
|
||
Remark string `gorm:"type:text;comment:描述"`
|
||
CreatedAt string `gorm:"size:20;comment:创建日期"`
|
||
CreatorID uint `gorm:"not null;index;comment:创建者id"`
|
||
ParentID *uint `gorm:"index;comment:父容器id,nil=顶级"`
|
||
ItemCount int `gorm:"default:0;comment:直接子物品数量"`
|
||
ChildCount int `gorm:"default:0;comment:子容器数量"`
|
||
}
|
||
|
||
type TabWarehouseItem struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
Name string `gorm:"size:255;not null;comment:物品名"`
|
||
SerialNumber string `gorm:"size:255;comment:序列号"`
|
||
Remark string `gorm:"type:text;comment:描述"`
|
||
Quantity int `gorm:"default:1;comment:数量"`
|
||
CreatedAt string `gorm:"size:20;comment:创建日期"`
|
||
CreatorID uint `gorm:"not null;index;comment:创建者id"`
|
||
ContainerID *uint `gorm:"index;comment:所属容器id,nil=未入库"`
|
||
}
|
||
|
||
type TabWarehouseContainerFileBind struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
ContainerID uint `gorm:"not null;index;comment:关联容器id"`
|
||
FileID uint `gorm:"not null;comment:关联文件id"`
|
||
CreatorID uint `gorm:"not null;comment:上传人id"`
|
||
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"`
|
||
}
|
||
|
||
type TabWarehouseItemFileBind struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
ItemID uint `gorm:"not null;index;comment:关联物品id"`
|
||
FileID uint `gorm:"not null;comment:关联文件id"`
|
||
CreatorID uint `gorm:"not null;comment:上传人id"`
|
||
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"`
|
||
}
|
||
|
||
type TabWarehouseItemCommit struct {
|
||
ID uint `gorm:"primaryKey" json:"ID"`
|
||
ItemID uint `gorm:"not null;index;comment:关联物品id" json:"ItemID"`
|
||
UserID uint `gorm:"not null;comment:操作人id" json:"UserID"`
|
||
OldContainer *uint `gorm:"index;comment:原容器id" json:"OldContainer"`
|
||
NewContainer *uint `gorm:"index;comment:新容器id" json:"NewContainer"`
|
||
Remark string `gorm:"type:text;comment:备注" json:"Remark"`
|
||
IP string `gorm:"size:50;comment:操作IP" json:"IP"`
|
||
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime" json:"CreatedAt"`
|
||
}
|
||
|
||
type TabWarehouseLog struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
EntityType string `gorm:"size:50;not null;index;comment:操作对象类型"`
|
||
EntityID uint `gorm:"not null;index;comment:操作对象id"`
|
||
UserID uint `gorm:"not null;index;comment:操作人id"`
|
||
ActionType string `gorm:"size:50;not null;comment:操作类型: create update delete move query"`
|
||
OldContent string `gorm:"type:text;comment:修改前内容(JSON)"`
|
||
NewContent string `gorm:"type:text;comment:修改后内容(JSON)"`
|
||
IP string `gorm:"size:50;comment:操作IP"`
|
||
Remark string `gorm:"size:500;comment:备注"`
|
||
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"`
|
||
}
|
||
|
||
type TabWarehouseItemWorkOrderBind struct {
|
||
ID uint `gorm:"primaryKey"`
|
||
ItemID uint `gorm:"not null;index;comment:关联物品id"`
|
||
WorkOrderID uint `gorm:"not null;index;comment:关联工单id"`
|
||
Remark string `gorm:"size:500;comment:备注"`
|
||
CreatorID uint `gorm:"not null;comment:绑定人id"`
|
||
CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"`
|
||
}
|
||
|
||
// ---------- 初始化 ----------
|
||
|
||
func ApiWarehouseInit() {
|
||
models.DB.AutoMigrate(
|
||
&TabWarehouseContainer{},
|
||
&TabWarehouseItem{},
|
||
&TabWarehouseContainerFileBind{},
|
||
&TabWarehouseItemFileBind{},
|
||
&TabWarehouseItemCommit{},
|
||
&TabWarehouseLog{},
|
||
&TabWarehouseItemWorkOrderBind{},
|
||
)
|
||
}
|
||
|
||
// ---------- 路由注册 ----------
|
||
|
||
func ApiWarehouse(r *gin.RouterGroup) {
|
||
|
||
// 新增容器
|
||
r.POST("/add_container", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromAdd struct {
|
||
Title string `json:"title"`
|
||
Remark string `json:"remark"`
|
||
ParentID *uint `json:"parent_id"`
|
||
Photos []string `json:"photos"`
|
||
}
|
||
var from FromAdd
|
||
if err := decodeJSON(data, &from); err != nil || from.Title == "" {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
// 校验图片哈希
|
||
for _, hash := range from.Photos {
|
||
if models.IsContainsSpecialChar(hash) {
|
||
ReturnJson(ctx, "photo_hash_invalid", nil)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查嵌套层级不超过5层
|
||
if from.ParentID != nil {
|
||
var parent TabWarehouseContainer
|
||
if err := models.DB.First(&parent, *from.ParentID).Error; err != nil {
|
||
ReturnJson(ctx, "parent_not_found", nil)
|
||
return
|
||
}
|
||
depth := 0
|
||
curID := *from.ParentID
|
||
for depth < 100 {
|
||
var c TabWarehouseContainer
|
||
if err := models.DB.First(&c, curID).Error; err != nil {
|
||
break
|
||
}
|
||
if c.ParentID == nil {
|
||
break
|
||
}
|
||
curID = *c.ParentID
|
||
depth++
|
||
}
|
||
if depth >= 4 { // 新容器将落在第5层
|
||
ReturnJson(ctx, "max_depth_exceeded", nil)
|
||
return
|
||
}
|
||
}
|
||
|
||
c := TabWarehouseContainer{
|
||
Title: from.Title,
|
||
Remark: from.Remark,
|
||
CreatedAt: strconv.FormatInt(time.Now().Unix(), 10),
|
||
CreatorID: user.ID,
|
||
ParentID: from.ParentID,
|
||
}
|
||
models.DB.Create(&c)
|
||
|
||
// 绑定图片
|
||
for _, hash := range from.Photos {
|
||
var findFile TabFileInfo_
|
||
if models.DB.Where(&TabFileInfo_{Sha256: hash, Type: "image"}).First(&findFile).Error == nil {
|
||
models.DB.Create(&TabWarehouseContainerFileBind{
|
||
ContainerID: c.ID,
|
||
FileID: findFile.ID,
|
||
CreatorID: user.ID,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 父容器的 ChildCount +1
|
||
if from.ParentID != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *from.ParentID).Update("child_count", models.DB.Raw("child_count + 1"))
|
||
}
|
||
|
||
// 写操作日志
|
||
newContent, _ := json.Marshal(from)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "container",
|
||
EntityID: c.ID,
|
||
UserID: user.ID,
|
||
ActionType: "create",
|
||
NewContent: string(newContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{"id": c.ID})
|
||
})
|
||
|
||
// 编辑容器
|
||
r.POST("/update_container", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromUpdate struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title"`
|
||
Remark string `json:"remark"`
|
||
Photos []string `json:"photos"`
|
||
}
|
||
var from FromUpdate
|
||
if err := decodeJSON(data, &from); err != nil || from.ID == 0 || from.Title == "" {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
// 校验图片哈希
|
||
for _, hash := range from.Photos {
|
||
if models.IsContainsSpecialChar(hash) {
|
||
ReturnJson(ctx, "photo_hash_invalid", nil)
|
||
return
|
||
}
|
||
}
|
||
|
||
var c TabWarehouseContainer
|
||
if err := models.DB.Where("id = ?", from.ID).First(&c).Error; err != nil {
|
||
ReturnJson(ctx, "container_not_found", nil)
|
||
return
|
||
}
|
||
|
||
oldContent, _ := json.Marshal(c)
|
||
models.DB.Model(&c).Updates(map[string]interface{}{
|
||
"title": from.Title,
|
||
"remark": from.Remark,
|
||
})
|
||
|
||
// 重建图片绑定
|
||
models.DB.Where("container_id = ?", from.ID).Delete(&TabWarehouseContainerFileBind{})
|
||
for _, hash := range from.Photos {
|
||
var findFile TabFileInfo_
|
||
if models.DB.Where(&TabFileInfo_{Sha256: hash, Type: "image"}).First(&findFile).Error == nil {
|
||
models.DB.Create(&TabWarehouseContainerFileBind{
|
||
ContainerID: from.ID,
|
||
FileID: findFile.ID,
|
||
CreatorID: user.ID,
|
||
})
|
||
}
|
||
}
|
||
|
||
newContent, _ := json.Marshal(from)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "container",
|
||
EntityID: from.ID,
|
||
UserID: user.ID,
|
||
ActionType: "update",
|
||
OldContent: string(oldContent),
|
||
NewContent: string(newContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
ReturnJson(ctx, "apiOK", nil)
|
||
})
|
||
|
||
// 删除容器
|
||
r.POST("/delete_container", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromDelete struct {
|
||
ID uint `json:"id"`
|
||
}
|
||
var from FromDelete
|
||
if err := decodeJSON(data, &from); err != nil || from.ID == 0 {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
var c TabWarehouseContainer
|
||
if err := models.DB.Where("id = ?", from.ID).First(&c).Error; err != nil {
|
||
ReturnJson(ctx, "container_not_found", nil)
|
||
return
|
||
}
|
||
|
||
// 检查是否有子容器或物品
|
||
if c.ChildCount > 0 || c.ItemCount > 0 {
|
||
ReturnJson(ctx, "container_not_empty", nil)
|
||
return
|
||
}
|
||
|
||
models.DB.Where("container_id = ?", from.ID).Delete(&TabWarehouseContainerFileBind{})
|
||
|
||
// 父容器 ChildCount -1
|
||
if c.ParentID != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *c.ParentID).Update("child_count", models.DB.Raw("child_count - 1"))
|
||
}
|
||
|
||
oldContent, _ := json.Marshal(c)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "container",
|
||
EntityID: from.ID,
|
||
UserID: user.ID,
|
||
ActionType: "delete",
|
||
OldContent: string(oldContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
models.DB.Delete(&c)
|
||
|
||
ReturnJson(ctx, "apiOK", nil)
|
||
})
|
||
|
||
// 获取容器列表
|
||
r.POST("/list_container", func(ctx *gin.Context) {
|
||
isAuth, _, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromList struct {
|
||
Search string `json:"search"`
|
||
ParentID *uint `json:"parent_id"`
|
||
Entries int `json:"entries"`
|
||
Page int `json:"page"`
|
||
}
|
||
var from FromList
|
||
if err := decodeJSON(data, &from); err != nil {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
if from.Entries <= 0 || from.Entries > 300 {
|
||
from.Entries = 10
|
||
}
|
||
if from.Page <= 0 {
|
||
from.Page = 1
|
||
}
|
||
|
||
var count int64
|
||
query := models.DB.Model(&TabWarehouseContainer{})
|
||
if from.Search != "" {
|
||
query = query.Where("title LIKE ?", "%"+from.Search+"%")
|
||
}
|
||
if from.ParentID != nil {
|
||
query = query.Where("parent_id = ?", *from.ParentID)
|
||
} else if from.Search == "" {
|
||
// 无搜索时默认只显示顶级容器
|
||
query = query.Where("parent_id IS NULL")
|
||
}
|
||
query.Count(&count)
|
||
|
||
var containers []TabWarehouseContainer
|
||
query.Order("created_at DESC").
|
||
Offset(from.Entries * (from.Page - 1)).
|
||
Limit(from.Entries).
|
||
Find(&containers)
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{
|
||
"all_count": count,
|
||
"containers": containers,
|
||
})
|
||
})
|
||
|
||
// 获取容器详情
|
||
r.POST("/get_container", func(ctx *gin.Context) {
|
||
isAuth, _, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromGet struct {
|
||
ID uint `json:"id"`
|
||
}
|
||
var from FromGet
|
||
if err := decodeJSON(data, &from); err != nil || from.ID == 0 {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
var c TabWarehouseContainer
|
||
if err := models.DB.Where("id = ?", from.ID).First(&c).Error; err != nil {
|
||
ReturnJson(ctx, "container_not_found", nil)
|
||
return
|
||
}
|
||
|
||
// 构建父容器链(从根到当前)
|
||
type ParentItem struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title"`
|
||
}
|
||
parentChain := []ParentItem{}
|
||
if c.ParentID != nil {
|
||
curID := *c.ParentID
|
||
visited := map[uint]bool{}
|
||
for curID != 0 {
|
||
if visited[curID] {
|
||
break
|
||
}
|
||
visited[curID] = true
|
||
var parent TabWarehouseContainer
|
||
if err := models.DB.Select("id, title, parent_id").Where("id = ?", curID).First(&parent).Error; err != nil {
|
||
break
|
||
}
|
||
parentChain = append([]ParentItem{{ID: parent.ID, Title: parent.Title}}, parentChain...)
|
||
if parent.ParentID == nil {
|
||
break
|
||
}
|
||
curID = *parent.ParentID
|
||
}
|
||
}
|
||
|
||
// 计算当前容器深度(0=顶级,4=已达最大层级)
|
||
depth := len(parentChain)
|
||
|
||
// 关联图片
|
||
var binds []TabWarehouseContainerFileBind
|
||
models.DB.Where("container_id = ?", from.ID).Find(&binds)
|
||
var fileIDs []uint
|
||
for _, b := range binds {
|
||
fileIDs = append(fileIDs, b.FileID)
|
||
}
|
||
var files []TabFileInfo_
|
||
if len(fileIDs) > 0 {
|
||
models.DB.Where("id IN ?", fileIDs).Find(&files)
|
||
}
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{
|
||
"container": c,
|
||
"photos": files,
|
||
"parent_chain": parentChain,
|
||
"depth": depth,
|
||
})
|
||
})
|
||
|
||
// 新增物品(查重逻辑:Name+SerialNumber相同则更新容器)
|
||
r.POST("/add_item", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromAdd struct {
|
||
Name string `json:"name"`
|
||
SerialNumber string `json:"serial_number"`
|
||
Remark string `json:"remark"`
|
||
Quantity int `json:"quantity"`
|
||
ContainerID *uint `json:"container_id"`
|
||
Photos []string `json:"photos"`
|
||
}
|
||
var from FromAdd
|
||
if err := decodeJSON(data, &from); err != nil || from.Name == "" {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
// 校验图片哈希
|
||
for _, hash := range from.Photos {
|
||
if models.IsContainsSpecialChar(hash) {
|
||
ReturnJson(ctx, "photo_hash_invalid", nil)
|
||
return
|
||
}
|
||
}
|
||
|
||
quantity := from.Quantity
|
||
if quantity <= 0 {
|
||
quantity = 1
|
||
}
|
||
|
||
// 查重:Name + SerialNumber 相同则更新容器
|
||
var existingItem TabWarehouseItem
|
||
exists := models.DB.Where("name = ? AND serial_number = ?", from.Name, from.SerialNumber).First(&existingItem).Error == nil
|
||
|
||
var itemID uint
|
||
var oldContainer *uint
|
||
|
||
if exists {
|
||
// 已有记录:更新容器
|
||
oldContainer = existingItem.ContainerID
|
||
|
||
// 同一容器无需操作,但仍然记录 commit
|
||
if !ptrEqUint(oldContainer, from.ContainerID) {
|
||
// 旧容器 ItemCount -1
|
||
if oldContainer != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *oldContainer).Update("item_count", models.DB.Raw("item_count - 1"))
|
||
}
|
||
// 新容器 ItemCount +1
|
||
if from.ContainerID != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *from.ContainerID).Update("item_count", models.DB.Raw("item_count + 1"))
|
||
}
|
||
// 更新物品容器
|
||
existingItem.ContainerID = from.ContainerID
|
||
models.DB.Save(&existingItem)
|
||
}
|
||
|
||
itemID = existingItem.ID
|
||
|
||
// 写操作日志
|
||
newContent, _ := json.Marshal(from)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "item",
|
||
EntityID: itemID,
|
||
UserID: user.ID,
|
||
ActionType: "update",
|
||
OldContent: ptrStrUint(oldContainer),
|
||
NewContent: string(newContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
} else {
|
||
// 无记录:新建物品
|
||
item := TabWarehouseItem{
|
||
Name: from.Name,
|
||
SerialNumber: from.SerialNumber,
|
||
Remark: from.Remark,
|
||
Quantity: quantity,
|
||
CreatedAt: strconv.FormatInt(time.Now().Unix(), 10),
|
||
CreatorID: user.ID,
|
||
ContainerID: from.ContainerID,
|
||
}
|
||
models.DB.Create(&item)
|
||
itemID = item.ID
|
||
|
||
// 绑定图片
|
||
for _, hash := range from.Photos {
|
||
var findFile TabFileInfo_
|
||
if models.DB.Where(&TabFileInfo_{Sha256: hash, Type: "image"}).First(&findFile).Error == nil {
|
||
models.DB.Create(&TabWarehouseItemFileBind{
|
||
ItemID: item.ID,
|
||
FileID: findFile.ID,
|
||
CreatorID: user.ID,
|
||
})
|
||
}
|
||
}
|
||
|
||
// 所属容器的 ItemCount +1
|
||
if from.ContainerID != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *from.ContainerID).Update("item_count", models.DB.Raw("item_count + 1"))
|
||
}
|
||
|
||
// 写操作日志
|
||
newContent, _ := json.Marshal(from)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "item",
|
||
EntityID: itemID,
|
||
UserID: user.ID,
|
||
ActionType: "create",
|
||
NewContent: string(newContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
}
|
||
|
||
// 新增/更新时绑定图片
|
||
for _, hash := range from.Photos {
|
||
var findFile TabFileInfo_
|
||
if models.DB.Where(&TabFileInfo_{Sha256: hash, Type: "image"}).First(&findFile).Error == nil {
|
||
// 检查是否已绑定,避免重复
|
||
var count int64
|
||
models.DB.Model(&TabWarehouseItemFileBind{}).Where("item_id = ? AND file_id = ?", itemID, findFile.ID).Count(&count)
|
||
if count == 0 {
|
||
models.DB.Create(&TabWarehouseItemFileBind{
|
||
ItemID: itemID,
|
||
FileID: findFile.ID,
|
||
CreatorID: user.ID,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 记录 commit(无论新建还是更新容器都记录)
|
||
models.DB.Create(&TabWarehouseItemCommit{
|
||
ItemID: itemID,
|
||
UserID: user.ID,
|
||
OldContainer: oldContainer,
|
||
NewContainer: from.ContainerID,
|
||
Remark: from.Remark,
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{"id": itemID, "updated": exists})
|
||
})
|
||
|
||
// 编辑物品
|
||
r.POST("/update_item", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromUpdate struct {
|
||
ID uint `json:"id"`
|
||
Name string `json:"name"`
|
||
SerialNumber string `json:"serial_number"`
|
||
Remark string `json:"remark"`
|
||
Quantity int `json:"quantity"`
|
||
Photos []string `json:"photos"`
|
||
}
|
||
var from FromUpdate
|
||
if err := decodeJSON(data, &from); err != nil || from.ID == 0 || from.Name == "" {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
// 校验图片哈希
|
||
for _, hash := range from.Photos {
|
||
if models.IsContainsSpecialChar(hash) {
|
||
ReturnJson(ctx, "photo_hash_invalid", nil)
|
||
return
|
||
}
|
||
}
|
||
|
||
var item TabWarehouseItem
|
||
if err := models.DB.Where("id = ?", from.ID).First(&item).Error; err != nil {
|
||
ReturnJson(ctx, "item_not_found", nil)
|
||
return
|
||
}
|
||
|
||
oldContent, _ := json.Marshal(item)
|
||
models.DB.Model(&item).Updates(map[string]interface{}{
|
||
"name": from.Name,
|
||
"serial_number": from.SerialNumber,
|
||
"remark": from.Remark,
|
||
"quantity": from.Quantity,
|
||
})
|
||
|
||
// 重建图片绑定
|
||
models.DB.Where("item_id = ?", from.ID).Delete(&TabWarehouseItemFileBind{})
|
||
for _, hash := range from.Photos {
|
||
var findFile TabFileInfo_
|
||
if models.DB.Where(&TabFileInfo_{Sha256: hash, Type: "image"}).First(&findFile).Error == nil {
|
||
models.DB.Create(&TabWarehouseItemFileBind{
|
||
ItemID: from.ID,
|
||
FileID: findFile.ID,
|
||
CreatorID: user.ID,
|
||
})
|
||
}
|
||
}
|
||
|
||
newContent, _ := json.Marshal(from)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "item",
|
||
EntityID: from.ID,
|
||
UserID: user.ID,
|
||
ActionType: "update",
|
||
OldContent: string(oldContent),
|
||
NewContent: string(newContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
ReturnJson(ctx, "apiOK", nil)
|
||
})
|
||
|
||
// 删除物品
|
||
r.POST("/delete_item", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromDelete struct {
|
||
ID uint `json:"id"`
|
||
}
|
||
var from FromDelete
|
||
if err := decodeJSON(data, &from); err != nil || from.ID == 0 {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
var item TabWarehouseItem
|
||
if err := models.DB.Where("id = ?", from.ID).First(&item).Error; err != nil {
|
||
ReturnJson(ctx, "item_not_found", nil)
|
||
return
|
||
}
|
||
|
||
// 所属容器 ItemCount -1
|
||
if item.ContainerID != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *item.ContainerID).Update("item_count", models.DB.Raw("item_count - 1"))
|
||
}
|
||
|
||
// 删除关联
|
||
models.DB.Where("item_id = ?", from.ID).Delete(&TabWarehouseItemFileBind{})
|
||
models.DB.Where("item_id = ?", from.ID).Delete(&TabWarehouseItemCommit{})
|
||
models.DB.Where("item_id = ?", from.ID).Delete(&TabWarehouseItemWorkOrderBind{})
|
||
|
||
oldContent, _ := json.Marshal(item)
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "item",
|
||
EntityID: from.ID,
|
||
UserID: user.ID,
|
||
ActionType: "delete",
|
||
OldContent: string(oldContent),
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
models.DB.Delete(&item)
|
||
|
||
ReturnJson(ctx, "apiOK", nil)
|
||
})
|
||
|
||
// 获取物品列表
|
||
r.POST("/list_item", func(ctx *gin.Context) {
|
||
isAuth, _, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromList struct {
|
||
Search string `json:"search"`
|
||
ContainerID *uint `json:"container_id"`
|
||
Entries int `json:"entries"`
|
||
Page int `json:"page"`
|
||
}
|
||
var from FromList
|
||
if err := decodeJSON(data, &from); err != nil {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
if from.Entries <= 0 || from.Entries > 300 {
|
||
from.Entries = 10
|
||
}
|
||
if from.Page <= 0 {
|
||
from.Page = 1
|
||
}
|
||
|
||
var count int64
|
||
query := models.DB.Model(&TabWarehouseItem{})
|
||
if from.Search != "" {
|
||
query = query.Where("name LIKE ? OR serial_number LIKE ? OR remark LIKE ?",
|
||
"%"+from.Search+"%", "%"+from.Search+"%", "%"+from.Search+"%")
|
||
}
|
||
if from.ContainerID != nil {
|
||
query = query.Where("container_id = ?", *from.ContainerID)
|
||
}
|
||
query.Count(&count)
|
||
|
||
var items []TabWarehouseItem
|
||
query.Order("created_at DESC").
|
||
Offset(from.Entries * (from.Page - 1)).
|
||
Limit(from.Entries).
|
||
Find(&items)
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{
|
||
"all_count": count,
|
||
"items": items,
|
||
})
|
||
})
|
||
|
||
// 获取物品详情
|
||
r.POST("/get_item", func(ctx *gin.Context) {
|
||
isAuth, _, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromGet struct {
|
||
ID uint `json:"id"`
|
||
}
|
||
var from FromGet
|
||
if err := decodeJSON(data, &from); err != nil || from.ID == 0 {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
var item TabWarehouseItem
|
||
if err := models.DB.Where("id = ?", from.ID).First(&item).Error; err != nil {
|
||
ReturnJson(ctx, "item_not_found", nil)
|
||
return
|
||
}
|
||
|
||
// 关联图片
|
||
var binds []TabWarehouseItemFileBind
|
||
models.DB.Where("item_id = ?", from.ID).Find(&binds)
|
||
var fileIDs []uint
|
||
for _, b := range binds {
|
||
fileIDs = append(fileIDs, b.FileID)
|
||
}
|
||
var files []TabFileInfo_
|
||
if len(fileIDs) > 0 {
|
||
models.DB.Where("id IN ?", fileIDs).Find(&files)
|
||
}
|
||
|
||
// 移动历史
|
||
var commits []TabWarehouseItemCommit
|
||
models.DB.Where("item_id = ?", from.ID).Order("created_at DESC").Find(&commits)
|
||
|
||
// 关联工单
|
||
var woBinds []TabWarehouseItemWorkOrderBind
|
||
models.DB.Where("item_id = ?", from.ID).Find(&woBinds)
|
||
|
||
type WOInfo struct {
|
||
ID uint `json:"id"`
|
||
Title string `json:"title"`
|
||
Status string `json:"status"`
|
||
}
|
||
var workOrders []WOInfo
|
||
for _, b := range woBinds {
|
||
var wo TabWorkOrder
|
||
if models.DB.Where("id = ?", b.WorkOrderID).First(&wo).Error == nil {
|
||
workOrders = append(workOrders, WOInfo{ID: wo.ID, Title: wo.Title, Status: wo.CurrentStatus})
|
||
}
|
||
}
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{
|
||
"item": item,
|
||
"photos": files,
|
||
"commits": commits,
|
||
"work_orders": workOrders,
|
||
})
|
||
})
|
||
|
||
// 移动物品到其他容器
|
||
r.POST("/move_item", func(ctx *gin.Context) {
|
||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type FromMove struct {
|
||
ItemID uint `json:"item_id"`
|
||
NewContainer *uint `json:"new_container"`
|
||
Remark string `json:"remark"`
|
||
}
|
||
var from FromMove
|
||
if err := decodeJSON(data, &from); err != nil || from.ItemID == 0 {
|
||
ReturnJson(ctx, "jsonErr", nil)
|
||
return
|
||
}
|
||
|
||
var item TabWarehouseItem
|
||
if err := models.DB.Where("id = ?", from.ItemID).First(&item).Error; err != nil {
|
||
ReturnJson(ctx, "item_not_found", nil)
|
||
return
|
||
}
|
||
|
||
oldContainer := item.ContainerID
|
||
|
||
// 同一容器无需操作
|
||
if ptrEqUint(oldContainer, from.NewContainer) {
|
||
ReturnJson(ctx, "apiOK", nil)
|
||
return
|
||
}
|
||
|
||
// 旧容器 ItemCount -1
|
||
if oldContainer != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *oldContainer).Update("item_count", models.DB.Raw("item_count - 1"))
|
||
}
|
||
|
||
// 新容器 ItemCount +1
|
||
if from.NewContainer != nil {
|
||
models.DB.Model(&TabWarehouseContainer{}).Where("id = ?", *from.NewContainer).Update("item_count", models.DB.Raw("item_count + 1"))
|
||
}
|
||
|
||
// 更新物品容器
|
||
item.ContainerID = from.NewContainer
|
||
models.DB.Save(&item)
|
||
|
||
// 记录移动日志
|
||
models.DB.Create(&TabWarehouseItemCommit{
|
||
ItemID: from.ItemID,
|
||
UserID: user.ID,
|
||
OldContainer: oldContainer,
|
||
NewContainer: from.NewContainer,
|
||
Remark: from.Remark,
|
||
IP: ctx.ClientIP(),
|
||
})
|
||
|
||
// 写通用操作日志
|
||
models.DB.Create(&TabWarehouseLog{
|
||
EntityType: "item",
|
||
EntityID: from.ItemID,
|
||
UserID: user.ID,
|
||
ActionType: "move",
|
||
OldContent: ptrStrUint(oldContainer),
|
||
NewContent: ptrStrUint(from.NewContainer),
|
||
IP: ctx.ClientIP(),
|
||
Remark: from.Remark,
|
||
})
|
||
|
||
ReturnJson(ctx, "apiOK", nil)
|
||
})
|
||
|
||
// 获取仓库统计
|
||
r.POST("/count", func(ctx *gin.Context) {
|
||
isAuth, _, _ := AuthenticationAuthority(ctx)
|
||
if !isAuth {
|
||
ReturnJson(ctx, "userCookieError", nil)
|
||
return
|
||
}
|
||
|
||
type WCount struct {
|
||
ContainerTotal int64 `json:"container_total"`
|
||
ItemTotal int64 `json:"item_total"`
|
||
UnstoredItems int64 `json:"unstored_items"`
|
||
}
|
||
var count WCount
|
||
models.DB.Model(&TabWarehouseContainer{}).Count(&count.ContainerTotal)
|
||
models.DB.Model(&TabWarehouseItem{}).Count(&count.ItemTotal)
|
||
models.DB.Model(&TabWarehouseItem{}).Where("container_id IS NULL").Count(&count.UnstoredItems)
|
||
|
||
ReturnJson(ctx, "apiOK", gin.H{
|
||
"container_total": count.ContainerTotal,
|
||
"item_total": count.ItemTotal,
|
||
"unstored_items": count.UnstoredItems,
|
||
})
|
||
})
|
||
}
|
||
|
||
// ---------- 辅助函数 ----------
|
||
|
||
func ptrEqUint(a, b *uint) bool {
|
||
if a == nil && b == nil {
|
||
return true
|
||
}
|
||
if a == nil || b == nil {
|
||
return false
|
||
}
|
||
return *a == *b
|
||
}
|
||
|
||
func ptrStrUint(p *uint) string {
|
||
if p == nil {
|
||
return "nil"
|
||
}
|
||
return strconv.FormatUint(uint64(*p), 10)
|
||
}
|