Initial commit: Material Texture API service
- Go + Gin + GORM + PostgreSQL backend - RESTful API for material management - Docker deployment support - Database partitioning for billion-scale data - API documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
26
internal/config/config.go
Normal file
26
internal/config/config.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerPort string
|
||||
DatabaseURL string
|
||||
APIToken string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
ServerPort: getEnv("SERVER_PORT", "8080"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/material_db?sslmode=disable"),
|
||||
APIToken: getEnv("API_TOKEN", "seatons3d"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
33
internal/config/database.go
Normal file
33
internal/config/database.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func NewDatabase(cfg *Config) *gorm.DB {
|
||||
db, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get database instance: %v", err)
|
||||
}
|
||||
|
||||
// 连接池配置 (优化: 支持更高并发)
|
||||
sqlDB.SetMaxIdleConns(50) // 空闲连接数 (原 10)
|
||||
sqlDB.SetMaxOpenConns(200) // 最大连接数 (原 100)
|
||||
sqlDB.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
|
||||
sqlDB.SetConnMaxIdleTime(2 * time.Minute) // 空闲连接最大生命周期
|
||||
|
||||
log.Println("Database connected successfully")
|
||||
return db
|
||||
}
|
||||
157
internal/handlers/binding.go
Normal file
157
internal/handlers/binding.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"material_texture/internal/models"
|
||||
"material_texture/internal/repository"
|
||||
"material_texture/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type BindingHandler struct {
|
||||
bindingRepo *repository.BindingRepository
|
||||
materialRepo *repository.MaterialRepository
|
||||
}
|
||||
|
||||
func NewBindingHandler(bindingRepo *repository.BindingRepository, materialRepo *repository.MaterialRepository) *BindingHandler {
|
||||
return &BindingHandler{
|
||||
bindingRepo: bindingRepo,
|
||||
materialRepo: materialRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// BindMaterial 绑定材质到多个group_id
|
||||
// POST /api/v1/materials/:id/bindings
|
||||
// Body: {"group_ids": ["g1", "g2", "g3"]}
|
||||
func (h *BindingHandler) BindMaterial(c *gin.Context) {
|
||||
materialID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "invalid material id")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查材质是否存在
|
||||
exists, err := h.materialRepo.Exists(materialID)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to check material")
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
response.NotFound(c, "material not found")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.BindingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.bindingRepo.BindMaterial(materialID, req.GroupIDs); err != nil {
|
||||
response.InternalError(c, "failed to bind material")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"material_id": materialID,
|
||||
"group_ids": req.GroupIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// UnbindMaterial 解绑材质与指定group_id
|
||||
// DELETE /api/v1/materials/:id/bindings
|
||||
// Body: {"group_ids": ["g1", "g2"]}
|
||||
func (h *BindingHandler) UnbindMaterial(c *gin.Context) {
|
||||
materialID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "invalid material id")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UnbindRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.bindingRepo.UnbindMaterial(materialID, req.GroupIDs); err != nil {
|
||||
response.InternalError(c, "failed to unbind material")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"material_id": materialID,
|
||||
"unbound": req.GroupIDs,
|
||||
})
|
||||
}
|
||||
|
||||
// GetGroupsByMaterial 获取材质关联的所有group_id (分页版本)
|
||||
// GET /api/v1/materials/:id/groups?page=1&page_size=20
|
||||
func (h *BindingHandler) GetGroupsByMaterial(c *gin.Context) {
|
||||
materialID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "invalid material id")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析分页参数
|
||||
var query models.GroupListQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.BadRequest(c, "invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize < 1 || query.PageSize > 100 {
|
||||
query.PageSize = 20
|
||||
}
|
||||
|
||||
// 检查材质是否存在
|
||||
exists, err := h.materialRepo.Exists(materialID)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to check material")
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
response.NotFound(c, "material not found")
|
||||
return
|
||||
}
|
||||
|
||||
groupIDs, total, err := h.bindingRepo.GetGroupsByMaterialID(materialID, query.Page, query.PageSize)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to get groups")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回分页格式
|
||||
response.Success(c, models.GroupListResponse{
|
||||
Items: groupIDs,
|
||||
Total: total,
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetMaterialsByGroups 根据多个group_id获取关联的材质
|
||||
// POST /api/v1/groups/materials
|
||||
// Body: {"group_ids": ["g1", "g2", "g3"]}
|
||||
func (h *BindingHandler) GetMaterialsByGroups(c *gin.Context) {
|
||||
var req models.GroupMaterialsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
results, err := h.bindingRepo.GetMaterialsByGroupIDs(req.GroupIDs)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to get materials")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, results)
|
||||
}
|
||||
178
internal/handlers/material.go
Normal file
178
internal/handlers/material.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"material_texture/internal/models"
|
||||
"material_texture/internal/repository"
|
||||
"material_texture/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MaterialHandler struct {
|
||||
repo *repository.MaterialRepository
|
||||
}
|
||||
|
||||
func NewMaterialHandler(repo *repository.MaterialRepository) *MaterialHandler {
|
||||
return &MaterialHandler{repo: repo}
|
||||
}
|
||||
|
||||
// List 获取材质列表
|
||||
// GET /api/v1/materials?page=1&page_size=20&name=xxx
|
||||
func (h *MaterialHandler) List(c *gin.Context) {
|
||||
var query models.MaterialListQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
response.BadRequest(c, "invalid query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize < 1 || query.PageSize > 100 {
|
||||
query.PageSize = 20
|
||||
}
|
||||
|
||||
materials, total, err := h.repo.List(query)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to fetch materials")
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessPaged(c, materials, total, query.Page, query.PageSize)
|
||||
}
|
||||
|
||||
// GetByID 获取单个材质详情
|
||||
// GET /api/v1/materials/:id
|
||||
func (h *MaterialHandler) GetByID(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "invalid material id")
|
||||
return
|
||||
}
|
||||
|
||||
material, err := h.repo.GetByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.NotFound(c, "material not found")
|
||||
return
|
||||
}
|
||||
response.InternalError(c, "failed to fetch material")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, material)
|
||||
}
|
||||
|
||||
// Create 创建材质
|
||||
// POST /api/v1/materials
|
||||
func (h *MaterialHandler) Create(c *gin.Context) {
|
||||
var req models.MaterialRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
material := &models.Material{
|
||||
Name: req.Name,
|
||||
DiffuseR: req.DiffuseR,
|
||||
DiffuseG: req.DiffuseG,
|
||||
DiffuseB: req.DiffuseB,
|
||||
Alpha: req.Alpha,
|
||||
Shininess: req.Shininess,
|
||||
SpecularR: req.SpecularR,
|
||||
SpecularG: req.SpecularG,
|
||||
SpecularB: req.SpecularB,
|
||||
AmbientR: req.AmbientR,
|
||||
AmbientG: req.AmbientG,
|
||||
AmbientB: req.AmbientB,
|
||||
Metallic: req.Metallic,
|
||||
Roughness: req.Roughness,
|
||||
Reflectance: req.Reflectance,
|
||||
}
|
||||
|
||||
if err := h.repo.Create(material); err != nil {
|
||||
response.InternalError(c, "failed to create material")
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, material)
|
||||
}
|
||||
|
||||
// Update 更新材质 (优化: 单次查询)
|
||||
// PUT /api/v1/materials/:id
|
||||
func (h *MaterialHandler) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "invalid material id")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.MaterialRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 直接更新,通过 RowsAffected 判断是否存在
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"diffuse_r": req.DiffuseR,
|
||||
"diffuse_g": req.DiffuseG,
|
||||
"diffuse_b": req.DiffuseB,
|
||||
"alpha": req.Alpha,
|
||||
"shininess": req.Shininess,
|
||||
"specular_r": req.SpecularR,
|
||||
"specular_g": req.SpecularG,
|
||||
"specular_b": req.SpecularB,
|
||||
"ambient_r": req.AmbientR,
|
||||
"ambient_g": req.AmbientG,
|
||||
"ambient_b": req.AmbientB,
|
||||
"metallic": req.Metallic,
|
||||
"roughness": req.Roughness,
|
||||
"reflectance": req.Reflectance,
|
||||
}
|
||||
|
||||
rowsAffected, err := h.repo.UpdateByID(id, updates)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to update material")
|
||||
return
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
response.NotFound(c, "material not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的数据
|
||||
material, _ := h.repo.GetByID(id)
|
||||
response.Success(c, material)
|
||||
}
|
||||
|
||||
// Delete 删除材质 (优化: 单次查询,通过 RowsAffected 判断)
|
||||
// DELETE /api/v1/materials/:id
|
||||
func (h *MaterialHandler) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "invalid material id")
|
||||
return
|
||||
}
|
||||
|
||||
// 直接删除,通过 RowsAffected 判断是否存在
|
||||
rowsAffected, err := h.repo.DeleteByID(id)
|
||||
if err != nil {
|
||||
response.InternalError(c, "failed to delete material")
|
||||
return
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
response.NotFound(c, "material not found")
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"id": id})
|
||||
}
|
||||
32
internal/middleware/auth.go
Normal file
32
internal/middleware/auth.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"material_texture/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderAPIToken = "X-API-Token"
|
||||
)
|
||||
|
||||
// TokenAuth 简单Token认证中间件
|
||||
func TokenAuth(expectedToken string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader(HeaderAPIToken)
|
||||
|
||||
if token == "" {
|
||||
response.Unauthorized(c, "missing API token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if token != expectedToken {
|
||||
response.Unauthorized(c, "invalid API token")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
54
internal/models/binding.go
Normal file
54
internal/models/binding.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type MaterialBinding struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
MaterialID int64 `json:"material_id" gorm:"not null;index:idx_bindings_material_id"`
|
||||
GroupID string `json:"group_id" gorm:"size:255;not null;index:idx_bindings_group_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
|
||||
// 关联的材质(用于预加载)
|
||||
Material *Material `json:"material,omitempty" gorm:"foreignKey:MaterialID;constraint:OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
func (MaterialBinding) TableName() string {
|
||||
return "material_bindings"
|
||||
}
|
||||
|
||||
// 绑定请求 - 一个材质绑定到多个group_id
|
||||
type BindingRequest struct {
|
||||
GroupIDs []string `json:"group_ids" binding:"required,min=1,max=10000"`
|
||||
}
|
||||
|
||||
// 解绑请求
|
||||
type UnbindRequest struct {
|
||||
GroupIDs []string `json:"group_ids" binding:"required,min=1,max=10000"`
|
||||
}
|
||||
|
||||
// 批量查询group_id关联材质的请求
|
||||
type GroupMaterialsRequest struct {
|
||||
GroupIDs []string `json:"group_ids" binding:"required,min=1,max=10000"`
|
||||
}
|
||||
|
||||
// 批量查询结果 - 包含材质详情
|
||||
type GroupMaterialResult struct {
|
||||
GroupID string `json:"group_id"`
|
||||
Material *Material `json:"material"`
|
||||
}
|
||||
|
||||
// GetGroups 分页查询参数
|
||||
type GroupListQuery struct {
|
||||
Page int `form:"page"`
|
||||
PageSize int `form:"page_size"`
|
||||
}
|
||||
|
||||
// GetGroups 分页响应
|
||||
type GroupListResponse struct {
|
||||
Items []string `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
68
internal/models/material.go
Normal file
68
internal/models/material.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Material struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"size:255;not null"`
|
||||
|
||||
// 漫反射颜色 (Diffuse)
|
||||
DiffuseR float64 `json:"diffuse_r" gorm:"not null;default:0"`
|
||||
DiffuseG float64 `json:"diffuse_g" gorm:"not null;default:0"`
|
||||
DiffuseB float64 `json:"diffuse_b" gorm:"not null;default:0"`
|
||||
Alpha float64 `json:"alpha" gorm:"not null;default:1"`
|
||||
|
||||
// 高光 (Specular)
|
||||
Shininess float64 `json:"shininess" gorm:"not null;default:0"`
|
||||
SpecularR float64 `json:"specular_r" gorm:"not null;default:0"`
|
||||
SpecularG float64 `json:"specular_g" gorm:"not null;default:0"`
|
||||
SpecularB float64 `json:"specular_b" gorm:"not null;default:0"`
|
||||
|
||||
// 环境光 (Ambient)
|
||||
AmbientR float64 `json:"ambient_r" gorm:"not null;default:0"`
|
||||
AmbientG float64 `json:"ambient_g" gorm:"not null;default:0"`
|
||||
AmbientB float64 `json:"ambient_b" gorm:"not null;default:0"`
|
||||
|
||||
// PBR属性
|
||||
Metallic float64 `json:"metallic" gorm:"not null;default:0"`
|
||||
Roughness float64 `json:"roughness" gorm:"not null;default:0.5"`
|
||||
Reflectance float64 `json:"reflectance" gorm:"not null;default:0.5"`
|
||||
|
||||
// 乐观锁版本号 (用于防止并发覆盖)
|
||||
Version int64 `json:"version" gorm:"not null;default:0"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
func (Material) TableName() string {
|
||||
return "materials"
|
||||
}
|
||||
|
||||
// 创建/更新材质的请求结构
|
||||
type MaterialRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
DiffuseR float64 `json:"diffuse_r"`
|
||||
DiffuseG float64 `json:"diffuse_g"`
|
||||
DiffuseB float64 `json:"diffuse_b"`
|
||||
Alpha float64 `json:"alpha"`
|
||||
Shininess float64 `json:"shininess"`
|
||||
SpecularR float64 `json:"specular_r"`
|
||||
SpecularG float64 `json:"specular_g"`
|
||||
SpecularB float64 `json:"specular_b"`
|
||||
AmbientR float64 `json:"ambient_r"`
|
||||
AmbientG float64 `json:"ambient_g"`
|
||||
AmbientB float64 `json:"ambient_b"`
|
||||
Metallic float64 `json:"metallic"`
|
||||
Roughness float64 `json:"roughness"`
|
||||
Reflectance float64 `json:"reflectance"`
|
||||
}
|
||||
|
||||
// 列表查询参数
|
||||
type MaterialListQuery struct {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"page_size,default=20"`
|
||||
Name string `form:"name"`
|
||||
}
|
||||
104
internal/repository/binding.go
Normal file
104
internal/repository/binding.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"material_texture/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type BindingRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBindingRepository(db *gorm.DB) *BindingRepository {
|
||||
return &BindingRepository{db: db}
|
||||
}
|
||||
|
||||
// BindMaterial 绑定材质到多个group_id(幂等操作,使用upsert)
|
||||
// 优化: 分批处理,避免单条 SQL 过大
|
||||
func (r *BindingRepository) BindMaterial(materialID int64, groupIDs []string) error {
|
||||
const batchSize = 1000 // 每批最多 1000 条
|
||||
|
||||
for i := 0; i < len(groupIDs); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(groupIDs) {
|
||||
end = len(groupIDs)
|
||||
}
|
||||
|
||||
batch := groupIDs[i:end]
|
||||
bindings := make([]models.MaterialBinding, len(batch))
|
||||
for j, groupID := range batch {
|
||||
bindings[j] = models.MaterialBinding{
|
||||
MaterialID: materialID,
|
||||
GroupID: groupID,
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 ON CONFLICT DO NOTHING 实现幂等
|
||||
if err := r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "material_id"}, {Name: "group_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(&bindings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnbindMaterial 解绑材质与指定的group_id
|
||||
func (r *BindingRepository) UnbindMaterial(materialID int64, groupIDs []string) error {
|
||||
return r.db.Where("material_id = ? AND group_id IN ?", materialID, groupIDs).
|
||||
Delete(&models.MaterialBinding{}).Error
|
||||
}
|
||||
|
||||
// GetGroupsByMaterialID 根据材质ID获取所有关联的group_id (分页版本)
|
||||
func (r *BindingRepository) GetGroupsByMaterialID(materialID int64, page, pageSize int) ([]string, int64, error) {
|
||||
var groupIDs []string
|
||||
var total int64
|
||||
|
||||
db := r.db.Model(&models.MaterialBinding{}).Where("material_id = ?", materialID)
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
err := db.Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Pluck("group_id", &groupIDs).Error
|
||||
|
||||
return groupIDs, total, err
|
||||
}
|
||||
|
||||
// GetMaterialsByGroupIDs 根据多个group_id获取关联的材质(含材质详情)
|
||||
func (r *BindingRepository) GetMaterialsByGroupIDs(groupIDs []string) ([]models.GroupMaterialResult, error) {
|
||||
var bindings []models.MaterialBinding
|
||||
|
||||
err := r.db.Preload("Material").
|
||||
Where("group_id IN ?", groupIDs).
|
||||
Find(&bindings).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]models.GroupMaterialResult, len(bindings))
|
||||
for i, binding := range bindings {
|
||||
results[i] = models.GroupMaterialResult{
|
||||
GroupID: binding.GroupID,
|
||||
Material: binding.Material,
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// DeleteByMaterialID 删除材质的所有绑定(材质删除时级联调用)
|
||||
func (r *BindingRepository) DeleteByMaterialID(materialID int64) error {
|
||||
return r.db.Where("material_id = ?", materialID).
|
||||
Delete(&models.MaterialBinding{}).Error
|
||||
}
|
||||
99
internal/repository/material.go
Normal file
99
internal/repository/material.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"material_texture/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type MaterialRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewMaterialRepository(db *gorm.DB) *MaterialRepository {
|
||||
return &MaterialRepository{db: db}
|
||||
}
|
||||
|
||||
// List 获取材质列表(支持分页和名称搜索)
|
||||
func (r *MaterialRepository) List(query models.MaterialListQuery) ([]models.Material, int64, error) {
|
||||
var materials []models.Material
|
||||
var total int64
|
||||
|
||||
db := r.db.Model(&models.Material{})
|
||||
|
||||
// 名称模糊搜索
|
||||
if query.Name != "" {
|
||||
db = db.Where("name ILIKE ?", "%"+query.Name+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (query.Page - 1) * query.PageSize
|
||||
if err := db.Order("id DESC").Offset(offset).Limit(query.PageSize).Find(&materials).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return materials, total, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取单个材质
|
||||
func (r *MaterialRepository) GetByID(id int64) (*models.Material, error) {
|
||||
var material models.Material
|
||||
if err := r.db.First(&material, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &material, nil
|
||||
}
|
||||
|
||||
// Create 创建材质
|
||||
func (r *MaterialRepository) Create(material *models.Material) error {
|
||||
return r.db.Create(material).Error
|
||||
}
|
||||
|
||||
// Update 更新材质 (优化: 直接 UPDATE,返回影响行数)
|
||||
func (r *MaterialRepository) Update(material *models.Material) error {
|
||||
return r.db.Save(material).Error
|
||||
}
|
||||
|
||||
// UpdateByID 根据 ID 直接更新 (优化版本,减少查询)
|
||||
func (r *MaterialRepository) UpdateByID(id int64, updates map[string]interface{}) (int64, error) {
|
||||
result := r.db.Model(&models.Material{}).Where("id = ?", id).Updates(updates)
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// UpdateByIDWithVersion 带乐观锁的更新 (防止并发覆盖)
|
||||
// 只有当 version 匹配时才更新,并自动递增 version
|
||||
func (r *MaterialRepository) UpdateByIDWithVersion(id int64, version int64, updates map[string]interface{}) (int64, error) {
|
||||
// 在更新中自动递增 version
|
||||
updates["version"] = gorm.Expr("version + 1")
|
||||
|
||||
result := r.db.Model(&models.Material{}).
|
||||
Where("id = ? AND version = ?", id, version).
|
||||
Updates(updates)
|
||||
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// Delete 删除材质
|
||||
func (r *MaterialRepository) Delete(id int64) error {
|
||||
return r.db.Delete(&models.Material{}, id).Error
|
||||
}
|
||||
|
||||
// DeleteByID 删除材质并返回影响行数 (优化版本)
|
||||
func (r *MaterialRepository) DeleteByID(id int64) (int64, error) {
|
||||
result := r.db.Delete(&models.Material{}, id)
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// Exists 检查材质是否存在
|
||||
func (r *MaterialRepository) Exists(id int64) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&models.Material{}).Where("id = ?", id).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
53
internal/router/router.go
Normal file
53
internal/router/router.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"material_texture/internal/config"
|
||||
"material_texture/internal/handlers"
|
||||
"material_texture/internal/middleware"
|
||||
"material_texture/internal/repository"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// 健康检查(无需认证)
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// 初始化仓库
|
||||
materialRepo := repository.NewMaterialRepository(db)
|
||||
bindingRepo := repository.NewBindingRepository(db)
|
||||
|
||||
// 初始化处理器
|
||||
materialHandler := handlers.NewMaterialHandler(materialRepo)
|
||||
bindingHandler := handlers.NewBindingHandler(bindingRepo, materialRepo)
|
||||
|
||||
// API v1 路由组
|
||||
v1 := r.Group("/api/v1")
|
||||
v1.Use(middleware.TokenAuth(cfg.APIToken))
|
||||
{
|
||||
// 材质 CRUD
|
||||
materials := v1.Group("/materials")
|
||||
{
|
||||
materials.GET("", materialHandler.List)
|
||||
materials.POST("", materialHandler.Create)
|
||||
materials.GET("/:id", materialHandler.GetByID)
|
||||
materials.PUT("/:id", materialHandler.Update)
|
||||
materials.DELETE("/:id", materialHandler.Delete)
|
||||
|
||||
// 绑定管理
|
||||
materials.POST("/:id/bindings", bindingHandler.BindMaterial)
|
||||
materials.DELETE("/:id/bindings", bindingHandler.UnbindMaterial)
|
||||
materials.GET("/:id/groups", bindingHandler.GetGroupsByMaterial)
|
||||
}
|
||||
|
||||
// 根据group_id查询材质
|
||||
v1.POST("/groups/materials", bindingHandler.GetMaterialsByGroups)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user