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"` ItemID uint `gorm:"not null;index;comment:关联物品id"` UserID uint `gorm:"not null;comment:操作人id"` OldContainer *uint `gorm:"index;comment:原容器id"` NewContainer *uint `gorm:"index;comment:新容器id"` Remark string `gorm:"type:text;comment:备注"` IP string `gorm:"size:50;comment:操作IP"` CreatedAt time.Time `gorm:"type:datetime;autoCreateTime"` } 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 { 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 } // 关联图片 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, }) }) // 新增物品 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 } 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) // 绑定图片 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: item.ID, UserID: user.ID, ActionType: "create", NewContent: string(newContent), IP: ctx.ClientIP(), }) ReturnJson(ctx, "apiOK", gin.H{"id": item.ID}) }) // 编辑物品 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) }