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:
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# 服务器配置
|
||||||
|
SERVER_PORT=8080
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/material_db?sslmode=disable
|
||||||
|
|
||||||
|
# API认证Token
|
||||||
|
API_TOKEN=seatons3d
|
||||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 二进制文件
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# 测试覆盖
|
||||||
|
*.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# 依赖目录
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
*.log
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 构建阶段
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装必要的构建工具
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
# 复制go.mod和go.sum
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app/server ./cmd/server
|
||||||
|
|
||||||
|
# 运行阶段
|
||||||
|
FROM alpine:3.18
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装必要的运行时依赖
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
# 从构建阶段复制二进制文件
|
||||||
|
COPY --from=builder /app/server /app/server
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/app/server"]
|
||||||
68
Makefile
Normal file
68
Makefile
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.PHONY: build run dev docker-build docker-up docker-down docker-logs clean test
|
||||||
|
|
||||||
|
# 本地开发
|
||||||
|
build:
|
||||||
|
go build -o bin/server ./cmd/server
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./bin/server
|
||||||
|
|
||||||
|
dev:
|
||||||
|
go run ./cmd/server
|
||||||
|
|
||||||
|
# 依赖管理
|
||||||
|
deps:
|
||||||
|
go mod tidy
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
# Docker 操作
|
||||||
|
docker-build:
|
||||||
|
docker compose build --no-cache
|
||||||
|
|
||||||
|
docker-up:
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
docker-down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
docker-logs:
|
||||||
|
docker compose logs -f
|
||||||
|
|
||||||
|
docker-restart:
|
||||||
|
docker compose down && docker compose up -d
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
clean:
|
||||||
|
rm -rf bin/
|
||||||
|
docker compose down -v
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# 数据库迁移(本地开发用)
|
||||||
|
migrate:
|
||||||
|
go run ./cmd/server migrate
|
||||||
|
|
||||||
|
# 格式化
|
||||||
|
fmt:
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
# 检查
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# 帮助
|
||||||
|
help:
|
||||||
|
@echo "Available commands:"
|
||||||
|
@echo " make build - Build the binary"
|
||||||
|
@echo " make run - Build and run locally"
|
||||||
|
@echo " make dev - Run with go run (development)"
|
||||||
|
@echo " make deps - Download dependencies"
|
||||||
|
@echo " make docker-build - Build Docker image"
|
||||||
|
@echo " make docker-up - Start Docker containers"
|
||||||
|
@echo " make docker-down - Stop Docker containers"
|
||||||
|
@echo " make docker-logs - View Docker logs"
|
||||||
|
@echo " make clean - Clean build artifacts and volumes"
|
||||||
|
@echo " make test - Run tests"
|
||||||
|
@echo " make fmt - Format code"
|
||||||
37
cmd/server/main.go
Normal file
37
cmd/server/main.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"material_texture/internal/config"
|
||||||
|
"material_texture/internal/models"
|
||||||
|
"material_texture/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 加载配置
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
db := config.NewDatabase(cfg)
|
||||||
|
|
||||||
|
// 自动迁移表结构
|
||||||
|
if err := db.AutoMigrate(&models.Material{}, &models.MaterialBinding{}); err != nil {
|
||||||
|
log.Fatalf("Failed to migrate database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加唯一约束(GORM的uniqueIndex在某些情况下可能不生效)
|
||||||
|
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_material_group
|
||||||
|
ON material_bindings(material_id, group_id)`)
|
||||||
|
|
||||||
|
log.Println("Database migrated successfully")
|
||||||
|
|
||||||
|
// 设置路由
|
||||||
|
r := router.Setup(db, cfg)
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
log.Printf("Server starting on port %s", cfg.ServerPort)
|
||||||
|
if err := r.Run(":" + cfg.ServerPort); err != nil {
|
||||||
|
log.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
190
deploy.sh
Executable file
190
deploy.sh
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Material Texture API 部署脚本
|
||||||
|
# 用法: ./deploy.sh [命令]
|
||||||
|
# 命令:
|
||||||
|
# pack - 打包项目用于传输
|
||||||
|
# server - 在服务器上执行部署
|
||||||
|
# help - 显示帮助
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROJECT_NAME="material_texture"
|
||||||
|
DEPLOY_DIR="deploy_package"
|
||||||
|
ARCHIVE_NAME="${PROJECT_NAME}_deploy.tar.gz"
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 本地打包(在开发机器上执行)
|
||||||
|
pack() {
|
||||||
|
log_info "开始打包项目..."
|
||||||
|
|
||||||
|
# 清理旧文件
|
||||||
|
rm -rf "$DEPLOY_DIR" "$ARCHIVE_NAME"
|
||||||
|
|
||||||
|
# 创建部署目录
|
||||||
|
mkdir -p "$DEPLOY_DIR"
|
||||||
|
|
||||||
|
# 复制必要文件(排除不需要的)
|
||||||
|
rsync -av \
|
||||||
|
--exclude='bin/' \
|
||||||
|
--exclude='.git/' \
|
||||||
|
--exclude='*.log' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='.env.local' \
|
||||||
|
--exclude='.DS_Store' \
|
||||||
|
--exclude='deploy_package/' \
|
||||||
|
--exclude='*.tar.gz' \
|
||||||
|
. "$DEPLOY_DIR/"
|
||||||
|
|
||||||
|
# 打包
|
||||||
|
tar -czvf "$ARCHIVE_NAME" "$DEPLOY_DIR"
|
||||||
|
|
||||||
|
# 清理临时目录
|
||||||
|
rm -rf "$DEPLOY_DIR"
|
||||||
|
|
||||||
|
# 显示文件大小
|
||||||
|
SIZE=$(du -h "$ARCHIVE_NAME" | cut -f1)
|
||||||
|
log_info "打包完成: $ARCHIVE_NAME ($SIZE)"
|
||||||
|
log_info ""
|
||||||
|
log_info "下一步: 将文件传输到服务器"
|
||||||
|
echo " scp $ARCHIVE_NAME user@your-server:/path/to/deploy/"
|
||||||
|
log_info ""
|
||||||
|
log_info "然后在服务器上执行:"
|
||||||
|
echo " tar -xzvf $ARCHIVE_NAME"
|
||||||
|
echo " cd $DEPLOY_DIR"
|
||||||
|
echo " ./deploy.sh server"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 服务器部署(在服务器上执行)
|
||||||
|
server_deploy() {
|
||||||
|
log_info "开始服务器部署..."
|
||||||
|
|
||||||
|
# 检查 Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log_error "Docker 未安装,请先安装 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 docker compose
|
||||||
|
if ! docker compose version &> /dev/null; then
|
||||||
|
log_error "Docker Compose 未安装或版本过低"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建 .env 文件(如果不存在)
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
log_info "创建 .env 配置文件..."
|
||||||
|
cp .env.example .env
|
||||||
|
log_warn "请编辑 .env 文件设置生产环境密钥"
|
||||||
|
log_warn " vim .env"
|
||||||
|
log_warn "然后重新运行: ./deploy.sh server"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 停止旧容器(如果存在)
|
||||||
|
log_info "停止旧容器..."
|
||||||
|
docker compose down 2>/dev/null || true
|
||||||
|
|
||||||
|
# 构建并启动
|
||||||
|
log_info "构建并启动服务..."
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# 等待服务启动
|
||||||
|
log_info "等待服务启动..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
log_info "执行健康检查..."
|
||||||
|
if curl -s http://localhost:8081/health > /dev/null; then
|
||||||
|
log_info "服务启动成功!"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " API 地址: http://localhost:8081"
|
||||||
|
echo " 健康检查: http://localhost:8081/health"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
log_info "查看日志: docker compose logs -f"
|
||||||
|
else
|
||||||
|
log_error "健康检查失败,请查看日志: docker compose logs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示帮助
|
||||||
|
show_help() {
|
||||||
|
echo "Material Texture API 部署脚本"
|
||||||
|
echo ""
|
||||||
|
echo "用法: ./deploy.sh [命令]"
|
||||||
|
echo ""
|
||||||
|
echo "命令:"
|
||||||
|
echo " pack 在本地机器打包项目用于传输"
|
||||||
|
echo " server 在服务器上执行部署"
|
||||||
|
echo " logs 查看容器日志"
|
||||||
|
echo " stop 停止服务"
|
||||||
|
echo " restart 重启服务"
|
||||||
|
echo " help 显示此帮助信息"
|
||||||
|
echo ""
|
||||||
|
echo "部署流程:"
|
||||||
|
echo " 1. 本地: ./deploy.sh pack"
|
||||||
|
echo " 2. 传输: scp material_texture_deploy.tar.gz user@server:/path/"
|
||||||
|
echo " 3. 服务器: tar -xzvf material_texture_deploy.tar.gz"
|
||||||
|
echo " 4. 服务器: cd deploy_package && ./deploy.sh server"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
logs() {
|
||||||
|
docker compose logs -f
|
||||||
|
}
|
||||||
|
|
||||||
|
# 停止
|
||||||
|
stop() {
|
||||||
|
log_info "停止服务..."
|
||||||
|
docker compose down
|
||||||
|
log_info "服务已停止"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
restart() {
|
||||||
|
log_info "重启服务..."
|
||||||
|
docker compose restart
|
||||||
|
log_info "服务已重启"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主入口
|
||||||
|
case "${1:-help}" in
|
||||||
|
pack)
|
||||||
|
pack
|
||||||
|
;;
|
||||||
|
server)
|
||||||
|
server_deploy
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
logs
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
help|*)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
44
docker-compose.yml
Normal file
44
docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
environment:
|
||||||
|
- SERVER_PORT=8080
|
||||||
|
- DATABASE_URL=postgres://postgres:postgres@db:5432/material_db?sslmode=disable
|
||||||
|
- API_TOKEN=${API_TOKEN:-seatons3d}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- material-network
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_DB=material_db
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./migrations:/docker-entrypoint-initdb.d:ro
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- material-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
material-network:
|
||||||
|
driver: bridge
|
||||||
569
docs/API.md
Normal file
569
docs/API.md
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
# 材质管理系统 API 文档
|
||||||
|
|
||||||
|
## 基础信息
|
||||||
|
|
||||||
|
- **Base URL**: `http://10.99.98.248:8081/api/v1`
|
||||||
|
- **认证方式**: API Token
|
||||||
|
- **Content-Type**: `application/json`
|
||||||
|
|
||||||
|
## 认证
|
||||||
|
|
||||||
|
所有API请求需要在Header中携带Token:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-API-Token: seatons3d
|
||||||
|
```
|
||||||
|
|
||||||
|
错误响应示例:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 401,
|
||||||
|
"message": "missing API token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用响应格式
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分页响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"items": [...],
|
||||||
|
"total": 303,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"message": "error description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误码
|
||||||
|
| 状态码 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 201 | 创建成功 |
|
||||||
|
| 400 | 请求参数错误 |
|
||||||
|
| 401 | 未授权(Token无效) |
|
||||||
|
| 404 | 资源不存在 |
|
||||||
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 材质管理接口
|
||||||
|
|
||||||
|
### 1. 获取材质列表
|
||||||
|
|
||||||
|
**GET** `/materials`
|
||||||
|
|
||||||
|
#### 请求参数(Query)
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码,默认1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认20,最大100 |
|
||||||
|
| name | string | 否 | 按名称模糊搜索 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Token: seatons3d" \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials?page=1&page_size=10&name=red"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "dgnColor4",
|
||||||
|
"diffuse_r": 255,
|
||||||
|
"diffuse_g": 0,
|
||||||
|
"diffuse_b": 0,
|
||||||
|
"alpha": 255,
|
||||||
|
"shininess": 20,
|
||||||
|
"specular_r": 230,
|
||||||
|
"specular_g": 230,
|
||||||
|
"specular_b": 230,
|
||||||
|
"ambient_r": 50,
|
||||||
|
"ambient_g": 50,
|
||||||
|
"ambient_b": 50,
|
||||||
|
"metallic": 0,
|
||||||
|
"roughness": 0,
|
||||||
|
"reflectance": 0,
|
||||||
|
"created_at": "2025-12-09T09:53:33.209555Z",
|
||||||
|
"updated_at": "2025-12-09T09:53:33.209555Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 303,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 添加材质
|
||||||
|
|
||||||
|
**POST** `/materials`
|
||||||
|
|
||||||
|
#### 请求Body
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| name | string | 是 | 材质名称 |
|
||||||
|
| diffuse_r | float | 否 | 漫反射-红 (0-255) |
|
||||||
|
| diffuse_g | float | 否 | 漫反射-绿 (0-255) |
|
||||||
|
| diffuse_b | float | 否 | 漫反射-蓝 (0-255) |
|
||||||
|
| alpha | float | 否 | 透明度 (0-255) |
|
||||||
|
| shininess | float | 否 | 光泽度 |
|
||||||
|
| specular_r | float | 否 | 高光-红 (0-255) |
|
||||||
|
| specular_g | float | 否 | 高光-绿 (0-255) |
|
||||||
|
| specular_b | float | 否 | 高光-蓝 (0-255) |
|
||||||
|
| ambient_r | float | 否 | 环境光-红 (0-255) |
|
||||||
|
| ambient_g | float | 否 | 环境光-绿 (0-255) |
|
||||||
|
| ambient_b | float | 否 | 环境光-蓝 (0-255) |
|
||||||
|
| metallic | float | 否 | 金属度 |
|
||||||
|
| roughness | float | 否 | 粗糙度 |
|
||||||
|
| reflectance | float | 否 | 反射率 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "X-API-Token: seatons3d" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "红色金属",
|
||||||
|
"diffuse_r": 255,
|
||||||
|
"diffuse_g": 50,
|
||||||
|
"diffuse_b": 50,
|
||||||
|
"alpha": 255,
|
||||||
|
"metallic": 0.8,
|
||||||
|
"roughness": 0.2
|
||||||
|
}' \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "created",
|
||||||
|
"data": {
|
||||||
|
"id": 305,
|
||||||
|
"name": "红色金属",
|
||||||
|
"diffuse_r": 255,
|
||||||
|
"diffuse_g": 50,
|
||||||
|
"diffuse_b": 50,
|
||||||
|
"alpha": 255,
|
||||||
|
"shininess": 0,
|
||||||
|
"specular_r": 0,
|
||||||
|
"specular_g": 0,
|
||||||
|
"specular_b": 0,
|
||||||
|
"ambient_r": 0,
|
||||||
|
"ambient_g": 0,
|
||||||
|
"ambient_b": 0,
|
||||||
|
"metallic": 0.8,
|
||||||
|
"roughness": 0.2,
|
||||||
|
"reflectance": 0.5,
|
||||||
|
"created_at": "2025-12-09T18:02:22.846141Z",
|
||||||
|
"updated_at": "2025-12-09T18:02:22.846141Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 获取材质详情
|
||||||
|
|
||||||
|
**GET** `/materials/:id`
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int | 材质ID |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Token: seatons3d" \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials/4"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 4,
|
||||||
|
"name": "dgnColor4",
|
||||||
|
"diffuse_r": 255,
|
||||||
|
"diffuse_g": 0,
|
||||||
|
"diffuse_b": 0,
|
||||||
|
"alpha": 255,
|
||||||
|
"shininess": 20,
|
||||||
|
"specular_r": 230,
|
||||||
|
"specular_g": 230,
|
||||||
|
"specular_b": 230,
|
||||||
|
"ambient_r": 50,
|
||||||
|
"ambient_g": 50,
|
||||||
|
"ambient_b": 50,
|
||||||
|
"metallic": 0,
|
||||||
|
"roughness": 0,
|
||||||
|
"reflectance": 0,
|
||||||
|
"created_at": "2025-12-09T09:53:33.209555Z",
|
||||||
|
"updated_at": "2025-12-09T09:53:33.209555Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 错误响应
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 404,
|
||||||
|
"message": "material not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 编辑材质
|
||||||
|
|
||||||
|
**PUT** `/materials/:id`
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int | 材质ID |
|
||||||
|
|
||||||
|
#### 请求Body
|
||||||
|
同"添加材质"接口
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -X PUT -H "X-API-Token: seatons3d" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "红色金属-更新版",
|
||||||
|
"diffuse_r": 200,
|
||||||
|
"diffuse_g": 100,
|
||||||
|
"diffuse_b": 50,
|
||||||
|
"alpha": 255,
|
||||||
|
"metallic": 0.9,
|
||||||
|
"roughness": 0.1
|
||||||
|
}' \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials/305"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 305,
|
||||||
|
"name": "红色金属-更新版",
|
||||||
|
"diffuse_r": 200,
|
||||||
|
"diffuse_g": 100,
|
||||||
|
"diffuse_b": 50,
|
||||||
|
"alpha": 255,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 删除材质
|
||||||
|
|
||||||
|
**DELETE** `/materials/:id`
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int | 材质ID |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -X DELETE -H "X-API-Token: seatons3d" \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials/305"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 305
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: 删除材质会级联删除所有相关的绑定关系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 绑定管理接口
|
||||||
|
|
||||||
|
### 6. 绑定材质到多个Group
|
||||||
|
|
||||||
|
**POST** `/materials/:id/bindings`
|
||||||
|
|
||||||
|
将一个材质绑定到多个叶子节点(group_id),支持幂等操作(重复绑定不会报错)。
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int | 材质ID |
|
||||||
|
|
||||||
|
#### 请求Body
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| group_ids | string[] | 是 | 叶子节点ID数组 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "X-API-Token: seatons3d" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"group_ids": ["node_001", "node_002", "node_003"]
|
||||||
|
}' \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials/4/bindings"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"material_id": 4,
|
||||||
|
"group_ids": ["node_001", "node_002", "node_003"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 解绑材质与Group
|
||||||
|
|
||||||
|
**DELETE** `/materials/:id/bindings`
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int | 材质ID |
|
||||||
|
|
||||||
|
#### 请求Body
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| group_ids | string[] | 是 | 要解绑的叶子节点ID数组 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -X DELETE -H "X-API-Token: seatons3d" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"group_ids": ["node_001", "node_002"]
|
||||||
|
}' \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials/4/bindings"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"material_id": 4,
|
||||||
|
"unbound": ["node_001", "node_002"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 获取材质关联的所有Group(分页)
|
||||||
|
|
||||||
|
**GET** `/materials/:id/groups`
|
||||||
|
|
||||||
|
#### 路径参数
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int | 材质ID |
|
||||||
|
|
||||||
|
#### 请求参数(Query)
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码,默认1 |
|
||||||
|
| page_size | int | 否 | 每页数量,默认20,最大100 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -H "X-API-Token: seatons3d" \
|
||||||
|
"http://10.99.98.248:8081/api/v1/materials/4/groups?page=1&page_size=10"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
"20251021105717834754",
|
||||||
|
"20251021105717834758",
|
||||||
|
"20251021105717834762"
|
||||||
|
],
|
||||||
|
"total": 22,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 根据Group IDs批量查询关联材质
|
||||||
|
|
||||||
|
**POST** `/groups/materials`
|
||||||
|
|
||||||
|
根据一个或多个叶子节点ID,查询它们关联的材质详情。
|
||||||
|
|
||||||
|
#### 请求Body
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| group_ids | string[] | 是 | 叶子节点ID数组 |
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "X-API-Token: seatons3d" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"group_ids": ["202510211057245681447", "aDsIXMkjxdm2CDUj9gTvf"]
|
||||||
|
}' \
|
||||||
|
"http://10.99.98.248:8081/api/v1/groups/materials"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"group_id": "202510211057245681447",
|
||||||
|
"material": {
|
||||||
|
"id": 4,
|
||||||
|
"name": "dgnColor4",
|
||||||
|
"diffuse_r": 255,
|
||||||
|
"diffuse_g": 0,
|
||||||
|
"diffuse_b": 0,
|
||||||
|
"alpha": 255,
|
||||||
|
"shininess": 20,
|
||||||
|
"specular_r": 230,
|
||||||
|
"specular_g": 230,
|
||||||
|
"specular_b": 230,
|
||||||
|
"ambient_r": 50,
|
||||||
|
"ambient_g": 50,
|
||||||
|
"ambient_b": 50,
|
||||||
|
"metallic": 0,
|
||||||
|
"roughness": 0,
|
||||||
|
"reflectance": 0,
|
||||||
|
"created_at": "2025-12-09T09:53:33.209555Z",
|
||||||
|
"updated_at": "2025-12-09T09:53:33.209555Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_id": "aDsIXMkjxdm2CDUj9gTvf",
|
||||||
|
"material": {
|
||||||
|
"id": -1,
|
||||||
|
"name": "默认值",
|
||||||
|
"diffuse_r": 192,
|
||||||
|
"diffuse_g": 192,
|
||||||
|
"diffuse_b": 192,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**: 如果某个group_id没有绑定材质,则不会出现在返回结果中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 健康检查
|
||||||
|
|
||||||
|
**GET** `/health`
|
||||||
|
|
||||||
|
此接口无需认证。
|
||||||
|
|
||||||
|
#### 请求示例
|
||||||
|
```bash
|
||||||
|
curl http://10.99.98.248:8081/health
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据模型
|
||||||
|
|
||||||
|
### Material(材质)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int64 | 材质ID(主键) |
|
||||||
|
| name | string | 材质名称 |
|
||||||
|
| diffuse_r | float64 | 漫反射-红 (0-255) |
|
||||||
|
| diffuse_g | float64 | 漫反射-绿 (0-255) |
|
||||||
|
| diffuse_b | float64 | 漫反射-蓝 (0-255) |
|
||||||
|
| alpha | float64 | 透明度 (0-255) |
|
||||||
|
| shininess | float64 | 光泽度 |
|
||||||
|
| specular_r | float64 | 高光-红 (0-255) |
|
||||||
|
| specular_g | float64 | 高光-绿 (0-255) |
|
||||||
|
| specular_b | float64 | 高光-蓝 (0-255) |
|
||||||
|
| ambient_r | float64 | 环境光-红 (0-255) |
|
||||||
|
| ambient_g | float64 | 环境光-绿 (0-255) |
|
||||||
|
| ambient_b | float64 | 环境光-蓝 (0-255) |
|
||||||
|
| metallic | float64 | 金属度 |
|
||||||
|
| roughness | float64 | 粗糙度 |
|
||||||
|
| reflectance | float64 | 反射率 |
|
||||||
|
| created_at | timestamp | 创建时间 |
|
||||||
|
| updated_at | timestamp | 更新时间 |
|
||||||
|
|
||||||
|
### MaterialBinding(材质绑定)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | int64 | 绑定ID(主键) |
|
||||||
|
| material_id | int64 | 材质ID(外键) |
|
||||||
|
| group_id | string | 叶子节点ID |
|
||||||
|
| created_at | timestamp | 创建时间 |
|
||||||
257
docs/DATABASE.md
Normal file
257
docs/DATABASE.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# 数据库设计文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Material Texture 使用 PostgreSQL 数据库存储材质信息和绑定关系。
|
||||||
|
|
||||||
|
- **数据库**: PostgreSQL 15+
|
||||||
|
- **ORM**: GORM v1.31
|
||||||
|
- **特性**: 分区表、pg_trgm 模糊搜索
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ER 图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
materials ||--o{ material_bindings : "has"
|
||||||
|
|
||||||
|
materials {
|
||||||
|
bigint id PK "自增主键"
|
||||||
|
varchar name "材质名称"
|
||||||
|
float diffuse_r "漫反射-红"
|
||||||
|
float diffuse_g "漫反射-绿"
|
||||||
|
float diffuse_b "漫反射-蓝"
|
||||||
|
float alpha "透明度"
|
||||||
|
float shininess "光泽度"
|
||||||
|
float specular_r "高光-红"
|
||||||
|
float specular_g "高光-绿"
|
||||||
|
float specular_b "高光-蓝"
|
||||||
|
float ambient_r "环境光-红"
|
||||||
|
float ambient_g "环境光-绿"
|
||||||
|
float ambient_b "环境光-蓝"
|
||||||
|
float metallic "金属度(PBR)"
|
||||||
|
float roughness "粗糙度(PBR)"
|
||||||
|
float reflectance "反射率(PBR)"
|
||||||
|
bigint version "乐观锁版本"
|
||||||
|
timestamp created_at "创建时间"
|
||||||
|
timestamp updated_at "更新时间"
|
||||||
|
}
|
||||||
|
|
||||||
|
material_bindings {
|
||||||
|
bigint id PK "绑定ID"
|
||||||
|
bigint material_id FK "材质ID"
|
||||||
|
varchar group_id "叶子节点ID"
|
||||||
|
timestamp created_at "创建时间"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 表结构详情
|
||||||
|
|
||||||
|
### 1. materials 表(材质表)
|
||||||
|
|
||||||
|
存储 3D 材质的属性信息,包括传统渲染属性和 PBR 属性。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|
||||||
|
|------|------|------|--------|------|
|
||||||
|
| id | BIGSERIAL | PRIMARY KEY | 自增 | 材质唯一标识 |
|
||||||
|
| name | VARCHAR(255) | NOT NULL | - | 材质名称 |
|
||||||
|
| diffuse_r | FLOAT | NOT NULL | 0 | 漫反射颜色-红 (0-255) |
|
||||||
|
| diffuse_g | FLOAT | NOT NULL | 0 | 漫反射颜色-绿 (0-255) |
|
||||||
|
| diffuse_b | FLOAT | NOT NULL | 0 | 漫反射颜色-蓝 (0-255) |
|
||||||
|
| alpha | FLOAT | NOT NULL | 1 | 透明度 (0-255) |
|
||||||
|
| shininess | FLOAT | NOT NULL | 0 | 光泽度/高光强度 |
|
||||||
|
| specular_r | FLOAT | NOT NULL | 0 | 高光颜色-红 (0-255) |
|
||||||
|
| specular_g | FLOAT | NOT NULL | 0 | 高光颜色-绿 (0-255) |
|
||||||
|
| specular_b | FLOAT | NOT NULL | 0 | 高光颜色-蓝 (0-255) |
|
||||||
|
| ambient_r | FLOAT | NOT NULL | 0 | 环境光颜色-红 (0-255) |
|
||||||
|
| ambient_g | FLOAT | NOT NULL | 0 | 环境光颜色-绿 (0-255) |
|
||||||
|
| ambient_b | FLOAT | NOT NULL | 0 | 环境光颜色-蓝 (0-255) |
|
||||||
|
| metallic | FLOAT | NOT NULL | 0 | PBR 金属度 (0-1) |
|
||||||
|
| roughness | FLOAT | NOT NULL | 0.5 | PBR 粗糙度 (0-1) |
|
||||||
|
| reflectance | FLOAT | NOT NULL | 0.5 | PBR 反射率 (0-1) |
|
||||||
|
| version | BIGINT | NOT NULL | 0 | 乐观锁版本号 |
|
||||||
|
| created_at | TIMESTAMP | - | CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP | - | CURRENT_TIMESTAMP | 更新时间 |
|
||||||
|
|
||||||
|
#### 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 | 用途 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| materials_pkey | PRIMARY KEY | id | 主键 |
|
||||||
|
| idx_materials_name_trgm | GIN | name | 模糊搜索 (ILIKE '%keyword%') |
|
||||||
|
| idx_materials_created_at | BTREE | created_at DESC | 按创建时间排序 |
|
||||||
|
| idx_materials_updated_at | BTREE | updated_at DESC | 按更新时间排序 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. material_bindings 表(材质绑定表)
|
||||||
|
|
||||||
|
存储材质与叶子节点(group_id)的多对多绑定关系。
|
||||||
|
|
||||||
|
**重要**: 此表使用 HASH 分区,按 `material_id` 分为 16 个分区,支持亿级数据规模。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 约束 | 默认值 | 说明 |
|
||||||
|
|------|------|------|--------|------|
|
||||||
|
| id | BIGSERIAL | 复合主键 | 自增 | 绑定记录ID |
|
||||||
|
| material_id | BIGINT | NOT NULL, FK | - | 关联的材质ID |
|
||||||
|
| group_id | VARCHAR(255) | NOT NULL | - | 叶子节点ID |
|
||||||
|
| created_at | TIMESTAMP | - | CURRENT_TIMESTAMP | 创建时间 |
|
||||||
|
|
||||||
|
#### 约束
|
||||||
|
|
||||||
|
- **主键**: `(material_id, id)` - 复合主键,支持分区
|
||||||
|
- **外键**: `material_id` → `materials(id)` ON DELETE CASCADE
|
||||||
|
- **唯一约束**: `(material_id, group_id)` - 防止重复绑定
|
||||||
|
|
||||||
|
#### 分区策略
|
||||||
|
|
||||||
|
```
|
||||||
|
material_bindings (父表)
|
||||||
|
├── material_bindings_p0 (material_id % 16 = 0)
|
||||||
|
├── material_bindings_p1 (material_id % 16 = 1)
|
||||||
|
├── material_bindings_p2 (material_id % 16 = 2)
|
||||||
|
│ ...
|
||||||
|
└── material_bindings_p15 (material_id % 16 = 15)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 每个分区的索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 字段 | 用途 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| idx_bindings_pX_unique | UNIQUE | (material_id, group_id) | 防止重复绑定 |
|
||||||
|
| idx_bindings_pX_group | BTREE | group_id | 按 group_id 查询 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 迁移文件
|
||||||
|
|
||||||
|
| 文件 | 版本 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 001_init.sql | v1.0 | 初始化基础表结构 |
|
||||||
|
| 002_partition_bindings.sql | v1.1 | 将 bindings 表转为 16 分区 |
|
||||||
|
| 003_add_indexes.sql | v1.2 | 添加 pg_trgm 和时间索引 |
|
||||||
|
| 004_add_version_column.sql | v1.3 | 添加乐观锁版本字段 |
|
||||||
|
|
||||||
|
### 执行迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入数据库容器
|
||||||
|
docker compose exec db psql -U postgres -d material_db
|
||||||
|
|
||||||
|
# 或直接执行
|
||||||
|
psql -h localhost -p 5433 -U postgres -d material_db -f migrations/001_init.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能优化说明
|
||||||
|
|
||||||
|
### 1. 分区表
|
||||||
|
|
||||||
|
- **目的**: 支持亿级 binding 记录
|
||||||
|
- **策略**: HASH 分区,按 material_id 分 16 片
|
||||||
|
- **优势**:
|
||||||
|
- 单分区最大约 600-1000 万行
|
||||||
|
- 查询时自动路由到相关分区
|
||||||
|
- 可在线添加新分区
|
||||||
|
|
||||||
|
### 2. pg_trgm 扩展
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **目的**: 支持模糊搜索 `ILIKE '%keyword%'`
|
||||||
|
- **原理**: 将字符串拆分为三元组 (trigram),建立 GIN 倒排索引
|
||||||
|
|
||||||
|
### 3. 连接池配置
|
||||||
|
|
||||||
|
| 参数 | 值 | 说明 |
|
||||||
|
|------|-----|------|
|
||||||
|
| MaxIdleConns | 50 | 最大空闲连接数 |
|
||||||
|
| MaxOpenConns | 200 | 最大连接数 |
|
||||||
|
| ConnMaxLifetime | 5min | 连接最大生命周期 |
|
||||||
|
| ConnMaxIdleTime | 2min | 空闲连接最大生命周期 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据示例
|
||||||
|
|
||||||
|
### Material 示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "红色金属",
|
||||||
|
"diffuse_r": 255,
|
||||||
|
"diffuse_g": 0,
|
||||||
|
"diffuse_b": 0,
|
||||||
|
"alpha": 255,
|
||||||
|
"shininess": 80,
|
||||||
|
"specular_r": 255,
|
||||||
|
"specular_g": 200,
|
||||||
|
"specular_b": 200,
|
||||||
|
"ambient_r": 50,
|
||||||
|
"ambient_g": 0,
|
||||||
|
"ambient_b": 0,
|
||||||
|
"metallic": 0.9,
|
||||||
|
"roughness": 0.2,
|
||||||
|
"reflectance": 0.8,
|
||||||
|
"version": 0,
|
||||||
|
"created_at": "2025-12-09T09:53:33Z",
|
||||||
|
"updated_at": "2025-12-09T09:53:33Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binding 示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"material_id": 4,
|
||||||
|
"group_id": "20251021105716587535",
|
||||||
|
"created_at": "2025-12-09T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常用查询
|
||||||
|
|
||||||
|
### 1. 按名称模糊搜索材质
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM materials
|
||||||
|
WHERE name ILIKE '%金属%'
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取材质关联的所有 group_id
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT group_id FROM material_bindings
|
||||||
|
WHERE material_id = 4
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 20 OFFSET 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 根据 group_id 批量查询材质
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT mb.group_id, m.*
|
||||||
|
FROM material_bindings mb
|
||||||
|
JOIN materials m ON mb.material_id = m.id
|
||||||
|
WHERE mb.group_id IN ('group1', 'group2', 'group3');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 统计各分区数据量
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT tableoid::regclass AS partition, COUNT(*)
|
||||||
|
FROM material_bindings
|
||||||
|
GROUP BY tableoid;
|
||||||
|
```
|
||||||
242
docs/Material_API.postman_collection.json
Normal file
242
docs/Material_API.postman_collection.json
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "material-texture-api",
|
||||||
|
"name": "材质管理系统 API",
|
||||||
|
"description": "材质管理后端服务API集合",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"type": "apikey",
|
||||||
|
"apikey": [
|
||||||
|
{
|
||||||
|
"key": "key",
|
||||||
|
"value": "X-API-Token",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
"value": "{{api_token}}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "in",
|
||||||
|
"value": "header",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "base_url",
|
||||||
|
"value": "http://localhost:8081",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "api_token",
|
||||||
|
"value": "seatons3d",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "健康检查",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Health Check",
|
||||||
|
"request": {
|
||||||
|
"auth": {
|
||||||
|
"type": "noauth"
|
||||||
|
},
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/health",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["health"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "材质管理",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "1. 获取材质列表",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials?page=1&page_size=20&name=",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials"],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "page",
|
||||||
|
"value": "1",
|
||||||
|
"description": "页码"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "page_size",
|
||||||
|
"value": "20",
|
||||||
|
"description": "每页数量"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "name",
|
||||||
|
"value": "",
|
||||||
|
"description": "按名称搜索"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "2. 添加材质",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"新材质\",\n \"diffuse_r\": 255,\n \"diffuse_g\": 128,\n \"diffuse_b\": 64,\n \"alpha\": 255,\n \"shininess\": 20,\n \"specular_r\": 230,\n \"specular_g\": 230,\n \"specular_b\": 230,\n \"ambient_r\": 50,\n \"ambient_g\": 50,\n \"ambient_b\": 50,\n \"metallic\": 0.5,\n \"roughness\": 0.3,\n \"reflectance\": 0.5\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "3. 获取材质详情",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials/4",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials", "4"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "4. 编辑材质",
|
||||||
|
"request": {
|
||||||
|
"method": "PUT",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"name\": \"更新后的材质\",\n \"diffuse_r\": 200,\n \"diffuse_g\": 100,\n \"diffuse_b\": 50,\n \"alpha\": 255,\n \"shininess\": 30,\n \"specular_r\": 200,\n \"specular_g\": 200,\n \"specular_b\": 200,\n \"ambient_r\": 60,\n \"ambient_g\": 60,\n \"ambient_b\": 60,\n \"metallic\": 0.8,\n \"roughness\": 0.2,\n \"reflectance\": 0.6\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials/4",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials", "4"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "5. 删除材质",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials/999",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials", "999"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "绑定管理",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "6. 绑定材质到Groups",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"group_ids\": [\"node_001\", \"node_002\", \"node_003\"]\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials/4/bindings",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials", "4", "bindings"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "7. 解绑材质与Groups",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"group_ids\": [\"node_001\", \"node_002\"]\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials/4/bindings",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials", "4", "bindings"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "8. 获取材质关联的Groups",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/materials/4/groups",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "materials", "4", "groups"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "9. 根据Groups批量查询材质",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"group_ids\": [\"202510211057245681447\", \"aDsIXMkjxdm2CDUj9gTvf\"]\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{base_url}}/api/v1/groups/materials",
|
||||||
|
"host": ["{{base_url}}"],
|
||||||
|
"path": ["api", "v1", "groups", "materials"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
757537
export_tables/asset.csv
Normal file
757537
export_tables/asset.csv
Normal file
File diff suppressed because it is too large
Load Diff
304
export_tables/color.csv
Normal file
304
export_tables/color.csv
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
ColorId,Name,DiffuseRed,DiffuseGreen,DiffuseBlue,Alpha,Shininess,SpecularRed,SpecularGreen,SpecularBlue,AmbientRed,AmbientGreen,AmbientBlue,Metallic,Roughness,Reflectance
|
||||||
|
-1,默认值,192,192,192,255,20,230,230,230,50,50,50,3,6,0,
|
||||||
|
-9,红色,255,0,0,255,20,230,230,230,50,50,50,3,6,0,
|
||||||
|
-8,定位颜色,254,254,101,100,20,230,230,230,50,50,50,3,6,0,
|
||||||
|
0,dgnColor0,0,0,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
1,dgnColor1,204,204,204,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
2,dgnColor2,105,150,255,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
3,dgnColor3,0,204,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
4,dgnColor4,255,0,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
5,dgnColor5,255,255,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
6,dgnColor6,255,0,255,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
7,dgnColor7,255,155,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
8,dgnColor8,138,128,128,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
9,dgnColor9,196,196,196,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
10,dgnColor10,104,146,244,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
11,dgnColor11,6,196,6,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
12,dgnColor12,244,6,6,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
13,dgnColor13,244,244,6,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
14,dgnColor14,244,6,244,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
15,dgnColor15,244,150,6,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
16,dgnColor16,129,119,119,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
17,dgnColor17,188,188,188,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
18,dgnColor18,102,141,232,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
19,dgnColor19,11,188,11,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
20,dgnColor20,232,11,11,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
21,dgnColor21,232,232,11,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
22,dgnColor22,232,11,232,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
23,dgnColor23,232,146,11,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
24,dgnColor24,119,111,111,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
25,dgnColor25,180,180,180,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
26,dgnColor26,101,137,221,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
27,dgnColor27,17,180,17,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
28,dgnColor28,221,17,17,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
29,dgnColor29,221,221,17,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
30,dgnColor30,221,17,221,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
31,dgnColor31,221,141,17,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
32,dgnColor32,110,102,102,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
33,dgnColor33,172,172,172,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
34,dgnColor34,100,133,210,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
35,dgnColor35,23,172,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
36,dgnColor36,210,23,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
37,dgnColor37,210,210,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
38,dgnColor38,210,23,210,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
39,dgnColor39,210,136,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
40,dgnColor40,101,94,94,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
41,dgnColor41,164,164,164,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
42,dgnColor42,98,128,198,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
43,dgnColor43,28,164,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
44,dgnColor44,198,28,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
45,dgnColor45,198,198,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
46,dgnColor46,198,28,198,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
47,dgnColor47,198,132,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
48,dgnColor48,92,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
49,dgnColor49,156,156,156,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
50,dgnColor50,97,124,187,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
51,dgnColor51,34,156,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
52,dgnColor52,187,34,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
53,dgnColor53,187,187,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
54,dgnColor54,187,34,187,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
55,dgnColor55,187,127,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
56,dgnColor56,83,77,77,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
57,dgnColor57,148,148,148,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
58,dgnColor58,96,120,176,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
59,dgnColor59,40,148,40,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
60,dgnColor60,176,40,40,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
61,dgnColor61,176,176,40,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
62,dgnColor62,176,40,176,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
63,dgnColor63,176,122,40,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
64,dgnColor64,73,68,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
65,dgnColor65,141,141,141,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
66,dgnColor66,94,115,164,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
67,dgnColor67,45,141,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
68,dgnColor68,164,45,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
69,dgnColor69,164,164,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
70,dgnColor70,164,45,164,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
71,dgnColor71,164,118,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
72,dgnColor72,64,60,60,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
73,dgnColor73,133,133,133,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
74,dgnColor74,93,111,153,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
75,dgnColor75,51,133,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
76,dgnColor76,153,51,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
77,dgnColor77,153,153,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
78,dgnColor78,153,51,153,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
79,dgnColor79,153,113,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
80,dgnColor80,55,51,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
81,dgnColor81,125,125,125,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
82,dgnColor82,92,107,142,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
83,dgnColor83,57,125,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
84,dgnColor84,142,57,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
85,dgnColor85,142,142,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
86,dgnColor86,142,57,142,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
87,dgnColor87,142,108,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
88,dgnColor88,46,43,43,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
89,dgnColor89,117,117,117,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
90,dgnColor90,90,102,130,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
91,dgnColor91,62,117,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
92,dgnColor92,130,62,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
93,dgnColor93,130,130,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
94,dgnColor94,130,62,130,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
95,dgnColor95,130,104,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
96,dgnColor96,37,34,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
97,dgnColor97,109,109,109,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
98,dgnColor98,89,98,119,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
99,dgnColor99,68,109,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
100,dgnColor100,119,68,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
101,dgnColor101,119,119,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
102,dgnColor102,119,68,119,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
103,dgnColor103,119,99,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
104,dgnColor104,28,26,26,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
105,dgnColor105,101,101,101,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
106,dgnColor106,88,94,108,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
107,dgnColor107,74,101,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
108,dgnColor108,108,74,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
109,dgnColor109,108,108,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
110,dgnColor110,108,74,108,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
111,dgnColor111,108,94,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
112,dgnColor112,18,17,17,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
113,dgnColor113,93,93,93,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
114,dgnColor114,86,89,96,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
115,dgnColor115,79,93,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
116,dgnColor116,96,79,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
117,dgnColor117,96,96,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
118,dgnColor118,96,79,96,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
119,dgnColor119,96,90,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
120,dgnColor120,9,9,9,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
121,dgnColor121,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
122,dgnColor122,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
123,dgnColor123,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
124,dgnColor124,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
125,dgnColor125,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
126,dgnColor126,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
127,dgnColor127,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
128,dgnColor128,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
129,dgnColor129,141,141,141,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
130,dgnColor130,94,115,164,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
131,dgnColor131,45,141,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
132,dgnColor132,164,45,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
133,dgnColor133,164,164,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
134,dgnColor134,164,45,164,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
135,dgnColor135,164,118,45,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
136,dgnColor136,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
137,dgnColor137,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
138,dgnColor138,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
139,dgnColor139,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
140,dgnColor140,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
141,dgnColor141,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
142,dgnColor142,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
143,dgnColor143,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
144,dgnColor144,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
145,dgnColor145,133,133,133,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
146,dgnColor146,93,111,153,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
147,dgnColor147,51,133,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
148,dgnColor148,153,51,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
149,dgnColor149,153,153,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
150,dgnColor150,153,51,153,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
151,dgnColor151,153,113,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
152,dgnColor152,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
153,dgnColor153,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
154,dgnColor154,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
155,dgnColor155,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
156,dgnColor156,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
157,dgnColor157,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
158,dgnColor158,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
159,dgnColor159,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
160,dgnColor160,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
161,dgnColor161,125,125,125,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
162,dgnColor162,92,107,142,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
163,dgnColor163,57,125,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
164,dgnColor164,142,57,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
165,dgnColor165,142,142,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
166,dgnColor166,142,57,142,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
167,dgnColor167,142,108,57,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
168,dgnColor168,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
169,dgnColor169,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
170,dgnColor170,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
171,dgnColor171,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
172,dgnColor172,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
173,dgnColor173,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
174,dgnColor174,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
175,dgnColor175,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
176,dgnColor176,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
177,dgnColor177,117,117,117,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
178,dgnColor178,90,102,130,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
179,dgnColor179,62,117,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
180,dgnColor180,130,62,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
181,dgnColor181,130,130,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
182,dgnColor182,130,62,130,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
183,dgnColor183,130,104,62,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
184,dgnColor184,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
185,dgnColor185,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
186,dgnColor186,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
187,dgnColor187,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
188,dgnColor188,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
189,dgnColor189,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
190,dgnColor190,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
191,dgnColor191,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
192,dgnColor192,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
193,dgnColor193,109,109,109,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
194,dgnColor194,89,98,119,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
195,dgnColor195,68,109,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
196,dgnColor196,119,68,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
197,dgnColor197,119,119,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
198,dgnColor198,119,68,119,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
199,dgnColor199,119,99,68,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
200,dgnColor200,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
201,dgnColor201,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
202,dgnColor202,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
203,dgnColor203,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
204,dgnColor204,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
205,dgnColor205,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
206,dgnColor206,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
207,dgnColor207,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
208,dgnColor208,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
209,dgnColor209,101,101,101,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
210,dgnColor210,88,94,108,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
211,dgnColor211,74,101,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
212,dgnColor212,108,74,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
213,dgnColor213,108,108,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
214,dgnColor214,108,74,108,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
215,dgnColor215,108,94,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
216,dgnColor216,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
217,dgnColor217,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
218,dgnColor218,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
219,dgnColor219,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
220,dgnColor220,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
221,dgnColor221,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
222,dgnColor222,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
223,dgnColor223,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
224,dgnColor224,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
225,dgnColor225,93,93,93,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
226,dgnColor226,86,89,96,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
227,dgnColor227,79,93,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
228,dgnColor228,96,79,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
229,dgnColor229,96,96,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
230,dgnColor230,96,79,96,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
231,dgnColor231,96,90,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
232,dgnColor232,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
233,dgnColor233,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
234,dgnColor234,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
235,dgnColor235,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
236,dgnColor236,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
237,dgnColor237,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
238,dgnColor238,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
239,dgnColor239,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
240,dgnColor240,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
241,dgnColor241,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
242,dgnColor242,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
243,dgnColor243,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
244,dgnColor244,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
245,dgnColor245,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
246,dgnColor246,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
247,dgnColor247,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
248,dgnColor248,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
249,dgnColor249,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
250,dgnColor250,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
251,dgnColor251,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
252,dgnColor252,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
253,dgnColor253,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
254,dgnColor254,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
255,dgnColor255,255,255,255,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
256,银白色,220,220,220,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
257,艳绿色,0,200,70,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
258,粉红色,250,128,128,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
259,浅蓝色,157,215,255,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
260,橙黄色,255,120,30,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
261,银灰色,190,205,210,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
265,dgnColor0,0,0,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
266,dgnColor1,204,204,204,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
267,dgnColor2,105,150,255,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
268,dgnColor3,0,204,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
269,dgnColor4,255,0,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
270,dgnColor5,255,255,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
271,dgnColor7,255,155,0,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
272,dgnColor14,244,6,244,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
273,dgnColor16,129,119,119,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
274,dgnColor17,188,188,188,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
275,dgnColor18,102,141,232,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
276,dgnColor21,232,232,11,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
277,dgnColor24,119,111,111,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
278,dgnColor32,110,102,102,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
279,dgnColor34,100,133,210,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
280,dgnColor35,23,172,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
281,dgnColor36,210,23,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
282,dgnColor37,210,210,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
283,dgnColor39,210,136,23,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
284,dgnColor40,101,94,94,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
285,dgnColor41,164,164,164,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
286,dgnColor42,98,128,198,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
287,dgnColor43,28,164,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
288,dgnColor44,198,28,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
289,dgnColor45,198,198,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
290,dgnColor46,198,28,198,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
291,dgnColor47,198,132,28,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
292,dgnColor49,156,156,156,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
293,dgnColor50,97,124,187,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
294,dgnColor51,34,156,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
295,dgnColor54,187,34,187,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
296,dgnColor55,187,127,34,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
297,dgnColor108,108,74,74,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
298,dgnColor115,79,93,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
299,dgnColor119,96,90,79,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
300,dgnColor148,153,51,51,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
301,dgnColor249,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
302,dgnColor252,85,85,85,255,20,230,230,230,50,50,50,0,0,0,
|
||||||
|
757537
export_tables/map.csv
Normal file
757537
export_tables/map.csv
Normal file
File diff suppressed because it is too large
Load Diff
49
go.mod
Normal file
49
go.mod
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
module material_texture
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
|
golang.org/x/net v0.42.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.27.0 // indirect
|
||||||
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
|
)
|
||||||
105
go.sum
Normal file
105
go.sum
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||||
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||||
|
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||||
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
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
|
||||||
|
}
|
||||||
37
migrations/001_init.sql
Normal file
37
migrations/001_init.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- 材质管理系统初始化脚本
|
||||||
|
-- 此脚本由GORM AutoMigrate自动执行,仅作参考
|
||||||
|
|
||||||
|
-- 材质表
|
||||||
|
CREATE TABLE IF NOT EXISTS materials (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
diffuse_r FLOAT NOT NULL DEFAULT 0,
|
||||||
|
diffuse_g FLOAT NOT NULL DEFAULT 0,
|
||||||
|
diffuse_b FLOAT NOT NULL DEFAULT 0,
|
||||||
|
alpha FLOAT NOT NULL DEFAULT 1,
|
||||||
|
shininess FLOAT NOT NULL DEFAULT 0,
|
||||||
|
specular_r FLOAT NOT NULL DEFAULT 0,
|
||||||
|
specular_g FLOAT NOT NULL DEFAULT 0,
|
||||||
|
specular_b FLOAT NOT NULL DEFAULT 0,
|
||||||
|
ambient_r FLOAT NOT NULL DEFAULT 0,
|
||||||
|
ambient_g FLOAT NOT NULL DEFAULT 0,
|
||||||
|
ambient_b FLOAT NOT NULL DEFAULT 0,
|
||||||
|
metallic FLOAT NOT NULL DEFAULT 0,
|
||||||
|
roughness FLOAT NOT NULL DEFAULT 0.5,
|
||||||
|
reflectance FLOAT NOT NULL DEFAULT 0.5,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 材质绑定表
|
||||||
|
CREATE TABLE IF NOT EXISTS material_bindings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
material_id BIGINT NOT NULL REFERENCES materials(id) ON DELETE CASCADE,
|
||||||
|
group_id VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bindings_material_id ON material_bindings(material_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bindings_group_id ON material_bindings(group_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_material_group ON material_bindings(material_id, group_id);
|
||||||
113
migrations/002_partition_bindings.sql
Normal file
113
migrations/002_partition_bindings.sql
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Migration: 002_partition_bindings.sql
|
||||||
|
-- Purpose: 将 material_bindings 表改为 HASH 分区表
|
||||||
|
-- 支持亿级数据规模
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 注意: 此迁移需要在维护窗口执行,会有短暂锁表
|
||||||
|
-- 预估执行时间: 取决于现有数据量
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- 1. 创建新的分区表
|
||||||
|
CREATE TABLE material_bindings_new (
|
||||||
|
id BIGSERIAL,
|
||||||
|
material_id BIGINT NOT NULL,
|
||||||
|
group_id VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (material_id, id)
|
||||||
|
) PARTITION BY HASH (material_id);
|
||||||
|
|
||||||
|
-- 2. 创建 16 个分区 (每个分区预计容纳 600-1000 万行)
|
||||||
|
CREATE TABLE material_bindings_p0 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 0);
|
||||||
|
CREATE TABLE material_bindings_p1 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 1);
|
||||||
|
CREATE TABLE material_bindings_p2 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 2);
|
||||||
|
CREATE TABLE material_bindings_p3 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 3);
|
||||||
|
CREATE TABLE material_bindings_p4 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 4);
|
||||||
|
CREATE TABLE material_bindings_p5 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 5);
|
||||||
|
CREATE TABLE material_bindings_p6 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 6);
|
||||||
|
CREATE TABLE material_bindings_p7 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 7);
|
||||||
|
CREATE TABLE material_bindings_p8 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 8);
|
||||||
|
CREATE TABLE material_bindings_p9 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 9);
|
||||||
|
CREATE TABLE material_bindings_p10 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 10);
|
||||||
|
CREATE TABLE material_bindings_p11 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 11);
|
||||||
|
CREATE TABLE material_bindings_p12 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 12);
|
||||||
|
CREATE TABLE material_bindings_p13 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 13);
|
||||||
|
CREATE TABLE material_bindings_p14 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 14);
|
||||||
|
CREATE TABLE material_bindings_p15 PARTITION OF material_bindings_new
|
||||||
|
FOR VALUES WITH (MODULUS 16, REMAINDER 15);
|
||||||
|
|
||||||
|
-- 3. 为每个分区创建 group_id 索引 (用于按 group 查询)
|
||||||
|
CREATE INDEX idx_bindings_p0_group ON material_bindings_p0(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p1_group ON material_bindings_p1(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p2_group ON material_bindings_p2(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p3_group ON material_bindings_p3(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p4_group ON material_bindings_p4(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p5_group ON material_bindings_p5(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p6_group ON material_bindings_p6(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p7_group ON material_bindings_p7(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p8_group ON material_bindings_p8(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p9_group ON material_bindings_p9(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p10_group ON material_bindings_p10(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p11_group ON material_bindings_p11(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p12_group ON material_bindings_p12(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p13_group ON material_bindings_p13(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p14_group ON material_bindings_p14(group_id);
|
||||||
|
CREATE INDEX idx_bindings_p15_group ON material_bindings_p15(group_id);
|
||||||
|
|
||||||
|
-- 4. 为每个分区创建唯一约束 (防止重复绑定)
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p0_unique ON material_bindings_p0(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p1_unique ON material_bindings_p1(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p2_unique ON material_bindings_p2(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p3_unique ON material_bindings_p3(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p4_unique ON material_bindings_p4(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p5_unique ON material_bindings_p5(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p6_unique ON material_bindings_p6(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p7_unique ON material_bindings_p7(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p8_unique ON material_bindings_p8(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p9_unique ON material_bindings_p9(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p10_unique ON material_bindings_p10(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p11_unique ON material_bindings_p11(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p12_unique ON material_bindings_p12(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p13_unique ON material_bindings_p13(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p14_unique ON material_bindings_p14(material_id, group_id);
|
||||||
|
CREATE UNIQUE INDEX idx_bindings_p15_unique ON material_bindings_p15(material_id, group_id);
|
||||||
|
|
||||||
|
-- 5. 迁移现有数据 (如果表已存在)
|
||||||
|
-- 注意: 如果数据量大,建议分批迁移
|
||||||
|
INSERT INTO material_bindings_new (id, material_id, group_id, created_at)
|
||||||
|
SELECT id, material_id, group_id, created_at
|
||||||
|
FROM material_bindings
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- 6. 切换表名
|
||||||
|
ALTER TABLE material_bindings RENAME TO material_bindings_old;
|
||||||
|
ALTER TABLE material_bindings_new RENAME TO material_bindings;
|
||||||
|
|
||||||
|
-- 7. 重置序列 (确保新插入的 ID 不冲突)
|
||||||
|
SELECT setval('material_bindings_new_id_seq', COALESCE((SELECT MAX(id) FROM material_bindings), 0) + 1, false);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 回滚脚本 (如需回滚,手动执行)
|
||||||
|
-- ============================================
|
||||||
|
-- BEGIN;
|
||||||
|
-- DROP TABLE IF EXISTS material_bindings CASCADE;
|
||||||
|
-- ALTER TABLE material_bindings_old RENAME TO material_bindings;
|
||||||
|
-- COMMIT;
|
||||||
26
migrations/003_add_indexes.sql
Normal file
26
migrations/003_add_indexes.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Migration: 003_add_indexes.sql
|
||||||
|
-- Purpose: 添加性能优化索引
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 1. 启用 pg_trgm 扩展 (支持模糊搜索索引)
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
|
||||||
|
-- 2. 材质名称模糊搜索索引 (GIN 索引,支持 ILIKE '%keyword%')
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_name_trgm
|
||||||
|
ON materials USING gin (name gin_trgm_ops);
|
||||||
|
|
||||||
|
-- 3. 材质创建时间索引 (用于排序)
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_created_at
|
||||||
|
ON materials(created_at DESC);
|
||||||
|
|
||||||
|
-- 4. 材质更新时间索引
|
||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_materials_updated_at
|
||||||
|
ON materials(updated_at DESC);
|
||||||
|
|
||||||
|
-- 注意: 如果使用了分区表 (002_partition_bindings.sql),
|
||||||
|
-- 以下索引已在分区迁移中创建,可跳过
|
||||||
|
|
||||||
|
-- 5. binding 表 group_id + material_id 复合索引 (优化批量查询)
|
||||||
|
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_bindings_group_material
|
||||||
|
-- ON material_bindings(group_id, material_id);
|
||||||
11
migrations/004_add_version_column.sql
Normal file
11
migrations/004_add_version_column.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- Migration: 004_add_version_column.sql
|
||||||
|
-- Purpose: 添加乐观锁版本字段
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 添加 version 字段到 materials 表 (用于乐观锁)
|
||||||
|
ALTER TABLE materials
|
||||||
|
ADD COLUMN IF NOT EXISTS version BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- 注释
|
||||||
|
COMMENT ON COLUMN materials.version IS '乐观锁版本号,用于防止并发更新覆盖';
|
||||||
72
pkg/response/response.go
Normal file
72
pkg/response/response.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PagedData struct {
|
||||||
|
Items interface{} `json:"items"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Success(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(http.StatusOK, Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuccessPaged(c *gin.Context, items interface{}, total int64, page, pageSize int) {
|
||||||
|
c.JSON(http.StatusOK, Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: PagedData{
|
||||||
|
Items: items,
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Created(c *gin.Context, data interface{}) {
|
||||||
|
c.JSON(http.StatusCreated, Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "created",
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(c *gin.Context, statusCode int, message string) {
|
||||||
|
c.JSON(statusCode, Response{
|
||||||
|
Code: statusCode,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BadRequest(c *gin.Context, message string) {
|
||||||
|
Error(c, http.StatusBadRequest, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFound(c *gin.Context, message string) {
|
||||||
|
Error(c, http.StatusNotFound, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unauthorized(c *gin.Context, message string) {
|
||||||
|
Error(c, http.StatusUnauthorized, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InternalError(c *gin.Context, message string) {
|
||||||
|
Error(c, http.StatusInternalServerError, message)
|
||||||
|
}
|
||||||
91
test_api.sh
Executable file
91
test_api.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# API测试脚本
|
||||||
|
BASE_URL="http://localhost:8081"
|
||||||
|
TOKEN="seatons3d"
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "材质管理API接口测试"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "1. GET /api/v1/materials - 材质列表"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -H "X-API-Token: $TOKEN" "$BASE_URL/api/v1/materials?page=1&page_size=3" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. POST /api/v1/materials - 添加材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
NEW_MATERIAL=$(curl -s -X POST -H "X-API-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"测试材质API","diffuse_r":100,"diffuse_g":150,"diffuse_b":200,"alpha":255,"metallic":0.5,"roughness":0.3}' \
|
||||||
|
"$BASE_URL/api/v1/materials")
|
||||||
|
echo "$NEW_MATERIAL" | jq .
|
||||||
|
NEW_ID=$(echo "$NEW_MATERIAL" | jq -r '.data.id')
|
||||||
|
echo "新建材质ID: $NEW_ID"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. GET /api/v1/materials/:id - 获取材质详情"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -H "X-API-Token: $TOKEN" "$BASE_URL/api/v1/materials/$NEW_ID" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "4. PUT /api/v1/materials/:id - 编辑材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -X PUT -H "X-API-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"测试材质API-已更新","diffuse_r":200,"diffuse_g":100,"diffuse_b":50,"alpha":255,"metallic":0.8,"roughness":0.2}' \
|
||||||
|
"$BASE_URL/api/v1/materials/$NEW_ID" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "5. DELETE /api/v1/materials/:id - 删除材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -X DELETE -H "X-API-Token: $TOKEN" "$BASE_URL/api/v1/materials/$NEW_ID" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "绑定管理API接口测试"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "6. POST /api/v1/materials/:id/bindings - 绑定材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -X POST -H "X-API-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"group_ids": ["test_group_001", "test_group_002", "test_group_003"]}' \
|
||||||
|
"$BASE_URL/api/v1/materials/4/bindings" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "7. GET /api/v1/materials/:id/groups - 获取材质关联的groups"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -H "X-API-Token: $TOKEN" "$BASE_URL/api/v1/materials/4/groups" | jq '{material_id: .data.material_id, group_count: (.data.group_ids | length), sample_groups: (.data.group_ids[:5])}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "8. POST /api/v1/groups/materials - 根据group_ids查询材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -X POST -H "X-API-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"group_ids": ["test_group_001", "202510211057245681447"]}' \
|
||||||
|
"$BASE_URL/api/v1/groups/materials" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "9. DELETE /api/v1/materials/:id/bindings - 解绑材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -X DELETE -H "X-API-Token: $TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d '{"group_ids": ["test_group_001", "test_group_002", "test_group_003"]}' \
|
||||||
|
"$BASE_URL/api/v1/materials/4/bindings" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "错误处理测试"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "10. 无Token访问"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s "$BASE_URL/api/v1/materials" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "11. 访问不存在的材质"
|
||||||
|
echo "------------------------------------------"
|
||||||
|
curl -s -H "X-API-Token: $TOKEN" "$BASE_URL/api/v1/materials/99999999" | jq .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "测试完成!"
|
||||||
|
echo "=========================================="
|
||||||
Reference in New Issue
Block a user