init: iloom WMS monorepo with K8s deployment manifests
Go workspace (go.work) with 5 microservices + shared library: - gateway (8080), auth-service (8081), purchaser-service (8082) - textile-service (8083), washing-service (8084) - shared: proto definitions, common packages Infrastructure: docker-compose for local dev, K8s manifests for K3s cluster deployment (mysql/redis/etcd + traefik ingress). Frontend (iloom-flatten) added as git submodule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
9c39c8cbd7
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.claude
|
||||||
|
*.md
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
iloom-flatten/node_modules
|
||||||
|
iloom-flatten/dist
|
||||||
|
iloom-flatten/.cache
|
||||||
|
**/*_test.go
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.test.tsx
|
||||||
28
.env.example
Normal file
28
.env.example
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# iloom 微服务环境变量配置
|
||||||
|
|
||||||
|
# MySQL (共享 muyu_wms 数据库)
|
||||||
|
DB_DSN=root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
|
||||||
|
|
||||||
|
# JWT (复用 muyu 认证)
|
||||||
|
JWT_SECRET=muyu-wms-jwt-secret-key-2026
|
||||||
|
JWT_REFRESH_SECRET=muyu-wms-jwt-refresh-2026
|
||||||
|
|
||||||
|
# muyu gRPC 地址
|
||||||
|
MUYU_SYSTEM_RPC_ADDR=system-rpc:9001
|
||||||
|
MUYU_INVENTORY_RPC_ADDR=inventory-rpc:9002
|
||||||
|
|
||||||
|
# 服务间调用密钥
|
||||||
|
INTERNAL_SERVICE_KEY=internal-service-shared-secret
|
||||||
|
|
||||||
|
# 服务端口
|
||||||
|
GATEWAY_PORT=8080
|
||||||
|
AUTH_SERVICE_PORT=8081
|
||||||
|
PURCHASER_SERVICE_PORT=8082
|
||||||
|
TEXTILE_SERVICE_PORT=8083
|
||||||
|
WASHING_SERVICE_PORT=8084
|
||||||
|
|
||||||
|
# 前端
|
||||||
|
FRONTEND_URL=http://localhost:3015
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
ALLOW_ORIGINS=http://localhost:3015,http://localhost:3000
|
||||||
79
.gitignore
vendored
Normal file
79
.gitignore
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# ========================
|
||||||
|
# Go
|
||||||
|
# ========================
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
*.prof
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
bin/
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Node / Frontend
|
||||||
|
# ========================
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
coverage/
|
||||||
|
*.tsbuildinfo
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Frontend build artifacts
|
||||||
|
# ========================
|
||||||
|
.microcompact/
|
||||||
|
.meoo/
|
||||||
|
outputs/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Environment & Secrets
|
||||||
|
# ========================
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
!.env.example
|
||||||
|
!.env.production
|
||||||
|
|
||||||
|
# K8s secrets (real values)
|
||||||
|
k8s/01-secrets.yaml
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# IDE & Editor
|
||||||
|
# ========================
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# OS
|
||||||
|
# ========================
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Docker
|
||||||
|
# ========================
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# ========================
|
||||||
|
# Planning / task tracking
|
||||||
|
# ========================
|
||||||
|
.todo/
|
||||||
|
.plan/
|
||||||
|
.claude/
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "iloom-flatten"]
|
||||||
|
path = iloom-flatten
|
||||||
|
url = https://github.com/kihaneseifu/iloom-flatten.git
|
||||||
74
Makefile
Normal file
74
Makefile
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
.PHONY: all build run stop test lint migrate-up clean help
|
||||||
|
|
||||||
|
SERVICES = gateway auth-service purchaser-service textile-service washing-service
|
||||||
|
|
||||||
|
help: ## 显示帮助信息
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
all: build ## 构建所有服务
|
||||||
|
|
||||||
|
build: ## 构建所有服务二进制
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
echo "==> Building $$svc..."; \
|
||||||
|
cd $$svc && go build -o bin/$$svc ./cmd/ && cd ..; \
|
||||||
|
done
|
||||||
|
@echo "==> All services built."
|
||||||
|
|
||||||
|
run: ## 启动所有服务 (Docker Compose)
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
run-dev: ## 启动开发环境 (前台,含日志)
|
||||||
|
docker-compose up --build
|
||||||
|
|
||||||
|
stop: ## 停止所有服务
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
restart: stop run ## 重启所有服务
|
||||||
|
|
||||||
|
logs: ## 查看所有服务日志
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
logs-%: ## 查看指定服务日志 (如: make logs-gateway)
|
||||||
|
docker-compose logs -f $*
|
||||||
|
|
||||||
|
test: ## 运行所有服务的测试
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
echo "==> Testing $$svc..."; \
|
||||||
|
cd $$svc && go test ./... -v && cd ..; \
|
||||||
|
done
|
||||||
|
|
||||||
|
lint: ## 代码检查
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
echo "==> Linting $$svc..."; \
|
||||||
|
cd $$svc && go vet ./... && cd ..; \
|
||||||
|
done
|
||||||
|
|
||||||
|
tidy: ## 整理所有模块依赖
|
||||||
|
@for svc in shared $(SERVICES); do \
|
||||||
|
echo "==> Tidying $$svc..."; \
|
||||||
|
cd $$svc && go mod tidy && cd ..; \
|
||||||
|
done
|
||||||
|
|
||||||
|
migrate-up: ## 执行数据库迁移
|
||||||
|
@echo "==> Running database migrations..."
|
||||||
|
@PGPASSWORD=$${DB_PASSWORD:-iloom_secret_2024} psql -h localhost -U $${POSTGRES_USER:-iloom} -d $${POSTGRES_DB:-iloom} -f scripts/init-schemas.sql
|
||||||
|
@echo "==> Migrations complete."
|
||||||
|
|
||||||
|
clean: ## 清理构建产物
|
||||||
|
@for svc in $(SERVICES); do \
|
||||||
|
rm -rf $$svc/bin; \
|
||||||
|
done
|
||||||
|
@echo "==> Cleaned."
|
||||||
|
|
||||||
|
dev-%: ## 单独运行指定服务 (如: make dev-gateway)
|
||||||
|
cd $* && go run ./cmd/
|
||||||
|
|
||||||
|
docker-build-%: ## 单独构建指定服务镜像 (如: make docker-build-gateway)
|
||||||
|
docker-compose build $*
|
||||||
|
|
||||||
|
health: ## 检查所有服务健康状态
|
||||||
|
@echo "Gateway: $$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/health)"
|
||||||
|
@echo "Auth: $$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8081/health)"
|
||||||
|
@echo "Purchaser: $$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8082/health)"
|
||||||
|
@echo "Textile: $$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8083/health)"
|
||||||
|
@echo "Washing: $$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8084/health)"
|
||||||
12
auth-service/Dockerfile
Normal file
12
auth-service/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY shared/ ./shared/
|
||||||
|
COPY auth-service/ ./auth-service/
|
||||||
|
WORKDIR /app/auth-service
|
||||||
|
RUN go build -o /auth-service ./cmd/
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache wget
|
||||||
|
COPY --from=builder /auth-service /auth-service
|
||||||
|
EXPOSE 8081
|
||||||
|
CMD ["/auth-service"]
|
||||||
93
auth-service/cmd/main.go
Normal file
93
auth-service/cmd/main.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/config"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/handler"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/repository"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/auth"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/database"
|
||||||
|
sgrpc "github.com/muyuqingfeng/iloom/shared/pkg/grpc"
|
||||||
|
sharedMW "github.com/muyuqingfeng/iloom/shared/pkg/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
db, err := database.NewDBFromDSN(cfg.DBDSN)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("connect database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
sysClient, err := sgrpc.NewSystemClient(cfg.SystemRPCAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("connect system-rpc: %v", err)
|
||||||
|
}
|
||||||
|
defer sysClient.Close()
|
||||||
|
|
||||||
|
jwtMgr := auth.NewJWTManager(cfg.JWTSecret, cfg.JWTRefreshSecret)
|
||||||
|
|
||||||
|
profileRepo := repository.NewProfileRepo(db)
|
||||||
|
companyRepo := repository.NewCompanyRepo(db)
|
||||||
|
memberRepo := repository.NewMemberRepo(db)
|
||||||
|
notifRepo := repository.NewNotificationRepo(db)
|
||||||
|
shareLinkRepo := repository.NewShareLinkRepo(db)
|
||||||
|
auditLogRepo := repository.NewAuditLogRepo(db)
|
||||||
|
|
||||||
|
authSvc := service.NewAuthService(profileRepo, companyRepo, memberRepo, sysClient, jwtMgr)
|
||||||
|
companySvc := service.NewCompanyService(companyRepo)
|
||||||
|
memberSvc := service.NewMemberService(memberRepo, sysClient)
|
||||||
|
notifSvc := service.NewNotificationService(notifRepo)
|
||||||
|
|
||||||
|
authH := handler.NewAuthHandler(authSvc)
|
||||||
|
companyH := handler.NewCompanyHandler(companySvc)
|
||||||
|
memberH := handler.NewMemberHandler(memberSvc)
|
||||||
|
notifH := handler.NewNotificationHandler(notifSvc)
|
||||||
|
commonH := handler.NewCommonHandler(shareLinkRepo, auditLogRepo)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(
|
||||||
|
sharedMW.RequestID(),
|
||||||
|
sharedMW.Logger(),
|
||||||
|
sharedMW.Recovery(),
|
||||||
|
)
|
||||||
|
|
||||||
|
r.GET("/health", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
|
|
||||||
|
authGroup := r.Group("/api/v1/auth")
|
||||||
|
{
|
||||||
|
authGroup.POST("/register", authH.Register)
|
||||||
|
authGroup.POST("/login", authH.Login)
|
||||||
|
authGroup.POST("/logout", authH.Logout)
|
||||||
|
authGroup.POST("/refresh", authH.Refresh)
|
||||||
|
authGroup.GET("/me", authH.Me)
|
||||||
|
}
|
||||||
|
|
||||||
|
commonGroup := r.Group("/api/v1/common")
|
||||||
|
{
|
||||||
|
commonGroup.GET("/companies", companyH.ListRelationships)
|
||||||
|
commonGroup.GET("/companies/:id", companyH.GetCompany)
|
||||||
|
commonGroup.GET("/members", memberH.List)
|
||||||
|
commonGroup.POST("/members", memberH.Create)
|
||||||
|
commonGroup.DELETE("/members/:id", memberH.Delete)
|
||||||
|
commonGroup.GET("/notifications", notifH.List)
|
||||||
|
commonGroup.PATCH("/notifications/:id", notifH.MarkRead)
|
||||||
|
commonGroup.POST("/share-links", commonH.CreateShareLink)
|
||||||
|
commonGroup.GET("/share-links/:code", commonH.GetShareLink)
|
||||||
|
commonGroup.POST("/feedback", commonH.Feedback)
|
||||||
|
commonGroup.POST("/crash-reports", commonH.CrashReport)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := ":" + cfg.Port
|
||||||
|
log.Printf("auth-service listening on %s", addr)
|
||||||
|
if err := r.Run(addr); err != nil {
|
||||||
|
log.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
auth-service/go.mod
Normal file
48
auth-service/go.mod
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
module github.com/muyuqingfeng/iloom/auth-service
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require github.com/muyuqingfeng/iloom/shared v0.0.0
|
||||||
|
|
||||||
|
replace github.com/muyuqingfeng/iloom/shared => ../shared
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.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.20.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // 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-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
|
google.golang.org/grpc v1.72.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
117
auth-service/go.sum
Normal file
117
auth-service/go.sum
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
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/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
|
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||||
|
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||||
|
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||||
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
|
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||||
|
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||||
|
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
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=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
28
auth-service/internal/config/config.go
Normal file
28
auth-service/internal/config/config.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
DBDSN string
|
||||||
|
JWTSecret string
|
||||||
|
JWTRefreshSecret string
|
||||||
|
SystemRPCAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Port: getEnv("PORT", "8081"),
|
||||||
|
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"),
|
||||||
|
JWTSecret: getEnv("JWT_SECRET", "muyu-wms-jwt-secret-key-2026"),
|
||||||
|
JWTRefreshSecret: getEnv("JWT_REFRESH_SECRET", "muyu-wms-jwt-refresh-2026"),
|
||||||
|
SystemRPCAddr: getEnv("MUYU_SYSTEM_RPC_ADDR", "127.0.0.1:9001"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
116
auth-service/internal/handler/auth.go
Normal file
116
auth-service/internal/handler/auth.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
svc *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(svc *service.AuthService) *AuthHandler {
|
||||||
|
return &AuthHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required,min=6"`
|
||||||
|
CompanyName string `json:"company_name" binding:"required"`
|
||||||
|
Role string `json:"role" binding:"required,oneof=purchaser textile washing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
User *model.Profile `json:"user"`
|
||||||
|
Company *model.Company `json:"company"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Register(c *gin.Context) {
|
||||||
|
var req RegisterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.RegisterRequest{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
CompanyName: req.CompanyName,
|
||||||
|
Role: req.Role,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.svc.Register(c.Request.Context(), svcReq)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusConflict, response.ErrCodeBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.svc.Login(c.Request.Context(), req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||||
|
response.OK(c, gin.H{"message": "logged out"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Refresh(c *gin.Context) {
|
||||||
|
var req RefreshRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := h.svc.RefreshToken(c.Request.Context(), req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, gin.H{"access_token": accessToken})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Me(c *gin.Context) {
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if userID == "" {
|
||||||
|
response.Unauthorized(c, "user id not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.svc.GetMe(c.Request.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
response.NotFound(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
134
auth-service/internal/handler/common.go
Normal file
134
auth-service/internal/handler/common.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/repository"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommonHandler struct {
|
||||||
|
shareLinkRepo *repository.ShareLinkRepo
|
||||||
|
auditLogRepo *repository.AuditLogRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommonHandler(slRepo *repository.ShareLinkRepo, alRepo *repository.AuditLogRepo) *CommonHandler {
|
||||||
|
return &CommonHandler{shareLinkRepo: slRepo, auditLogRepo: alRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
type createShareLinkReq struct {
|
||||||
|
ResourceType string `json:"resource_type" binding:"required"`
|
||||||
|
ResourceID string `json:"resource_id" binding:"required"`
|
||||||
|
ExpiresIn int `json:"expires_in"` // seconds, 0 = no expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommonHandler) CreateShareLink(c *gin.Context) {
|
||||||
|
var req createShareLinkReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
|
||||||
|
var expiresAt *time.Time
|
||||||
|
if req.ExpiresIn > 0 {
|
||||||
|
t := time.Now().Add(time.Duration(req.ExpiresIn) * time.Second)
|
||||||
|
expiresAt = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := h.shareLinkRepo.Create(c.Request.Context(), companyID, userID, req.ResourceType, req.ResourceID, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommonHandler) GetShareLink(c *gin.Context) {
|
||||||
|
code := c.Param("code")
|
||||||
|
|
||||||
|
link, err := h.shareLinkRepo.GetByCode(c.Request.Context(), code)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if link == nil {
|
||||||
|
response.NotFound(c, "share link not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check expiry
|
||||||
|
if link.ExpiresAt != nil && link.ExpiresAt.Before(time.Now()) {
|
||||||
|
response.BadRequest(c, "share link expired")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
type feedbackReq struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommonHandler) Feedback(c *gin.Context) {
|
||||||
|
var req feedbackReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
|
||||||
|
feedbackType := req.Type
|
||||||
|
if feedbackType == "" {
|
||||||
|
feedbackType = "general"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.auditLogRepo.Create(
|
||||||
|
c.Request.Context(), userID, companyID,
|
||||||
|
"feedback", feedbackType, "",
|
||||||
|
c.ClientIP(), map[string]string{"content": req.Content},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, gin.H{"message": "feedback received"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type crashReportReq struct {
|
||||||
|
Error string `json:"error" binding:"required"`
|
||||||
|
StackTrace string `json:"stack_trace"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CommonHandler) CrashReport(c *gin.Context) {
|
||||||
|
var req crashReportReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
|
||||||
|
err := h.auditLogRepo.Create(
|
||||||
|
c.Request.Context(), userID, companyID,
|
||||||
|
"crash_report", "frontend", "",
|
||||||
|
c.ClientIP(), req,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, gin.H{"message": "crash report received"})
|
||||||
|
}
|
||||||
41
auth-service/internal/handler/company.go
Normal file
41
auth-service/internal/handler/company.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompanyHandler struct {
|
||||||
|
svc *service.CompanyService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompanyHandler(svc *service.CompanyService) *CompanyHandler {
|
||||||
|
return &CompanyHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CompanyHandler) GetCompany(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
company, err := h.svc.GetByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
response.NotFound(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, company)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CompanyHandler) ListRelationships(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "company id not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rels, err := h.svc.ListRelationships(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, rels)
|
||||||
|
}
|
||||||
84
auth-service/internal/handler/member.go
Normal file
84
auth-service/internal/handler/member.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemberHandler struct {
|
||||||
|
svc *service.MemberService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemberHandler(svc *service.MemberService) *MemberHandler {
|
||||||
|
return &MemberHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateMemberRequest struct {
|
||||||
|
Username string `json:"username" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required,min=6"`
|
||||||
|
RealName string `json:"real_name"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MemberHandler) List(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "company id not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := h.svc.List(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, members)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MemberHandler) Create(c *gin.Context) {
|
||||||
|
var req CreateMemberRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
roleKey := c.GetHeader("X-User-Role")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.CreateMemberRequest{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
RealName: req.RealName,
|
||||||
|
Phone: req.Phone,
|
||||||
|
}
|
||||||
|
|
||||||
|
member, err := h.svc.Create(c.Request.Context(), companyID, roleKey, svcReq)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, member)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MemberHandler) Delete(c *gin.Context) {
|
||||||
|
memberID := c.Param("id")
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "company id not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.Delete(c.Request.Context(), memberID, companyID); err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, gin.H{"message": "member deleted"})
|
||||||
|
}
|
||||||
47
auth-service/internal/handler/notification.go
Normal file
47
auth-service/internal/handler/notification.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationHandler struct {
|
||||||
|
svc *service.NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationHandler(svc *service.NotificationService) *NotificationHandler {
|
||||||
|
return &NotificationHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NotificationHandler) List(c *gin.Context) {
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if userID == "" {
|
||||||
|
response.BadRequest(c, "user id not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications, err := h.svc.List(c.Request.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, notifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NotificationHandler) MarkRead(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if userID == "" {
|
||||||
|
response.BadRequest(c, "user id not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.MarkRead(c.Request.Context(), id, userID); err != nil {
|
||||||
|
response.InternalError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, gin.H{"message": "marked as read"})
|
||||||
|
}
|
||||||
32
auth-service/internal/model/company.go
Normal file
32
auth-service/internal/model/company.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Company struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Address string `json:"address,omitempty"`
|
||||||
|
Phone string `json:"phone,omitempty"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompanyMember struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CompanyID string `json:"company_id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
IsMaster bool `json:"is_master"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompanyRelationship struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
PurchaserCompanyID string `json:"purchaser_company_id"`
|
||||||
|
FactoryCompanyID string `json:"factory_company_id"`
|
||||||
|
FactoryType string `json:"factory_type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
35
auth-service/internal/model/notification.go
Normal file
35
auth-service/internal/model/notification.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
RecipientID string `json:"recipient_id" db:"recipient_id"`
|
||||||
|
SenderID *string `json:"sender_id,omitempty" db:"sender_id"`
|
||||||
|
SenderCompanyID *string `json:"sender_company_id,omitempty" db:"sender_company_id"`
|
||||||
|
Title string `json:"title" db:"title"`
|
||||||
|
Content string `json:"content" db:"content"`
|
||||||
|
Type string `json:"type" db:"type"`
|
||||||
|
PlanID *string `json:"plan_id,omitempty" db:"plan_id"`
|
||||||
|
IsRead bool `json:"is_read" db:"is_read"`
|
||||||
|
ReadAt *time.Time `json:"read_at,omitempty" db:"read_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShareLink struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanID string `json:"plan_id" db:"plan_id"`
|
||||||
|
CreatedBy string `json:"created_by" db:"created_by"`
|
||||||
|
ShortCode string `json:"short_code" db:"short_code"`
|
||||||
|
FactoryType string `json:"factory_type" db:"factory_type"`
|
||||||
|
HideSensitive bool `json:"hide_sensitive" db:"hide_sensitive"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty" db:"expires_at"`
|
||||||
|
ClickedAt *time.Time `json:"clicked_at,omitempty" db:"clicked_at"`
|
||||||
|
ClickCount int `json:"click_count" db:"click_count"`
|
||||||
|
UsedAt *time.Time `json:"used_at,omitempty" db:"used_at"`
|
||||||
|
ResponseResult *string `json:"response_result,omitempty" db:"response_result"`
|
||||||
|
RejectReason *string `json:"reject_reason,omitempty" db:"reject_reason"`
|
||||||
|
RespondedAt *time.Time `json:"responded_at,omitempty" db:"responded_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
18
auth-service/internal/model/profile.go
Normal file
18
auth-service/internal/model/profile.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
RealName string `json:"real_name,omitempty"`
|
||||||
|
Phone string `json:"phone,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
CompanyID string `json:"company_id,omitempty"`
|
||||||
|
CompanyName string `json:"company_name,omitempty"`
|
||||||
|
Role string `json:"role,omitempty"`
|
||||||
|
IsMaster bool `json:"is_master"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
41
auth-service/internal/repository/audit_log_repo.go
Normal file
41
auth-service/internal/repository/audit_log_repo.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuditLogRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuditLogRepo(db *sql.DB) *AuditLogRepo {
|
||||||
|
return &AuditLogRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AuditLogRepo) Create(ctx context.Context, userID, companyID, action, resourceType, resourceID, ip string, newValue any) error {
|
||||||
|
id := uuid.New().String()
|
||||||
|
|
||||||
|
var valueJSON []byte
|
||||||
|
if newValue != nil {
|
||||||
|
var err error
|
||||||
|
valueJSON, err = json.Marshal(newValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal audit value: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO ilm_audit_log (id, user_id, company_id, action, resource_type, resource_id, new_value, ip)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
id, userID, companyID, action, resourceType, resourceID, valueJSON, ip,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create audit log: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
69
auth-service/internal/repository/company_repo.go
Normal file
69
auth-service/internal/repository/company_repo.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompanyRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompanyRepo(db *sql.DB) *CompanyRepo {
|
||||||
|
return &CompanyRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CompanyRepo) Create(ctx context.Context, c *model.Company) error {
|
||||||
|
query := `INSERT INTO ilm_company (id, name, type, address, phone, status, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
c.ID, c.Name, c.Role, c.Address, c.Phone, c.Status, c.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert company: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CompanyRepo) GetByID(ctx context.Context, id string) (*model.Company, error) {
|
||||||
|
query := `SELECT id, name, type, address, phone, status, created_at FROM ilm_company WHERE id = ?`
|
||||||
|
c := &model.Company{}
|
||||||
|
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||||
|
&c.ID, &c.Name, &c.Role, &c.Address, &c.Phone, &c.Status, &c.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get company by id: %w", err)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CompanyRepo) ListRelationships(ctx context.Context, companyID string) ([]model.CompanyRelationship, error) {
|
||||||
|
query := `SELECT id, purchaser_company_id, factory_company_id, factory_type, status, created_at, updated_at
|
||||||
|
FROM ilm_company_relationship
|
||||||
|
WHERE purchaser_company_id = ? OR factory_company_id = ?
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list relationships: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var rels []model.CompanyRelationship
|
||||||
|
for rows.Next() {
|
||||||
|
var rel model.CompanyRelationship
|
||||||
|
if err := rows.Scan(
|
||||||
|
&rel.ID, &rel.PurchaserCompanyID, &rel.FactoryCompanyID,
|
||||||
|
&rel.FactoryType, &rel.Status, &rel.CreatedAt, &rel.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan relationship: %w", err)
|
||||||
|
}
|
||||||
|
rels = append(rels, rel)
|
||||||
|
}
|
||||||
|
return rels, nil
|
||||||
|
}
|
||||||
85
auth-service/internal/repository/member_repo.go
Normal file
85
auth-service/internal/repository/member_repo.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemberRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemberRepo(db *sql.DB) *MemberRepo {
|
||||||
|
return &MemberRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MemberRepo) Create(ctx context.Context, m *model.CompanyMember) error {
|
||||||
|
query := `INSERT INTO ilm_company_member (id, company_id, user_id, role, is_master, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
m.ID, m.CompanyID, m.UserID, m.Role, m.IsMaster, m.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert member: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MemberRepo) Delete(ctx context.Context, id, companyID string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`DELETE FROM ilm_company_member WHERE id = ? AND company_id = ?`,
|
||||||
|
id, companyID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete member: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MemberRepo) GetByUserAndCompany(ctx context.Context, userID, companyID string) (*model.CompanyMember, error) {
|
||||||
|
query := `SELECT id, company_id, user_id, role, is_master, created_at
|
||||||
|
FROM ilm_company_member WHERE user_id = ? AND company_id = ?`
|
||||||
|
m := &model.CompanyMember{}
|
||||||
|
err := r.db.QueryRowContext(ctx, query, userID, companyID).Scan(
|
||||||
|
&m.ID, &m.CompanyID, &m.UserID, &m.Role, &m.IsMaster, &m.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get member: %w", err)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MemberRepo) ListByCompany(ctx context.Context, companyID string) ([]model.Profile, error) {
|
||||||
|
query := `SELECT su.user_id, su.username, su.real_name, su.phone, su.email, su.avatar, su.status, su.created_at,
|
||||||
|
cm.company_id, COALESCE(c.name, ''), COALESCE(c.type, ''), cm.is_master
|
||||||
|
FROM ilm_company_member cm
|
||||||
|
JOIN sys_user su ON su.user_id = cm.user_id AND su.deleted_at IS NULL
|
||||||
|
LEFT JOIN ilm_company c ON c.id = cm.company_id
|
||||||
|
WHERE cm.company_id = ?
|
||||||
|
ORDER BY cm.created_at`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list members by company: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var profiles []model.Profile
|
||||||
|
for rows.Next() {
|
||||||
|
var p model.Profile
|
||||||
|
if err := rows.Scan(
|
||||||
|
&p.ID, &p.Username, &p.RealName, &p.Phone, &p.Email,
|
||||||
|
&p.Avatar, &p.Status, &p.CreatedAt,
|
||||||
|
&p.CompanyID, &p.CompanyName, &p.Role, &p.IsMaster,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan member profile: %w", err)
|
||||||
|
}
|
||||||
|
profiles = append(profiles, p)
|
||||||
|
}
|
||||||
|
return profiles, nil
|
||||||
|
}
|
||||||
54
auth-service/internal/repository/notification_repo.go
Normal file
54
auth-service/internal/repository/notification_repo.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationRepo(db *sql.DB) *NotificationRepo {
|
||||||
|
return &NotificationRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepo) ListByRecipient(ctx context.Context, userID string) ([]model.Notification, error) {
|
||||||
|
query := `SELECT id, recipient_id, sender_id, sender_company_id, title, content, type, plan_id, is_read, read_at, created_at
|
||||||
|
FROM ilm_notification WHERE recipient_id = ? ORDER BY created_at DESC`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list notifications: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notifications []model.Notification
|
||||||
|
for rows.Next() {
|
||||||
|
var n model.Notification
|
||||||
|
if err := rows.Scan(
|
||||||
|
&n.ID, &n.RecipientID, &n.SenderID, &n.SenderCompanyID,
|
||||||
|
&n.Title, &n.Content, &n.Type, &n.PlanID,
|
||||||
|
&n.IsRead, &n.ReadAt, &n.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan notification: %w", err)
|
||||||
|
}
|
||||||
|
notifications = append(notifications, n)
|
||||||
|
}
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NotificationRepo) MarkRead(ctx context.Context, id, userID string) error {
|
||||||
|
now := time.Now()
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`UPDATE ilm_notification SET is_read = true, read_at = ? WHERE id = ? AND recipient_id = ?`,
|
||||||
|
now, id, userID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mark notification read: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
74
auth-service/internal/repository/profile_repo.go
Normal file
74
auth-service/internal/repository/profile_repo.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfileRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfileRepo(db *sql.DB) *ProfileRepo {
|
||||||
|
return &ProfileRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProfileRepo) GetByID(ctx context.Context, userID string) (*model.Profile, error) {
|
||||||
|
query := `SELECT user_id, username, real_name, phone, email, avatar, status, created_at
|
||||||
|
FROM sys_user WHERE user_id = ? AND deleted_at IS NULL`
|
||||||
|
p := &model.Profile{}
|
||||||
|
err := r.db.QueryRowContext(ctx, query, userID).Scan(
|
||||||
|
&p.ID, &p.Username, &p.RealName, &p.Phone, &p.Email,
|
||||||
|
&p.Avatar, &p.Status, &p.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get profile by id: %w", err)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProfileRepo) GetByUsername(ctx context.Context, username string) (*model.Profile, error) {
|
||||||
|
query := `SELECT user_id, username, real_name, phone, email, avatar, status, created_at
|
||||||
|
FROM sys_user WHERE username = ? AND deleted_at IS NULL`
|
||||||
|
p := &model.Profile{}
|
||||||
|
err := r.db.QueryRowContext(ctx, query, username).Scan(
|
||||||
|
&p.ID, &p.Username, &p.RealName, &p.Phone, &p.Email,
|
||||||
|
&p.Avatar, &p.Status, &p.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get profile by username: %w", err)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProfileRepo) GetWithCompany(ctx context.Context, userID string) (*model.Profile, error) {
|
||||||
|
query := `SELECT su.user_id, su.username, su.real_name, su.phone, su.email, su.avatar, su.status, su.created_at,
|
||||||
|
COALESCE(cm.company_id, ''), COALESCE(c.name, ''), COALESCE(c.type, ''), COALESCE(cm.is_master, 0)
|
||||||
|
FROM sys_user su
|
||||||
|
LEFT JOIN ilm_company_member cm ON cm.user_id = su.user_id
|
||||||
|
LEFT JOIN ilm_company c ON c.id = cm.company_id
|
||||||
|
WHERE su.user_id = ? AND su.deleted_at IS NULL
|
||||||
|
LIMIT 1`
|
||||||
|
p := &model.Profile{}
|
||||||
|
err := r.db.QueryRowContext(ctx, query, userID).Scan(
|
||||||
|
&p.ID, &p.Username, &p.RealName, &p.Phone, &p.Email,
|
||||||
|
&p.Avatar, &p.Status, &p.CreatedAt,
|
||||||
|
&p.CompanyID, &p.CompanyName, &p.Role, &p.IsMaster,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get profile with company: %w", err)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
77
auth-service/internal/repository/share_link_repo.go
Normal file
77
auth-service/internal/repository/share_link_repo.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShareLinkRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShareLinkRepo(db *sql.DB) *ShareLinkRepo {
|
||||||
|
return &ShareLinkRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShareLink struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
CompanyID string `json:"company_id"`
|
||||||
|
CreatedBy string `json:"created_by"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceID string `json:"resource_id"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ShareLinkRepo) Create(ctx context.Context, companyID, createdBy, resourceType, resourceID string, expiresAt *time.Time) (*ShareLink, error) {
|
||||||
|
id := uuid.New().String()
|
||||||
|
code := generateShortCode()
|
||||||
|
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO ilm_share_link (id, code, company_id, created_by, resource_type, resource_id, expires_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
id, code, companyID, createdBy, resourceType, resourceID, expiresAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create share link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ShareLink{
|
||||||
|
ID: id,
|
||||||
|
Code: code,
|
||||||
|
CompanyID: companyID,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
ResourceType: resourceType,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ShareLinkRepo) GetByCode(ctx context.Context, code string) (*ShareLink, error) {
|
||||||
|
var sl ShareLink
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
`SELECT id, code, company_id, created_by, resource_type, resource_id, expires_at, created_at
|
||||||
|
FROM ilm_share_link WHERE code = ?`, code,
|
||||||
|
).Scan(&sl.ID, &sl.Code, &sl.CompanyID, &sl.CreatedBy, &sl.ResourceType, &sl.ResourceID, &sl.ExpiresAt, &sl.CreatedAt)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get share link: %w", err)
|
||||||
|
}
|
||||||
|
return &sl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateShortCode() string {
|
||||||
|
b := make([]byte, 6)
|
||||||
|
rand.Read(b)
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
220
auth-service/internal/service/auth_service.go
Normal file
220
auth-service/internal/service/auth_service.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/repository"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/auth"
|
||||||
|
sgrpc "github.com/muyuqingfeng/iloom/shared/pkg/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var roleIDMap = map[string]string{
|
||||||
|
"purchaser": "r_010",
|
||||||
|
"textile": "r_011",
|
||||||
|
"washing": "r_012",
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthService struct {
|
||||||
|
profileRepo *repository.ProfileRepo
|
||||||
|
compRepo *repository.CompanyRepo
|
||||||
|
memberRepo *repository.MemberRepo
|
||||||
|
sysClient *sgrpc.SystemClient
|
||||||
|
jwt *auth.JWTManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(
|
||||||
|
profileRepo *repository.ProfileRepo,
|
||||||
|
compRepo *repository.CompanyRepo,
|
||||||
|
memberRepo *repository.MemberRepo,
|
||||||
|
sysClient *sgrpc.SystemClient,
|
||||||
|
jwt *auth.JWTManager,
|
||||||
|
) *AuthService {
|
||||||
|
return &AuthService{
|
||||||
|
profileRepo: profileRepo,
|
||||||
|
compRepo: compRepo,
|
||||||
|
memberRepo: memberRepo,
|
||||||
|
sysClient: sysClient,
|
||||||
|
jwt: jwt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
CompanyName string `json:"company_name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
User *model.Profile `json:"user"`
|
||||||
|
Company *model.Company `json:"company"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileWithCompany struct {
|
||||||
|
User *model.Profile `json:"user"`
|
||||||
|
Company *model.Company `json:"company,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Register(ctx context.Context, req RegisterRequest) (*LoginResponse, error) {
|
||||||
|
roleID, ok := roleIDMap[req.Role]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid role")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := s.sysClient.CreateUser(ctx, req.Username, req.Password, "", "", roleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
companyID := uuid.New().String()
|
||||||
|
company := &model.Company{
|
||||||
|
ID: companyID,
|
||||||
|
Name: req.CompanyName,
|
||||||
|
Role: req.Role,
|
||||||
|
Status: 1,
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
if err := s.compRepo.Create(ctx, company); err != nil {
|
||||||
|
return nil, fmt.Errorf("create company: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
member := &model.CompanyMember{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
CompanyID: companyID,
|
||||||
|
UserID: userID,
|
||||||
|
Role: "owner",
|
||||||
|
IsMaster: true,
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
if err := s.memberRepo.Create(ctx, member); err != nil {
|
||||||
|
return nil, fmt.Errorf("create member: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile := &model.Profile{
|
||||||
|
ID: userID,
|
||||||
|
Username: req.Username,
|
||||||
|
CompanyID: companyID,
|
||||||
|
CompanyName: req.CompanyName,
|
||||||
|
Role: req.Role,
|
||||||
|
IsMaster: true,
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.generateTokens(profile, company)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Login(ctx context.Context, username, password string) (*LoginResponse, error) {
|
||||||
|
result, err := s.sysClient.Login(ctx, username, password, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := s.profileRepo.GetWithCompany(ctx, result.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get profile: %w", err)
|
||||||
|
}
|
||||||
|
if profile == nil {
|
||||||
|
profile = &model.Profile{
|
||||||
|
ID: result.UserID,
|
||||||
|
Username: result.Username,
|
||||||
|
RealName: result.RealName,
|
||||||
|
Avatar: result.Avatar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var company *model.Company
|
||||||
|
if profile.CompanyID != "" {
|
||||||
|
company, err = s.compRepo.GetByID(ctx, profile.CompanyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get company: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.generateTokens(profile, company)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (string, error) {
|
||||||
|
userID, err := s.jwt.ParseRefreshToken(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := s.profileRepo.GetWithCompany(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get user: %w", err)
|
||||||
|
}
|
||||||
|
if profile == nil {
|
||||||
|
return "", errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := auth.Claims{
|
||||||
|
UserID: profile.ID,
|
||||||
|
CompanyID: profile.CompanyID,
|
||||||
|
Role: profile.Role,
|
||||||
|
IsMaster: profile.IsMaster,
|
||||||
|
Username: profile.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.jwt.GenerateAccessToken(claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) GetMe(ctx context.Context, userID string) (*ProfileWithCompany, error) {
|
||||||
|
profile, err := s.profileRepo.GetWithCompany(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get profile: %w", err)
|
||||||
|
}
|
||||||
|
if profile == nil {
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &ProfileWithCompany{User: profile}
|
||||||
|
if profile.CompanyID != "" {
|
||||||
|
company, err := s.compRepo.GetByID(ctx, profile.CompanyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get company: %w", err)
|
||||||
|
}
|
||||||
|
result.Company = company
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) generateTokens(profile *model.Profile, company *model.Company) (*LoginResponse, error) {
|
||||||
|
role := profile.Role
|
||||||
|
if role == "" && company != nil {
|
||||||
|
role = company.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := auth.Claims{
|
||||||
|
UserID: profile.ID,
|
||||||
|
CompanyID: profile.CompanyID,
|
||||||
|
Role: role,
|
||||||
|
IsMaster: profile.IsMaster,
|
||||||
|
Username: profile.Username,
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := s.jwt.GenerateAccessToken(claims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate access token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := s.jwt.GenerateRefreshToken(profile.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LoginResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
User: profile,
|
||||||
|
Company: company,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
33
auth-service/internal/service/company_service.go
Normal file
33
auth-service/internal/service/company_service.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompanyService struct {
|
||||||
|
repo *repository.CompanyRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompanyService(repo *repository.CompanyRepo) *CompanyService {
|
||||||
|
return &CompanyService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompanyService) GetByID(ctx context.Context, id string) (*model.Company, error) {
|
||||||
|
company, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get company: %w", err)
|
||||||
|
}
|
||||||
|
if company == nil {
|
||||||
|
return nil, errors.New("company not found")
|
||||||
|
}
|
||||||
|
return company, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CompanyService) ListRelationships(ctx context.Context, companyID string) ([]model.CompanyRelationship, error) {
|
||||||
|
return s.repo.ListRelationships(ctx, companyID)
|
||||||
|
}
|
||||||
78
auth-service/internal/service/member_service.go
Normal file
78
auth-service/internal/service/member_service.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/repository"
|
||||||
|
sgrpc "github.com/muyuqingfeng/iloom/shared/pkg/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemberService struct {
|
||||||
|
memberRepo *repository.MemberRepo
|
||||||
|
sysClient *sgrpc.SystemClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemberService(memberRepo *repository.MemberRepo, sysClient *sgrpc.SystemClient) *MemberService {
|
||||||
|
return &MemberService{
|
||||||
|
memberRepo: memberRepo,
|
||||||
|
sysClient: sysClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateMemberRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
RealName string `json:"real_name"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemberService) List(ctx context.Context, companyID string) ([]model.Profile, error) {
|
||||||
|
return s.memberRepo.ListByCompany(ctx, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemberService) Create(ctx context.Context, companyID, roleKey string, req CreateMemberRequest) (*model.Profile, error) {
|
||||||
|
roleID := roleIDMap[roleKey]
|
||||||
|
|
||||||
|
userID, err := s.sysClient.CreateUser(ctx, req.Username, req.Password, req.RealName, req.Phone, roleID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
member := &model.CompanyMember{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
CompanyID: companyID,
|
||||||
|
UserID: userID,
|
||||||
|
Role: "member",
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
if err := s.memberRepo.Create(ctx, member); err != nil {
|
||||||
|
return nil, fmt.Errorf("create member: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Profile{
|
||||||
|
ID: userID,
|
||||||
|
Username: req.Username,
|
||||||
|
RealName: req.RealName,
|
||||||
|
Phone: req.Phone,
|
||||||
|
CompanyID: companyID,
|
||||||
|
CreatedAt: now,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemberService) Delete(ctx context.Context, memberID, companyID string) error {
|
||||||
|
member, err := s.memberRepo.GetByUserAndCompany(ctx, memberID, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get member: %w", err)
|
||||||
|
}
|
||||||
|
if member == nil {
|
||||||
|
return errors.New("member not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.memberRepo.Delete(ctx, member.ID, companyID)
|
||||||
|
}
|
||||||
24
auth-service/internal/service/notification_service.go
Normal file
24
auth-service/internal/service/notification_service.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/auth-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationService struct {
|
||||||
|
repo *repository.NotificationRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationService(repo *repository.NotificationRepo) *NotificationService {
|
||||||
|
return &NotificationService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationService) List(ctx context.Context, userID string) ([]model.Notification, error) {
|
||||||
|
return s.repo.ListByRecipient(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationService) MarkRead(ctx context.Context, id, userID string) error {
|
||||||
|
return s.repo.MarkRead(ctx, id, userID)
|
||||||
|
}
|
||||||
128
docker-compose.integrated.yml
Normal file
128
docker-compose.integrated.yml
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# iloom 集成模式 — 连接 muyu-apiserver 的 MySQL 和 gRPC
|
||||||
|
# 用法:docker compose -f docker-compose.integrated.yml up -d
|
||||||
|
|
||||||
|
services:
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: gateway/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${GATEWAY_PORT:-8080}:8080"
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026}
|
||||||
|
- AUTH_SERVICE_URL=http://auth-service:8081
|
||||||
|
- PURCHASER_SERVICE_URL=http://purchaser-service:8082
|
||||||
|
- TEXTILE_SERVICE_URL=http://textile-service:8083
|
||||||
|
- WASHING_SERVICE_URL=http://washing-service:8084
|
||||||
|
- ALLOW_ORIGINS=${ALLOW_ORIGINS:-http://localhost:3015}
|
||||||
|
depends_on:
|
||||||
|
auth-service:
|
||||||
|
condition: service_healthy
|
||||||
|
purchaser-service:
|
||||||
|
condition: service_healthy
|
||||||
|
textile-service:
|
||||||
|
condition: service_healthy
|
||||||
|
washing-service:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- muyu-net
|
||||||
|
- default
|
||||||
|
|
||||||
|
auth-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: auth-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${AUTH_SERVICE_PORT:-8081}:8081"
|
||||||
|
environment:
|
||||||
|
- PORT=8081
|
||||||
|
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026}
|
||||||
|
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-muyu-wms-jwt-refresh-2026}
|
||||||
|
- MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8081/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- muyu-net
|
||||||
|
- default
|
||||||
|
|
||||||
|
purchaser-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: purchaser-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${PURCHASER_SERVICE_PORT:-8082}:8082"
|
||||||
|
environment:
|
||||||
|
- PORT=8082
|
||||||
|
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
|
||||||
|
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret}
|
||||||
|
- MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8082/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- muyu-net
|
||||||
|
- default
|
||||||
|
|
||||||
|
textile-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: textile-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${TEXTILE_SERVICE_PORT:-8083}:8083"
|
||||||
|
environment:
|
||||||
|
- PORT=8083
|
||||||
|
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
|
||||||
|
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret}
|
||||||
|
- PURCHASER_SERVICE_URL=http://purchaser-service:8082
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8083/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- muyu-net
|
||||||
|
- default
|
||||||
|
|
||||||
|
washing-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: washing-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${WASHING_SERVICE_PORT:-8084}:8084"
|
||||||
|
environment:
|
||||||
|
- PORT=8084
|
||||||
|
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
|
||||||
|
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret}
|
||||||
|
- PURCHASER_SERVICE_URL=http://purchaser-service:8082
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8084/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- muyu-net
|
||||||
|
- default
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: iloom-flatten/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-3015}:3015"
|
||||||
|
depends_on:
|
||||||
|
gateway:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
networks:
|
||||||
|
muyu-net:
|
||||||
|
external: true
|
||||||
|
name: deploy_default
|
||||||
150
docker-compose.yml
Normal file
150
docker-compose.yml
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# 使用方式:
|
||||||
|
# 独立模式(开发):docker compose up
|
||||||
|
# - 启动本地 MySQL,自动初始化 muyu + iloom 表
|
||||||
|
# - gRPC 服务需要另外启动 muyu-apiserver(cd ../muyu-apiserver/deploy && docker compose up)
|
||||||
|
#
|
||||||
|
# 集成模式(与 muyu-apiserver 联合):
|
||||||
|
# 1. cd ../muyu-apiserver/deploy && docker compose up
|
||||||
|
# 2. 设置环境变量指向 muyu 的服务:
|
||||||
|
# export DB_DSN="root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4"
|
||||||
|
# export MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001
|
||||||
|
# export MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002
|
||||||
|
# 3. docker compose --profile integrated up
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-muyu2026}
|
||||||
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-muyu_wms}
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "${MYSQL_PORT:-3306}:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- ../muyu-apiserver/deploy/mysql/init.sql:/docker-entrypoint-initdb.d/01-muyu.sql
|
||||||
|
- ./scripts/init-iloom.sql:/docker-entrypoint-initdb.d/02-iloom.sql
|
||||||
|
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-muyu2026}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
gateway:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: gateway/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${GATEWAY_PORT:-8080}:8080"
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026}
|
||||||
|
- AUTH_SERVICE_URL=http://auth-service:8081
|
||||||
|
- PURCHASER_SERVICE_URL=http://purchaser-service:8082
|
||||||
|
- TEXTILE_SERVICE_URL=http://textile-service:8083
|
||||||
|
- WASHING_SERVICE_URL=http://washing-service:8084
|
||||||
|
- ALLOW_ORIGINS=${ALLOW_ORIGINS:-http://localhost:3015}
|
||||||
|
depends_on:
|
||||||
|
auth-service:
|
||||||
|
condition: service_healthy
|
||||||
|
purchaser-service:
|
||||||
|
condition: service_healthy
|
||||||
|
textile-service:
|
||||||
|
condition: service_healthy
|
||||||
|
washing-service:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
auth-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: auth-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${AUTH_SERVICE_PORT:-8081}:8081"
|
||||||
|
environment:
|
||||||
|
- PORT=8081
|
||||||
|
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4}
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026}
|
||||||
|
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-muyu-wms-jwt-refresh-2026}
|
||||||
|
- MUYU_SYSTEM_RPC_ADDR=${MUYU_SYSTEM_RPC_ADDR:-system-rpc:9001}
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8081/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
purchaser-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: purchaser-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${PURCHASER_SERVICE_PORT:-8082}:8082"
|
||||||
|
environment:
|
||||||
|
- PORT=8082
|
||||||
|
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4}
|
||||||
|
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret}
|
||||||
|
- MUYU_INVENTORY_RPC_ADDR=${MUYU_INVENTORY_RPC_ADDR:-inventory-rpc:9002}
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8082/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
textile-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: textile-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${TEXTILE_SERVICE_PORT:-8083}:8083"
|
||||||
|
environment:
|
||||||
|
- PORT=8083
|
||||||
|
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4}
|
||||||
|
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret}
|
||||||
|
- PURCHASER_SERVICE_URL=http://purchaser-service:8082
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8083/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
washing-service:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: washing-service/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${WASHING_SERVICE_PORT:-8084}:8084"
|
||||||
|
environment:
|
||||||
|
- PORT=8084
|
||||||
|
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4}
|
||||||
|
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret}
|
||||||
|
- PURCHASER_SERVICE_URL=http://purchaser-service:8082
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:8084/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: iloom-flatten/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-3015}:3015"
|
||||||
|
depends_on:
|
||||||
|
gateway:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
1563
docs/architecture.html
Normal file
1563
docs/architecture.html
Normal file
File diff suppressed because it is too large
Load Diff
12
gateway/Dockerfile
Normal file
12
gateway/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY shared/ ./shared/
|
||||||
|
COPY gateway/ ./gateway/
|
||||||
|
WORKDIR /app/gateway
|
||||||
|
RUN go build -o /gateway ./cmd/
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache wget
|
||||||
|
COPY --from=builder /gateway /gateway
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["/gateway"]
|
||||||
71
gateway/cmd/main.go
Normal file
71
gateway/cmd/main.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/gateway/internal/config"
|
||||||
|
"github.com/muyuqingfeng/iloom/gateway/internal/middleware"
|
||||||
|
"github.com/muyuqingfeng/iloom/gateway/internal/proxy"
|
||||||
|
sharedMW "github.com/muyuqingfeng/iloom/shared/pkg/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(
|
||||||
|
sharedMW.RequestID(),
|
||||||
|
sharedMW.Logger(),
|
||||||
|
sharedMW.Recovery(),
|
||||||
|
sharedMW.CORS(cfg.AllowOrigins),
|
||||||
|
middleware.RateLimit(100),
|
||||||
|
)
|
||||||
|
|
||||||
|
r.GET("/health", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
|
|
||||||
|
v1 := r.Group("/api/v1")
|
||||||
|
{
|
||||||
|
// /api/v1/auth/* -> AUTH_SERVICE_URL (no auth required)
|
||||||
|
v1.Any("/auth/*path", proxy.NewReverseProxy(cfg.AuthServiceURL))
|
||||||
|
|
||||||
|
// /api/v1/common/* -> AUTH_SERVICE_URL (JWT required)
|
||||||
|
common := v1.Group("/common", middleware.AuthRequired(cfg.JWTSecret))
|
||||||
|
common.Any("/*path", proxy.NewReverseProxy(cfg.AuthServiceURL))
|
||||||
|
|
||||||
|
// /api/v1/purchaser/* -> PURCHASER_SERVICE_URL (JWT + role=purchaser)
|
||||||
|
purchaser := v1.Group("/purchaser",
|
||||||
|
middleware.AuthRequired(cfg.JWTSecret),
|
||||||
|
middleware.RoleRequired("purchaser"),
|
||||||
|
)
|
||||||
|
purchaser.Any("/*path", proxy.NewReverseProxy(cfg.PurchaserServiceURL))
|
||||||
|
|
||||||
|
// /api/v1/textile/* -> TEXTILE_SERVICE_URL (JWT + role=textile)
|
||||||
|
textile := v1.Group("/textile",
|
||||||
|
middleware.AuthRequired(cfg.JWTSecret),
|
||||||
|
middleware.RoleRequired("textile"),
|
||||||
|
)
|
||||||
|
textile.Any("/*path", proxy.NewReverseProxy(cfg.TextileServiceURL))
|
||||||
|
|
||||||
|
// /api/v1/washing/* -> WASHING_SERVICE_URL (JWT + role=washing)
|
||||||
|
washing := v1.Group("/washing",
|
||||||
|
middleware.AuthRequired(cfg.JWTSecret),
|
||||||
|
middleware.RoleRequired("washing"),
|
||||||
|
)
|
||||||
|
washing.Any("/*path", proxy.NewReverseProxy(cfg.WashingServiceURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket proxy — route by role
|
||||||
|
r.GET("/ws/v1/purchaser", proxy.NewWSProxy(cfg.PurchaserServiceURL))
|
||||||
|
r.GET("/ws/v1/textile", proxy.NewWSProxy(cfg.TextileServiceURL))
|
||||||
|
r.GET("/ws/v1/washing", proxy.NewWSProxy(cfg.WashingServiceURL))
|
||||||
|
|
||||||
|
addr := ":" + cfg.Port
|
||||||
|
log.Printf("gateway listening on %s", addr)
|
||||||
|
if err := r.Run(addr); err != nil {
|
||||||
|
log.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
gateway/go.mod
Normal file
47
gateway/go.mod
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
module github.com/muyuqingfeng/iloom/gateway
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require github.com/muyuqingfeng/iloom/shared v0.0.0
|
||||||
|
|
||||||
|
replace github.com/muyuqingfeng/iloom/shared => ../shared
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.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.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/kr/pretty v0.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-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
109
gateway/go.sum
Normal file
109
gateway/go.sum
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
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=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
35
gateway/internal/config/config.go
Normal file
35
gateway/internal/config/config.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
JWTSecret string
|
||||||
|
AuthServiceURL string
|
||||||
|
PurchaserServiceURL string
|
||||||
|
TextileServiceURL string
|
||||||
|
WashingServiceURL string
|
||||||
|
AllowOrigins []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Port: getEnv("PORT", "8080"),
|
||||||
|
JWTSecret: getEnv("JWT_SECRET", "muyu-wms-jwt-secret-key-2026"),
|
||||||
|
AuthServiceURL: getEnv("AUTH_SERVICE_URL", "http://localhost:8081"),
|
||||||
|
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
|
||||||
|
TextileServiceURL: getEnv("TEXTILE_SERVICE_URL", "http://localhost:8083"),
|
||||||
|
WashingServiceURL: getEnv("WASHING_SERVICE_URL", "http://localhost:8084"),
|
||||||
|
AllowOrigins: strings.Split(getEnv("ALLOW_ORIGINS", "*"), ","),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
65
gateway/internal/middleware/auth.go
Normal file
65
gateway/internal/middleware/auth.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/auth"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AuthRequired(jwtSecret string) gin.HandlerFunc {
|
||||||
|
secret := []byte(jwtSecret)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
header := c.GetHeader("Authorization")
|
||||||
|
if header == "" {
|
||||||
|
response.Unauthorized(c, "missing authorization header")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(header, " ", 2)
|
||||||
|
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
|
||||||
|
response.Unauthorized(c, "invalid authorization format")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := parts[1]
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, &auth.Claims{}, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 401, response.ErrCodeTokenExpired, "token expired or invalid")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*auth.Claims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
response.Error(c, 401, response.ErrCodeTokenInvalid, "invalid token")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("user_id", claims.UserID)
|
||||||
|
c.Set("company_id", claims.CompanyID)
|
||||||
|
c.Set("role", claims.Role)
|
||||||
|
c.Set("is_master", claims.IsMaster)
|
||||||
|
c.Set("username", claims.Username)
|
||||||
|
|
||||||
|
c.Request.Header.Set("X-User-ID", claims.UserID)
|
||||||
|
c.Request.Header.Set("X-Company-ID", claims.CompanyID)
|
||||||
|
c.Request.Header.Set("X-User-Role", claims.Role)
|
||||||
|
c.Request.Header.Set("X-Is-Master", fmt.Sprintf("%t", claims.IsMaster))
|
||||||
|
c.Request.Header.Set("X-Username", claims.Username)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
54
gateway/internal/middleware/ratelimit.go
Normal file
54
gateway/internal/middleware/ratelimit.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bucket struct {
|
||||||
|
tokens float64
|
||||||
|
lastCheck time.Time
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var buckets sync.Map
|
||||||
|
|
||||||
|
func RateLimit(rps int) gin.HandlerFunc {
|
||||||
|
rate := float64(rps)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ip := c.ClientIP()
|
||||||
|
|
||||||
|
val, _ := buckets.LoadOrStore(ip, &bucket{
|
||||||
|
tokens: rate,
|
||||||
|
lastCheck: time.Now(),
|
||||||
|
})
|
||||||
|
b := val.(*bucket)
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
elapsed := now.Sub(b.lastCheck).Seconds()
|
||||||
|
b.lastCheck = now
|
||||||
|
b.tokens += elapsed * rate
|
||||||
|
if b.tokens > rate {
|
||||||
|
b.tokens = rate
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.tokens < 1 {
|
||||||
|
b.mu.Unlock()
|
||||||
|
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"code": 42900,
|
||||||
|
"message": "rate limit exceeded",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.tokens--
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
31
gateway/internal/middleware/role_check.go
Normal file
31
gateway/internal/middleware/role_check.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RoleRequired(roles ...string) gin.HandlerFunc {
|
||||||
|
allowed := make(map[string]bool, len(roles))
|
||||||
|
for _, r := range roles {
|
||||||
|
allowed[r] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
role, exists := c.Get("role")
|
||||||
|
if !exists {
|
||||||
|
response.Forbidden(c, "role not found in context")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roleStr, ok := role.(string)
|
||||||
|
if !ok || !allowed[roleStr] {
|
||||||
|
response.Error(c, 403, response.ErrCodeRoleMismatch, "insufficient role permission")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
34
gateway/internal/proxy/reverse_proxy.go
Normal file
34
gateway/internal/proxy/reverse_proxy.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewReverseProxy(target string) gin.HandlerFunc {
|
||||||
|
targetURL, err := url.Parse(target)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid proxy target URL: %s, err: %v", target, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := &httputil.ReverseProxy{
|
||||||
|
Director: func(req *http.Request) {
|
||||||
|
req.URL.Scheme = targetURL.Scheme
|
||||||
|
req.URL.Host = targetURL.Host
|
||||||
|
req.Host = targetURL.Host
|
||||||
|
},
|
||||||
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
log.Printf("[proxy] error: %v", err)
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
_, _ = w.Write([]byte(`{"code":50200,"message":"service unavailable"}`))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
proxy.ServeHTTP(c.Writer, c.Request)
|
||||||
|
}
|
||||||
|
}
|
||||||
94
gateway/internal/proxy/ws_proxy.go
Normal file
94
gateway/internal/proxy/ws_proxy.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWSProxy(targetURL string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ws_proxy] upgrade error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
backendURL, err := buildWSURL(targetURL, c.Request)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ws_proxy] url parse error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := http.Header{}
|
||||||
|
for _, key := range []string{"X-User-ID", "X-Company-ID", "X-User-Role"} {
|
||||||
|
if v := c.Request.Header.Get(key); v != "" {
|
||||||
|
header.Set(key, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backendConn, _, err := websocket.DefaultDialer.Dial(backendURL, header)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ws_proxy] dial backend error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer backendConn.Close()
|
||||||
|
|
||||||
|
errCh := make(chan error, 2)
|
||||||
|
|
||||||
|
go pumpMessages(clientConn, backendConn, errCh)
|
||||||
|
go pumpMessages(backendConn, clientConn, errCh)
|
||||||
|
|
||||||
|
<-errCh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pumpMessages(src, dst *websocket.Conn, errCh chan<- error) {
|
||||||
|
for {
|
||||||
|
msgType, reader, err := src.NextReader()
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer, err := dst.NextWriter(msgType)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(writer, reader); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWSURL(target string, req *http.Request) (string, error) {
|
||||||
|
u, err := url.Parse(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := "ws"
|
||||||
|
if strings.HasPrefix(u.Scheme, "https") {
|
||||||
|
scheme = "wss"
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Scheme = scheme
|
||||||
|
u.Path = req.URL.Path
|
||||||
|
u.RawQuery = req.URL.RawQuery
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
12
go.work
Normal file
12
go.work
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
use (
|
||||||
|
./auth-service
|
||||||
|
./gateway
|
||||||
|
./purchaser-service
|
||||||
|
./shared
|
||||||
|
./textile-service
|
||||||
|
./washing-service
|
||||||
|
)
|
||||||
26
go.work.sum
Normal file
26
go.work.sum
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||||
|
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
|
||||||
|
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||||
|
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||||
|
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||||
|
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||||
|
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
1
iloom-flatten
Submodule
1
iloom-flatten
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 3093ae18c25f6d654e1c0c3a6cd546c04a5438d1
|
||||||
4
k8s/00-namespace.yaml
Normal file
4
k8s/00-namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: iloom
|
||||||
44
k8s/02-configmaps/frontend-nginx.yaml
Normal file
44
k8s/02-configmaps/frontend-nginx.yaml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: iloom-frontend-nginx
|
||||||
|
namespace: iloom
|
||||||
|
data:
|
||||||
|
default.conf: |
|
||||||
|
server {
|
||||||
|
listen 3015;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||||
|
gzip_min_length 256;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://iloom-gateway:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws/ {
|
||||||
|
proxy_pass http://iloom-gateway:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
83
k8s/02-configmaps/muyu-configs.yaml
Normal file
83
k8s/02-configmaps/muyu-configs.yaml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: muyu-system-config
|
||||||
|
namespace: iloom
|
||||||
|
data:
|
||||||
|
system.yaml: |
|
||||||
|
Name: system.rpc
|
||||||
|
ListenOn: 0.0.0.0:9001
|
||||||
|
|
||||||
|
Etcd:
|
||||||
|
Hosts:
|
||||||
|
- etcd:2379
|
||||||
|
Key: system.rpc
|
||||||
|
|
||||||
|
DataSource: root:muyu2026@tcp(mysql:3306)/muyu_wms?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||||
|
|
||||||
|
Cache:
|
||||||
|
- Host: redis:6379
|
||||||
|
|
||||||
|
Log:
|
||||||
|
ServiceName: system-rpc
|
||||||
|
Mode: console
|
||||||
|
Level: info
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: muyu-inventory-config
|
||||||
|
namespace: iloom
|
||||||
|
data:
|
||||||
|
inventory.yaml: |
|
||||||
|
Name: inventory.rpc
|
||||||
|
ListenOn: 0.0.0.0:9002
|
||||||
|
|
||||||
|
Etcd:
|
||||||
|
Hosts:
|
||||||
|
- etcd:2379
|
||||||
|
Key: inventory.rpc
|
||||||
|
|
||||||
|
DataSource: root:muyu2026@tcp(mysql:3306)/muyu_wms?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||||
|
|
||||||
|
Cache:
|
||||||
|
- Host: redis:6379
|
||||||
|
|
||||||
|
Log:
|
||||||
|
ServiceName: inventory-rpc
|
||||||
|
Mode: console
|
||||||
|
Level: info
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: muyu-gateway-config
|
||||||
|
namespace: iloom
|
||||||
|
data:
|
||||||
|
gateway.yaml: |
|
||||||
|
Name: gateway-api
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8888
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: muyu-wms-jwt-secret-key-2026
|
||||||
|
AccessExpire: 7200
|
||||||
|
|
||||||
|
SystemRpc:
|
||||||
|
Etcd:
|
||||||
|
Hosts:
|
||||||
|
- etcd:2379
|
||||||
|
Key: system.rpc
|
||||||
|
NonBlock: true
|
||||||
|
|
||||||
|
InventoryRpc:
|
||||||
|
Etcd:
|
||||||
|
Hosts:
|
||||||
|
- etcd:2379
|
||||||
|
Key: inventory.rpc
|
||||||
|
NonBlock: true
|
||||||
|
|
||||||
|
Log:
|
||||||
|
ServiceName: gateway-api
|
||||||
|
Mode: console
|
||||||
|
Level: info
|
||||||
89
k8s/10-infra/etcd.yaml
Normal file
89
k8s/10-infra/etcd.yaml
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: etcd
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
ports:
|
||||||
|
- name: client
|
||||||
|
port: 2379
|
||||||
|
targetPort: 2379
|
||||||
|
- name: peer
|
||||||
|
port: 2380
|
||||||
|
targetPort: 2380
|
||||||
|
selector:
|
||||||
|
app: etcd
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: etcd
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
serviceName: etcd
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: etcd
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: etcd
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
containers:
|
||||||
|
- name: etcd
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/etcd:v3.5.17
|
||||||
|
ports:
|
||||||
|
- containerPort: 2379
|
||||||
|
name: client
|
||||||
|
- containerPort: 2380
|
||||||
|
name: peer
|
||||||
|
env:
|
||||||
|
- name: ETCD_NAME
|
||||||
|
value: "etcd-0"
|
||||||
|
- name: ETCD_DATA_DIR
|
||||||
|
value: "/etcd-data"
|
||||||
|
- name: ETCD_LISTEN_CLIENT_URLS
|
||||||
|
value: "http://0.0.0.0:2379"
|
||||||
|
- name: ETCD_ADVERTISE_CLIENT_URLS
|
||||||
|
value: "http://etcd:2379"
|
||||||
|
- name: ETCD_LISTEN_PEER_URLS
|
||||||
|
value: "http://0.0.0.0:2380"
|
||||||
|
- name: ETCD_INITIAL_ADVERTISE_PEER_URLS
|
||||||
|
value: "http://etcd-0.etcd:2380"
|
||||||
|
- name: ETCD_INITIAL_CLUSTER
|
||||||
|
value: "etcd-0=http://etcd-0.etcd:2380"
|
||||||
|
- name: ETCD_INITIAL_CLUSTER_STATE
|
||||||
|
value: "new"
|
||||||
|
volumeMounts:
|
||||||
|
- name: etcd-data
|
||||||
|
mountPath: /etcd-data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["etcdctl", "endpoint", "health", "--endpoints=http://localhost:2379"]
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["etcdctl", "endpoint", "health", "--endpoints=http://localhost:2379"]
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: etcd-data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
87
k8s/10-infra/mysql.yaml
Normal file
87
k8s/10-infra/mysql.yaml
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: mysql
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
ports:
|
||||||
|
- port: 3306
|
||||||
|
targetPort: 3306
|
||||||
|
selector:
|
||||||
|
app: mysql
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: mysql
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
serviceName: mysql
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: mysql
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mysql
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
containers:
|
||||||
|
- name: mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/mysql:8.0
|
||||||
|
ports:
|
||||||
|
- containerPort: 3306
|
||||||
|
env:
|
||||||
|
- name: MYSQL_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: db-secret
|
||||||
|
key: MYSQL_ROOT_PASSWORD
|
||||||
|
- name: MYSQL_DATABASE
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: db-secret
|
||||||
|
key: MYSQL_DATABASE
|
||||||
|
- name: TZ
|
||||||
|
value: Asia/Shanghai
|
||||||
|
args:
|
||||||
|
- --character-set-server=utf8mb4
|
||||||
|
- --collation-server=utf8mb4_unicode_ci
|
||||||
|
volumeMounts:
|
||||||
|
- name: mysql-data
|
||||||
|
mountPath: /var/lib/mysql
|
||||||
|
- name: init-scripts
|
||||||
|
mountPath: /docker-entrypoint-initdb.d
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 30
|
||||||
|
volumes:
|
||||||
|
- name: init-scripts
|
||||||
|
configMap:
|
||||||
|
name: mysql-init-scripts
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: mysql-data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
66
k8s/10-infra/redis.yaml
Normal file
66
k8s/10-infra/redis.yaml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
ports:
|
||||||
|
- port: 6379
|
||||||
|
targetPort: 6379
|
||||||
|
selector:
|
||||||
|
app: redis
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
serviceName: redis
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: redis
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: redis
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: redis-data
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["redis-cli", "ping"]
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["redis-cli", "ping"]
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 30
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: redis-data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 2Gi
|
||||||
66
k8s/20-muyu/gateway.yaml
Normal file
66
k8s/20-muyu/gateway.yaml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: muyu-gateway
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8888
|
||||||
|
targetPort: 8888
|
||||||
|
selector:
|
||||||
|
app: muyu-gateway
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: muyu-gateway
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: muyu-gateway
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: muyu-gateway
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-etcd
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z etcd 2379; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: gateway
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/muyu-gateway:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8888
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: Asia/Shanghai
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /app/etc
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8888
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 8888
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: muyu-gateway-config
|
||||||
72
k8s/20-muyu/inventory-rpc.yaml
Normal file
72
k8s/20-muyu/inventory-rpc.yaml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: muyu-inventory-rpc
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 9002
|
||||||
|
targetPort: 9002
|
||||||
|
selector:
|
||||||
|
app: muyu-inventory-rpc
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: muyu-inventory-rpc
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: muyu-inventory-rpc
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: muyu-inventory-rpc
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z mysql 3306; do sleep 2; done']
|
||||||
|
- name: wait-etcd
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z etcd 2379; do sleep 2; done']
|
||||||
|
- name: wait-redis
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z redis 6379; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: inventory-rpc
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/muyu-inventory-rpc:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 9002
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: Asia/Shanghai
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /app/etc
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 9002
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 9002
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: muyu-inventory-config
|
||||||
72
k8s/20-muyu/system-rpc.yaml
Normal file
72
k8s/20-muyu/system-rpc.yaml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: muyu-system-rpc
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 9001
|
||||||
|
targetPort: 9001
|
||||||
|
selector:
|
||||||
|
app: muyu-system-rpc
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: muyu-system-rpc
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: muyu-system-rpc
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: muyu-system-rpc
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z mysql 3306; do sleep 2; done']
|
||||||
|
- name: wait-etcd
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z etcd 2379; do sleep 2; done']
|
||||||
|
- name: wait-redis
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z redis 6379; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: system-rpc
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/muyu-system-rpc:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 9001
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: Asia/Shanghai
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /app/etc
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 9001
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 9001
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: muyu-system-config
|
||||||
81
k8s/30-iloom/auth-service.yaml
Normal file
81
k8s/30-iloom/auth-service.yaml
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iloom-auth-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8081
|
||||||
|
targetPort: 8081
|
||||||
|
selector:
|
||||||
|
app: iloom-auth-service
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iloom-auth-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iloom-auth-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iloom-auth-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z mysql 3306; do sleep 2; done']
|
||||||
|
- name: wait-muyu-system
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z muyu-system-rpc 9001; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: auth-service
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/iloom-auth-service:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8081
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "8081"
|
||||||
|
- name: DB_DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: db-secret
|
||||||
|
key: DB_DSN
|
||||||
|
- name: JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: app-secret
|
||||||
|
key: JWT_SECRET
|
||||||
|
- name: JWT_REFRESH_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: app-secret
|
||||||
|
key: JWT_REFRESH_SECRET
|
||||||
|
- name: MUYU_SYSTEM_RPC_ADDR
|
||||||
|
value: "muyu-system-rpc:9001"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
61
k8s/30-iloom/frontend.yaml
Normal file
61
k8s/30-iloom/frontend.yaml
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iloom-frontend
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 3015
|
||||||
|
targetPort: 3015
|
||||||
|
selector:
|
||||||
|
app: iloom-frontend
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iloom-frontend
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iloom-frontend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iloom-frontend
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
containers:
|
||||||
|
- name: frontend
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/iloom-frontend:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 3015
|
||||||
|
volumeMounts:
|
||||||
|
- name: nginx-config
|
||||||
|
mountPath: /etc/nginx/conf.d
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 3015
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 3015
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
volumes:
|
||||||
|
- name: nginx-config
|
||||||
|
configMap:
|
||||||
|
name: iloom-frontend-nginx
|
||||||
85
k8s/30-iloom/gateway.yaml
Normal file
85
k8s/30-iloom/gateway.yaml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iloom-gateway
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
selector:
|
||||||
|
app: iloom-gateway
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iloom-gateway
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iloom-gateway
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iloom-gateway
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-auth
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z iloom-auth-service 8081; do sleep 2; done']
|
||||||
|
- name: wait-purchaser
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z iloom-purchaser-service 8082; do sleep 2; done']
|
||||||
|
- name: wait-textile
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z iloom-textile-service 8083; do sleep 2; done']
|
||||||
|
- name: wait-washing
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z iloom-washing-service 8084; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: gateway
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/iloom-gateway:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "8080"
|
||||||
|
- name: JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: app-secret
|
||||||
|
key: JWT_SECRET
|
||||||
|
- name: AUTH_SERVICE_URL
|
||||||
|
value: "http://iloom-auth-service:8081"
|
||||||
|
- name: PURCHASER_SERVICE_URL
|
||||||
|
value: "http://iloom-purchaser-service:8082"
|
||||||
|
- name: TEXTILE_SERVICE_URL
|
||||||
|
value: "http://iloom-textile-service:8083"
|
||||||
|
- name: WASHING_SERVICE_URL
|
||||||
|
value: "http://iloom-washing-service:8084"
|
||||||
|
- name: ALLOW_ORIGINS
|
||||||
|
value: "https://iloom.cheverjohn.me"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
76
k8s/30-iloom/purchaser-service.yaml
Normal file
76
k8s/30-iloom/purchaser-service.yaml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iloom-purchaser-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8082
|
||||||
|
targetPort: 8082
|
||||||
|
selector:
|
||||||
|
app: iloom-purchaser-service
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iloom-purchaser-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iloom-purchaser-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iloom-purchaser-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z mysql 3306; do sleep 2; done']
|
||||||
|
- name: wait-muyu-inventory
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z muyu-inventory-rpc 9002; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: purchaser-service
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/iloom-purchaser-service:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8082
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "8082"
|
||||||
|
- name: DB_DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: db-secret
|
||||||
|
key: DB_DSN
|
||||||
|
- name: INTERNAL_SERVICE_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: app-secret
|
||||||
|
key: INTERNAL_SERVICE_KEY
|
||||||
|
- name: MUYU_INVENTORY_RPC_ADDR
|
||||||
|
value: "muyu-inventory-rpc:9002"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8082
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8082
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
73
k8s/30-iloom/textile-service.yaml
Normal file
73
k8s/30-iloom/textile-service.yaml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iloom-textile-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8083
|
||||||
|
targetPort: 8083
|
||||||
|
selector:
|
||||||
|
app: iloom-textile-service
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iloom-textile-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iloom-textile-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iloom-textile-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z mysql 3306; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: textile-service
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/iloom-textile-service:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8083
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "8083"
|
||||||
|
- name: DB_DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: db-secret
|
||||||
|
key: DB_DSN
|
||||||
|
- name: INTERNAL_SERVICE_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: app-secret
|
||||||
|
key: INTERNAL_SERVICE_KEY
|
||||||
|
- name: PURCHASER_SERVICE_URL
|
||||||
|
value: "http://iloom-purchaser-service:8082"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8083
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8083
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
73
k8s/30-iloom/washing-service.yaml
Normal file
73
k8s/30-iloom/washing-service.yaml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: iloom-washing-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8084
|
||||||
|
targetPort: 8084
|
||||||
|
selector:
|
||||||
|
app: iloom-washing-service
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: iloom-washing-service
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: iloom-washing-service
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: iloom-washing-service
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: harbor-pull-secret
|
||||||
|
initContainers:
|
||||||
|
- name: wait-mysql
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/busybox:1.36
|
||||||
|
command: ['sh', '-c', 'until nc -z mysql 3306; do sleep 2; done']
|
||||||
|
containers:
|
||||||
|
- name: washing-service
|
||||||
|
image: harbor-in-k3s.cheverjohn.me/iloom/iloom-washing-service:v1
|
||||||
|
ports:
|
||||||
|
- containerPort: 8084
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "8084"
|
||||||
|
- name: DB_DSN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: db-secret
|
||||||
|
key: DB_DSN
|
||||||
|
- name: INTERNAL_SERVICE_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: app-secret
|
||||||
|
key: INTERNAL_SERVICE_KEY
|
||||||
|
- name: PURCHASER_SERVICE_URL
|
||||||
|
value: "http://iloom-purchaser-service:8082"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8084
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 30
|
||||||
33
k8s/40-ingress.yaml
Normal file
33
k8s/40-ingress.yaml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: iloom-frontend
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`iloom.cheverjohn.me`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: iloom-frontend
|
||||||
|
port: 3015
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: muyu-gateway
|
||||||
|
namespace: iloom
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`muyu-api.cheverjohn.me`)
|
||||||
|
kind: Rule
|
||||||
|
services:
|
||||||
|
- name: muyu-gateway
|
||||||
|
port: 8888
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
12
purchaser-service/Dockerfile
Normal file
12
purchaser-service/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY shared/ ./shared/
|
||||||
|
COPY purchaser-service/ ./purchaser-service/
|
||||||
|
WORKDIR /app/purchaser-service
|
||||||
|
RUN go build -o /purchaser-service ./cmd/
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache wget
|
||||||
|
COPY --from=builder /purchaser-service /purchaser-service
|
||||||
|
EXPOSE 8082
|
||||||
|
CMD ["/purchaser-service"]
|
||||||
102
purchaser-service/cmd/main.go
Normal file
102
purchaser-service/cmd/main.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/config"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/handler"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/database"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/middleware"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
db, err := database.NewDBFromDSN(cfg.DBDSN)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
wsHub := websocket.NewHub()
|
||||||
|
go wsHub.Run()
|
||||||
|
|
||||||
|
planRepo := repository.NewPlanRepo(db)
|
||||||
|
productRepo := repository.NewProductRepo(db)
|
||||||
|
apRepo := repository.NewAPRepo(db)
|
||||||
|
factoryRepo := repository.NewFactoryRepo(db)
|
||||||
|
washingPlanRepo := repository.NewWashingPlanRepo(db)
|
||||||
|
|
||||||
|
planSvc := service.NewPlanService(planRepo, db)
|
||||||
|
productSvc := service.NewProductService(productRepo, db)
|
||||||
|
apSvc := service.NewAPService(apRepo, db)
|
||||||
|
factorySvc := service.NewFactoryService(factoryRepo)
|
||||||
|
washingPlanSvc := service.NewWashingPlanService(washingPlanRepo)
|
||||||
|
dashboardSvc := service.NewDashboardService(planRepo, productRepo, apRepo)
|
||||||
|
|
||||||
|
planHandler := handler.NewPlanHandler(planSvc, wsHub)
|
||||||
|
productHandler := handler.NewProductHandler(productSvc, wsHub)
|
||||||
|
apHandler := handler.NewAPHandler(apSvc)
|
||||||
|
factoryHandler := handler.NewFactoryHandler(factorySvc)
|
||||||
|
washingPlanHandler := handler.NewWashingPlanHandler(washingPlanSvc)
|
||||||
|
dashboardHandler := handler.NewDashboardHandler(dashboardSvc)
|
||||||
|
|
||||||
|
if os.Getenv("GIN_MODE") == "" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(middleware.Recovery(), middleware.Logger(), middleware.RequestID())
|
||||||
|
|
||||||
|
r.GET("/health", func(c *gin.Context) {
|
||||||
|
response.OK(c, gin.H{"status": "ok", "service": "purchaser-service"})
|
||||||
|
})
|
||||||
|
|
||||||
|
api := r.Group("/api/v1/purchaser")
|
||||||
|
{
|
||||||
|
api.GET("/dashboard/stats", dashboardHandler.Stats)
|
||||||
|
|
||||||
|
api.GET("/plans", planHandler.List)
|
||||||
|
api.POST("/plans", planHandler.Create)
|
||||||
|
api.GET("/plans/:id", planHandler.Get)
|
||||||
|
api.PUT("/plans/:id", planHandler.Update)
|
||||||
|
api.DELETE("/plans/:id", planHandler.Delete)
|
||||||
|
api.POST("/plans/:id/factories", planHandler.AssignFactory)
|
||||||
|
api.GET("/plans/:id/steps", planHandler.GetSteps)
|
||||||
|
api.GET("/plans/:id/inventory", planHandler.GetInventory)
|
||||||
|
|
||||||
|
api.GET("/products", productHandler.List)
|
||||||
|
api.POST("/products", productHandler.Create)
|
||||||
|
api.PUT("/products/:id", productHandler.Update)
|
||||||
|
api.DELETE("/products/:id", productHandler.Delete)
|
||||||
|
api.POST("/products/:id/inbound", productHandler.Inbound)
|
||||||
|
api.POST("/products/:id/outbound", productHandler.Outbound)
|
||||||
|
api.GET("/products/:id/records", productHandler.Records)
|
||||||
|
api.PUT("/products/:id/price", productHandler.UpdatePrice)
|
||||||
|
api.GET("/products/:id/price-history", productHandler.PriceHistory)
|
||||||
|
|
||||||
|
api.GET("/factories", factoryHandler.List)
|
||||||
|
|
||||||
|
api.GET("/accounts-payable", apHandler.List)
|
||||||
|
api.POST("/accounts-payable/:id/pay", apHandler.Pay)
|
||||||
|
|
||||||
|
api.POST("/washing-plans", washingPlanHandler.Create)
|
||||||
|
api.GET("/washing-plans", washingPlanHandler.List)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/ws/v1/purchaser", handler.HandleWebSocket(wsHub))
|
||||||
|
|
||||||
|
port := cfg.Port
|
||||||
|
if port == "" {
|
||||||
|
port = "8082"
|
||||||
|
}
|
||||||
|
log.Printf("purchaser-service listening on :%s", port)
|
||||||
|
if err := r.Run(":" + port); err != nil {
|
||||||
|
log.Fatalf("failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
purchaser-service/go.mod
Normal file
45
purchaser-service/go.mod
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
module github.com/muyuqingfeng/iloom/purchaser-service
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/muyuqingfeng/iloom/shared v0.0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.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.20.0 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // 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-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/muyuqingfeng/iloom/shared => ../shared
|
||||||
97
purchaser-service/go.sum
Normal file
97
purchaser-service/go.sum
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
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/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
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/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
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=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
26
purchaser-service/internal/config/config.go
Normal file
26
purchaser-service/internal/config/config.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
DBDSN string
|
||||||
|
InternalServiceKey string
|
||||||
|
InventoryRPCAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Port: getEnv("PORT", "8082"),
|
||||||
|
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"),
|
||||||
|
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
|
||||||
|
InventoryRPCAddr: getEnv("MUYU_INVENTORY_RPC_ADDR", "127.0.0.1:9002"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
62
purchaser-service/internal/handler/accounts_payable.go
Normal file
62
purchaser-service/internal/handler/accounts_payable.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APHandler struct {
|
||||||
|
svc *service.APService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPHandler(svc *service.APService) *APHandler {
|
||||||
|
return &APHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayRequest struct {
|
||||||
|
Amount float64 `json:"amount" binding:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APHandler) List(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := h.svc.List(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("list accounts payable error: %v", err)
|
||||||
|
response.InternalError(c, "failed to list accounts payable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *APHandler) Pay(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apID := c.Param("id")
|
||||||
|
|
||||||
|
var req PayRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.Pay(c.Request.Context(), apID, companyID, req.Amount); err != nil {
|
||||||
|
log.Printf("pay error: %v", err)
|
||||||
|
response.InternalError(c, "failed to process payment")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
34
purchaser-service/internal/handler/dashboard.go
Normal file
34
purchaser-service/internal/handler/dashboard.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardHandler struct {
|
||||||
|
svc *service.DashboardService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboardHandler(svc *service.DashboardService) *DashboardHandler {
|
||||||
|
return &DashboardHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DashboardHandler) Stats(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := h.svc.Stats(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get dashboard stats error: %v", err)
|
||||||
|
response.InternalError(c, "failed to get stats")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, stats)
|
||||||
|
}
|
||||||
34
purchaser-service/internal/handler/factory.go
Normal file
34
purchaser-service/internal/handler/factory.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FactoryHandler struct {
|
||||||
|
svc *service.FactoryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactoryHandler(svc *service.FactoryService) *FactoryHandler {
|
||||||
|
return &FactoryHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FactoryHandler) List(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
factories, err := h.svc.List(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("list factories error: %v", err)
|
||||||
|
response.InternalError(c, "failed to list factories")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, factories)
|
||||||
|
}
|
||||||
274
purchaser-service/internal/handler/plan.go
Normal file
274
purchaser-service/internal/handler/plan.go
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/types"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlanHandler struct {
|
||||||
|
svc *service.PlanService
|
||||||
|
wsHub *websocket.Hub
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlanHandler(svc *service.PlanService, wsHub *websocket.Hub) *PlanHandler {
|
||||||
|
return &PlanHandler{svc: svc, wsHub: wsHub}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePlanReq struct {
|
||||||
|
ProductName string `json:"product_name" binding:"required"`
|
||||||
|
FabricCode string `json:"fabric_code" binding:"required"`
|
||||||
|
Color string `json:"color" binding:"required"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
TargetQuantity float64 `json:"target_quantity" binding:"required,gt=0"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnUsagePerMeter *float64 `json:"yarn_usage_per_meter"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
YarnRatios []CreateYarnRatioReq `json:"yarn_ratios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateYarnRatioReq struct {
|
||||||
|
YarnName string `json:"yarn_name" binding:"required"`
|
||||||
|
YarnType string `json:"yarn_type"`
|
||||||
|
Ratio float64 `json:"ratio" binding:"required"`
|
||||||
|
AmountPerMeter float64 `json:"amount_per_meter"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePlanReq struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
TargetQuantity float64 `json:"target_quantity"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnUsagePerMeter *float64 `json:"yarn_usage_per_meter"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignFactoryReq struct {
|
||||||
|
FactoryID string `json:"factory_id" binding:"required"`
|
||||||
|
FactoryType string `json:"factory_type" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) List(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var page types.Pagination
|
||||||
|
if err := c.ShouldBindQuery(&page); err != nil {
|
||||||
|
response.BadRequest(c, "invalid pagination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.Normalize()
|
||||||
|
|
||||||
|
status := c.Query("status")
|
||||||
|
|
||||||
|
plans, total, err := h.svc.List(c.Request.Context(), companyID, page, status)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("list plans error: %v", err)
|
||||||
|
response.InternalError(c, "failed to list plans")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OKWithMeta(c, plans, &response.Meta{
|
||||||
|
Page: page.Page,
|
||||||
|
PageSize: page.PageSize,
|
||||||
|
Total: total,
|
||||||
|
HasMore: page.Page*page.PageSize < total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) Create(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if companyID == "" || userID == "" {
|
||||||
|
response.BadRequest(c, "missing user or company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreatePlanReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.CreatePlanRequest{
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
TargetQuantity: req.TargetQuantity,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
Remark: req.Remark,
|
||||||
|
}
|
||||||
|
for _, yr := range req.YarnRatios {
|
||||||
|
svcReq.YarnRatios = append(svcReq.YarnRatios, service.CreateYarnRatio{
|
||||||
|
YarnName: yr.YarnName,
|
||||||
|
YarnType: yr.YarnType,
|
||||||
|
Ratio: yr.Ratio,
|
||||||
|
AmountPerMeter: yr.AmountPerMeter,
|
||||||
|
TotalAmount: yr.TotalAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := h.svc.Create(c.Request.Context(), companyID, userID, svcReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("create plan error: %v", err)
|
||||||
|
response.InternalError(c, "failed to create plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "production_plans",
|
||||||
|
Action: "INSERT",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, response.Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: plan,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) Get(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
plan, err := h.svc.GetByID(c.Request.Context(), id, companyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get plan error: %v", err)
|
||||||
|
response.NotFound(c, "plan not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) Update(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var req UpdatePlanReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.UpdatePlanRequest{
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
TargetQuantity: req.TargetQuantity,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
Status: req.Status,
|
||||||
|
Remark: req.Remark,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.Update(c.Request.Context(), id, companyID, svcReq); err != nil {
|
||||||
|
log.Printf("update plan error: %v", err)
|
||||||
|
response.InternalError(c, "failed to update plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "production_plans",
|
||||||
|
Action: "UPDATE",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) Delete(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.svc.Delete(c.Request.Context(), id, companyID); err != nil {
|
||||||
|
log.Printf("delete plan error: %v", err)
|
||||||
|
response.InternalError(c, "failed to delete plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "production_plans",
|
||||||
|
Action: "DELETE",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) AssignFactory(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
planID := c.Param("id")
|
||||||
|
|
||||||
|
var req AssignFactoryReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.AssignFactory(c.Request.Context(), planID, req.FactoryID, req.FactoryType); err != nil {
|
||||||
|
log.Printf("assign factory error: %v", err)
|
||||||
|
response.InternalError(c, "failed to assign factory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "plan_factories",
|
||||||
|
Action: "INSERT",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) GetSteps(c *gin.Context) {
|
||||||
|
planID := c.Param("id")
|
||||||
|
steps, err := h.svc.GetSteps(c.Request.Context(), planID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get steps error: %v", err)
|
||||||
|
response.InternalError(c, "failed to get steps")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PlanHandler) GetInventory(c *gin.Context) {
|
||||||
|
planID := c.Param("id")
|
||||||
|
records, err := h.svc.GetInventory(c.Request.Context(), planID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get inventory error: %v", err)
|
||||||
|
response.InternalError(c, "failed to get inventory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, records)
|
||||||
|
}
|
||||||
319
purchaser-service/internal/handler/product.go
Normal file
319
purchaser-service/internal/handler/product.go
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductHandler struct {
|
||||||
|
svc *service.ProductService
|
||||||
|
wsHub *websocket.Hub
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductHandler(svc *service.ProductService, wsHub *websocket.Hub) *ProductHandler {
|
||||||
|
return &ProductHandler{svc: svc, wsHub: wsHub}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProductReq struct {
|
||||||
|
ProductName string `json:"product_name" binding:"required"`
|
||||||
|
FabricCode string `json:"fabric_code" binding:"required"`
|
||||||
|
Color string `json:"color" binding:"required"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnTypes []string `json:"yarn_types"`
|
||||||
|
YarnRatios []float64 `json:"yarn_ratios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductReq struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnTypes []string `json:"yarn_types"`
|
||||||
|
YarnRatios []float64 `json:"yarn_ratios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboundReq struct {
|
||||||
|
BatchNo string `json:"batch_no" binding:"required"`
|
||||||
|
Rolls int `json:"rolls" binding:"required,gt=0"`
|
||||||
|
Meters float64 `json:"meters" binding:"required,gt=0"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
WarehouseID string `json:"warehouse_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundReq struct {
|
||||||
|
BatchNo string `json:"batch_no"`
|
||||||
|
Rolls int `json:"rolls" binding:"required,gt=0"`
|
||||||
|
Meters float64 `json:"meters" binding:"required,gt=0"`
|
||||||
|
OutboundType string `json:"outbound_type"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
RecipientCompanyID string `json:"recipient_company_id"`
|
||||||
|
RecipientCompanyName string `json:"recipient_company_name"`
|
||||||
|
SourceBatchID string `json:"source_batch_id"`
|
||||||
|
WashingPlanID string `json:"washing_plan_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePriceReq struct {
|
||||||
|
NewPrice float64 `json:"new_price" binding:"required,gt=0"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) List(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
products, err := h.svc.List(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("list products error: %v", err)
|
||||||
|
response.InternalError(c, "failed to list products")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, products)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Create(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateProductReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.CreateProductRequest{
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
Weight: req.Weight,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
YarnTypes: req.YarnTypes,
|
||||||
|
YarnRatios: req.YarnRatios,
|
||||||
|
}
|
||||||
|
|
||||||
|
product, err := h.svc.Create(c.Request.Context(), companyID, svcReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("create product error: %v", err)
|
||||||
|
response.InternalError(c, "failed to create product")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "products",
|
||||||
|
Action: "INSERT",
|
||||||
|
})
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, response.Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: product,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Update(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var req UpdateProductReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.UpdateProductRequest{
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
Weight: req.Weight,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
YarnTypes: req.YarnTypes,
|
||||||
|
YarnRatios: req.YarnRatios,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.Update(c.Request.Context(), id, companyID, svcReq); err != nil {
|
||||||
|
log.Printf("update product error: %v", err)
|
||||||
|
response.InternalError(c, "failed to update product")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "products",
|
||||||
|
Action: "UPDATE",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Delete(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.svc.Delete(c.Request.Context(), id, companyID); err != nil {
|
||||||
|
log.Printf("delete product error: %v", err)
|
||||||
|
response.InternalError(c, "failed to delete product")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "products",
|
||||||
|
Action: "DELETE",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Inbound(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if companyID == "" || userID == "" {
|
||||||
|
response.BadRequest(c, "missing user or company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
productID := c.Param("id")
|
||||||
|
|
||||||
|
var req InboundReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.InboundRequest{
|
||||||
|
BatchNo: req.BatchNo,
|
||||||
|
Rolls: req.Rolls,
|
||||||
|
Meters: req.Meters,
|
||||||
|
Notes: req.Notes,
|
||||||
|
WarehouseID: req.WarehouseID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.Inbound(c.Request.Context(), productID, companyID, userID, svcReq); err != nil {
|
||||||
|
log.Printf("inbound error: %v", err)
|
||||||
|
response.InternalError(c, "failed to inbound")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "product_inventory_records",
|
||||||
|
Action: "INSERT",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Outbound(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if companyID == "" || userID == "" {
|
||||||
|
response.BadRequest(c, "missing user or company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
productID := c.Param("id")
|
||||||
|
|
||||||
|
var req OutboundReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.OutboundRequest{
|
||||||
|
BatchNo: req.BatchNo,
|
||||||
|
Rolls: req.Rolls,
|
||||||
|
Meters: req.Meters,
|
||||||
|
OutboundType: req.OutboundType,
|
||||||
|
Notes: req.Notes,
|
||||||
|
RecipientCompanyID: req.RecipientCompanyID,
|
||||||
|
RecipientCompanyName: req.RecipientCompanyName,
|
||||||
|
SourceBatchID: req.SourceBatchID,
|
||||||
|
WashingPlanID: req.WashingPlanID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.Outbound(c.Request.Context(), productID, companyID, userID, svcReq); err != nil {
|
||||||
|
log.Printf("outbound error: %v", err)
|
||||||
|
response.InternalError(c, "failed to outbound")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.wsHub.BroadcastToCompany(companyID, &websocket.Event{
|
||||||
|
Table: "product_outbound_records",
|
||||||
|
Action: "INSERT",
|
||||||
|
})
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Records(c *gin.Context) {
|
||||||
|
productID := c.Param("id")
|
||||||
|
records, err := h.svc.Records(c.Request.Context(), productID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get records error: %v", err)
|
||||||
|
response.InternalError(c, "failed to get records")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, records)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) UpdatePrice(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if companyID == "" || userID == "" {
|
||||||
|
response.BadRequest(c, "missing user or company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
productID := c.Param("id")
|
||||||
|
|
||||||
|
var req UpdatePriceReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.UpdatePrice(c.Request.Context(), productID, companyID, userID, req.NewPrice, req.Reason); err != nil {
|
||||||
|
log.Printf("update price error: %v", err)
|
||||||
|
response.InternalError(c, "failed to update price")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) PriceHistory(c *gin.Context) {
|
||||||
|
productID := c.Param("id")
|
||||||
|
history, err := h.svc.PriceHistory(c.Request.Context(), productID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("get price history error: %v", err)
|
||||||
|
response.InternalError(c, "failed to get price history")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, history)
|
||||||
|
}
|
||||||
81
purchaser-service/internal/handler/washing_plan.go
Normal file
81
purchaser-service/internal/handler/washing_plan.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/service"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WashingPlanHandler struct {
|
||||||
|
svc *service.WashingPlanService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWashingPlanHandler(svc *service.WashingPlanService) *WashingPlanHandler {
|
||||||
|
return &WashingPlanHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateWashingPlanReq struct {
|
||||||
|
ProductID string `json:"product_id" binding:"required"`
|
||||||
|
WashingFactoryID string `json:"washing_factory_id"`
|
||||||
|
PlannedMeters float64 `json:"planned_meters" binding:"required,gt=0"`
|
||||||
|
WashingPrice float64 `json:"washing_price" binding:"required,gt=0"`
|
||||||
|
EstimatedShrinkageRate float64 `json:"estimated_shrinkage_rate"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WashingPlanHandler) Create(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if companyID == "" || userID == "" {
|
||||||
|
response.BadRequest(c, "missing user or company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateWashingPlanReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
svcReq := service.CreateWashingPlanRequest{
|
||||||
|
ProductID: req.ProductID,
|
||||||
|
WashingFactoryID: req.WashingFactoryID,
|
||||||
|
PlannedMeters: req.PlannedMeters,
|
||||||
|
WashingPrice: req.WashingPrice,
|
||||||
|
EstimatedShrinkageRate: req.EstimatedShrinkageRate,
|
||||||
|
Notes: req.Notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
wp, err := h.svc.Create(c.Request.Context(), companyID, userID, svcReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("create washing plan error: %v", err)
|
||||||
|
response.InternalError(c, "failed to create washing plan")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, response.Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: "success",
|
||||||
|
Data: wp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *WashingPlanHandler) List(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
response.BadRequest(c, "missing company id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plans, err := h.svc.List(c.Request.Context(), companyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("list washing plans error: %v", err)
|
||||||
|
response.InternalError(c, "failed to list washing plans")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, plans)
|
||||||
|
}
|
||||||
48
purchaser-service/internal/handler/ws.go
Normal file
48
purchaser-service/internal/handler/ws.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
gorillaWS "github.com/gorilla/websocket"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = gorillaWS.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleWebSocket(hub *websocket.Hub) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
companyID := c.GetHeader("X-Company-ID")
|
||||||
|
if companyID == "" {
|
||||||
|
companyID = c.Query("company_id")
|
||||||
|
}
|
||||||
|
userID := c.GetHeader("X-User-ID")
|
||||||
|
if userID == "" {
|
||||||
|
userID = c.Query("user_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if companyID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing company_id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("websocket upgrade error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := websocket.NewClient(hub, conn, companyID, userID)
|
||||||
|
client.Register()
|
||||||
|
|
||||||
|
go client.WritePump()
|
||||||
|
go client.ReadPump()
|
||||||
|
}
|
||||||
|
}
|
||||||
46
purchaser-service/internal/model/accounts_payable.go
Normal file
46
purchaser-service/internal/model/accounts_payable.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type AccountsPayable struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanID string `json:"plan_id" db:"plan_id"`
|
||||||
|
PurchaserID string `json:"purchaser_id" db:"purchaser_id"`
|
||||||
|
TextileFactoryID string `json:"textile_factory_id" db:"textile_factory_id"`
|
||||||
|
PricePerMeter float64 `json:"price_per_meter" db:"price_per_meter"`
|
||||||
|
TotalQuantity float64 `json:"total_quantity" db:"total_quantity"`
|
||||||
|
TotalAmount float64 `json:"total_amount" db:"total_amount"`
|
||||||
|
PaidQuantity float64 `json:"paid_quantity" db:"paid_quantity"`
|
||||||
|
PaidAmount float64 `json:"paid_amount" db:"paid_amount"`
|
||||||
|
UnpaidQuantity float64 `json:"unpaid_quantity" db:"unpaid_quantity"`
|
||||||
|
UnpaidAmount float64 `json:"unpaid_amount" db:"unpaid_amount"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
|
// Aggregated
|
||||||
|
PlanCode string `json:"plan_code,omitempty"`
|
||||||
|
FactoryName string `json:"factory_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WashingPlan struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanCode string `json:"plan_code" db:"plan_code"`
|
||||||
|
CompanyID string `json:"company_id" db:"company_id"`
|
||||||
|
ProductID string `json:"product_id" db:"product_id"`
|
||||||
|
WashingFactoryID *string `json:"washing_factory_id,omitempty" db:"washing_factory_id"`
|
||||||
|
PlannedMeters float64 `json:"planned_meters" db:"planned_meters"`
|
||||||
|
WashingPrice float64 `json:"washing_price" db:"washing_price"`
|
||||||
|
EstimatedShrinkageRate float64 `json:"estimated_shrinkage_rate" db:"estimated_shrinkage_rate"`
|
||||||
|
EstimatedWashedMeters *float64 `json:"estimated_washed_meters,omitempty" db:"estimated_washed_meters"`
|
||||||
|
EstimatedTotalCost *float64 `json:"estimated_total_cost,omitempty" db:"estimated_total_cost"`
|
||||||
|
ActualShrinkageRate *float64 `json:"actual_shrinkage_rate,omitempty" db:"actual_shrinkage_rate"`
|
||||||
|
ActualWashedMeters *float64 `json:"actual_washed_meters,omitempty" db:"actual_washed_meters"`
|
||||||
|
ActualTotalCost *float64 `json:"actual_total_cost,omitempty" db:"actual_total_cost"`
|
||||||
|
WashingDate *time.Time `json:"washing_date,omitempty" db:"washing_date"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||||
|
CreatedBy *string `json:"created_by,omitempty" db:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
16
purchaser-service/internal/model/company.go
Normal file
16
purchaser-service/internal/model/company.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type CompanyRelationship struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PurchaserCompanyID string `json:"purchaser_company_id" db:"purchaser_company_id"`
|
||||||
|
FactoryCompanyID string `json:"factory_company_id" db:"factory_company_id"`
|
||||||
|
FactoryType string `json:"factory_type" db:"factory_type"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
|
// Aggregated
|
||||||
|
FactoryName string `json:"factory_name,omitempty"`
|
||||||
|
}
|
||||||
84
purchaser-service/internal/model/plan.go
Normal file
84
purchaser-service/internal/model/plan.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ProductionPlan struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanCode string `json:"plan_code" db:"plan_code"`
|
||||||
|
PurchaserID string `json:"purchaser_id" db:"purchaser_id"`
|
||||||
|
ProductName string `json:"product_name" db:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code" db:"fabric_code"`
|
||||||
|
Color string `json:"color" db:"color"`
|
||||||
|
ColorCode string `json:"color_code" db:"color_code"`
|
||||||
|
TargetQuantity float64 `json:"target_quantity" db:"target_quantity"`
|
||||||
|
CompletedQuantity float64 `json:"completed_quantity" db:"completed_quantity"`
|
||||||
|
ProductionPrice *float64 `json:"production_price,omitempty" db:"production_price"`
|
||||||
|
YarnUsagePerMeter *float64 `json:"yarn_usage_per_meter,omitempty" db:"yarn_usage_per_meter"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
Remark *string `json:"remark,omitempty" db:"remark"`
|
||||||
|
StartTime *time.Time `json:"start_time,omitempty" db:"start_time"`
|
||||||
|
CreatedBy *string `json:"created_by,omitempty" db:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
|
||||||
|
// Aggregated fields
|
||||||
|
YarnRatios []YarnRatio `json:"yarn_ratios,omitempty"`
|
||||||
|
ProcessSteps []ProcessStep `json:"process_steps,omitempty"`
|
||||||
|
Factories []PlanFactory `json:"factories,omitempty"`
|
||||||
|
InventoryRecords []InventoryRecord `json:"inventory_records,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type YarnRatio struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanID string `json:"plan_id" db:"plan_id"`
|
||||||
|
YarnName string `json:"yarn_name" db:"yarn_name"`
|
||||||
|
YarnType *string `json:"yarn_type,omitempty" db:"yarn_type"`
|
||||||
|
Ratio float64 `json:"ratio" db:"ratio"`
|
||||||
|
AmountPerMeter float64 `json:"amount_per_meter" db:"amount_per_meter"`
|
||||||
|
TotalAmount float64 `json:"total_amount" db:"total_amount"`
|
||||||
|
CompanyID *string `json:"company_id,omitempty" db:"company_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanFactory struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanID string `json:"plan_id" db:"plan_id"`
|
||||||
|
FactoryID string `json:"factory_id" db:"factory_id"`
|
||||||
|
FactoryType string `json:"factory_type" db:"factory_type"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
|
||||||
|
// Aggregated
|
||||||
|
FactoryName string `json:"factory_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessStep struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanID string `json:"plan_id" db:"plan_id"`
|
||||||
|
StepType string `json:"step_type" db:"step_type"`
|
||||||
|
Status string `json:"status" db:"status"`
|
||||||
|
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||||
|
OperatorID *string `json:"operator_id,omitempty" db:"operator_id"`
|
||||||
|
CompanyID *string `json:"company_id,omitempty" db:"company_id"`
|
||||||
|
Timestamp *time.Time `json:"timestamp,omitempty" db:"timestamp"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
|
||||||
|
// Aggregated
|
||||||
|
OperatorName string `json:"operator_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InventoryRecord struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
PlanID string `json:"plan_id" db:"plan_id"`
|
||||||
|
WarehouseID string `json:"warehouse_id" db:"warehouse_id"`
|
||||||
|
Quantity float64 `json:"quantity" db:"quantity"`
|
||||||
|
Rolls *int `json:"rolls,omitempty" db:"rolls"`
|
||||||
|
PricePerMeter *float64 `json:"price_per_meter,omitempty" db:"price_per_meter"`
|
||||||
|
PriceNote *string `json:"price_note,omitempty" db:"price_note"`
|
||||||
|
WarehouseLocation *string `json:"warehouse_location,omitempty" db:"warehouse_location"`
|
||||||
|
OperatorID *string `json:"operator_id,omitempty" db:"operator_id"`
|
||||||
|
CompanyID *string `json:"company_id,omitempty" db:"company_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
|
||||||
|
// Aggregated
|
||||||
|
OperatorName string `json:"operator_name,omitempty"`
|
||||||
|
}
|
||||||
66
purchaser-service/internal/model/product.go
Normal file
66
purchaser-service/internal/model/product.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Product struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
CompanyID string `json:"company_id" db:"company_id"`
|
||||||
|
ProductName string `json:"product_name" db:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code" db:"fabric_code"`
|
||||||
|
Color string `json:"color" db:"color"`
|
||||||
|
ColorCode string `json:"color_code" db:"color_code"`
|
||||||
|
Weight float64 `json:"weight" db:"weight"`
|
||||||
|
WarpWeight *float64 `json:"warp_weight,omitempty" db:"warp_weight"`
|
||||||
|
WeftWeight *float64 `json:"weft_weight,omitempty" db:"weft_weight"`
|
||||||
|
TotalWeight *float64 `json:"total_weight,omitempty" db:"total_weight"`
|
||||||
|
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter" db:"yarn_usage_per_meter"`
|
||||||
|
ProductionPrice *float64 `json:"production_price,omitempty" db:"production_price"`
|
||||||
|
TotalStock float64 `json:"total_stock" db:"total_stock"`
|
||||||
|
RawFabricMeters float64 `json:"raw_fabric_meters" db:"raw_fabric_meters"`
|
||||||
|
RawFabricRolls int `json:"raw_fabric_rolls" db:"raw_fabric_rolls"`
|
||||||
|
ImageURL *string `json:"image_url,omitempty" db:"image_url"`
|
||||||
|
YarnTypes []string `json:"yarn_types,omitempty" db:"yarn_types"`
|
||||||
|
YarnRatios []float64 `json:"yarn_ratios,omitempty" db:"yarn_ratios"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductInventoryRecord struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
ProductID string `json:"product_id" db:"product_id"`
|
||||||
|
BatchNo string `json:"batch_no" db:"batch_no"`
|
||||||
|
Rolls int `json:"rolls" db:"rolls"`
|
||||||
|
Meters float64 `json:"meters" db:"meters"`
|
||||||
|
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||||
|
OperatorID *string `json:"operator_id,omitempty" db:"operator_id"`
|
||||||
|
CompanyID *string `json:"company_id,omitempty" db:"company_id"`
|
||||||
|
WarehouseID *string `json:"warehouse_id,omitempty" db:"warehouse_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductOutboundRecord struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
ProductID string `json:"product_id" db:"product_id"`
|
||||||
|
BatchNo string `json:"batch_no" db:"batch_no"`
|
||||||
|
Rolls int `json:"rolls" db:"rolls"`
|
||||||
|
Meters float64 `json:"meters" db:"meters"`
|
||||||
|
OutboundType string `json:"outbound_type" db:"outbound_type"`
|
||||||
|
Notes *string `json:"notes,omitempty" db:"notes"`
|
||||||
|
OperatorID *string `json:"operator_id,omitempty" db:"operator_id"`
|
||||||
|
CompanyID *string `json:"company_id,omitempty" db:"company_id"`
|
||||||
|
RecipientCompanyID *string `json:"recipient_company_id,omitempty" db:"recipient_company_id"`
|
||||||
|
RecipientCompanyName *string `json:"recipient_company_name,omitempty" db:"recipient_company_name"`
|
||||||
|
SourceBatchID *string `json:"source_batch_id,omitempty" db:"source_batch_id"`
|
||||||
|
WashingPlanID *string `json:"washing_plan_id,omitempty" db:"washing_plan_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductPriceHistory struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
ProductID string `json:"product_id" db:"product_id"`
|
||||||
|
OldPrice *float64 `json:"old_price,omitempty" db:"old_price"`
|
||||||
|
NewPrice float64 `json:"new_price" db:"new_price"`
|
||||||
|
Reason *string `json:"reason,omitempty" db:"reason"`
|
||||||
|
ChangedBy *string `json:"changed_by,omitempty" db:"changed_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
86
purchaser-service/internal/repository/ap_repo.go
Normal file
86
purchaser-service/internal/repository/ap_repo.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPRepo(db *sql.DB) *APRepo {
|
||||||
|
return &APRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APRepo) List(ctx context.Context, companyID string) ([]model.AccountsPayable, error) {
|
||||||
|
query := `SELECT ap.id, ap.plan_id, ap.purchaser_id, ap.textile_factory_id,
|
||||||
|
ap.price_per_meter, ap.total_quantity, ap.total_amount,
|
||||||
|
ap.paid_quantity, ap.paid_amount, ap.unpaid_quantity, ap.unpaid_amount,
|
||||||
|
ap.status, ap.created_at, ap.updated_at,
|
||||||
|
COALESCE(pp.plan_code, '') as plan_code,
|
||||||
|
COALESCE(c.name, '') as factory_name
|
||||||
|
FROM ilm_accounts_payable ap
|
||||||
|
LEFT JOIN ilm_production_plan pp ON pp.id = ap.plan_id
|
||||||
|
LEFT JOIN ilm_company c ON c.id = ap.textile_factory_id
|
||||||
|
WHERE ap.purchaser_id = ?
|
||||||
|
ORDER BY ap.created_at DESC`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list accounts payable: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var list []model.AccountsPayable
|
||||||
|
for rows.Next() {
|
||||||
|
var ap model.AccountsPayable
|
||||||
|
if err := rows.Scan(
|
||||||
|
&ap.ID, &ap.PlanID, &ap.PurchaserID, &ap.TextileFactoryID,
|
||||||
|
&ap.PricePerMeter, &ap.TotalQuantity, &ap.TotalAmount,
|
||||||
|
&ap.PaidQuantity, &ap.PaidAmount, &ap.UnpaidQuantity, &ap.UnpaidAmount,
|
||||||
|
&ap.Status, &ap.CreatedAt, &ap.UpdatedAt,
|
||||||
|
&ap.PlanCode, &ap.FactoryName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan accounts payable: %w", err)
|
||||||
|
}
|
||||||
|
list = append(list, ap)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APRepo) Pay(ctx context.Context, tx *sql.Tx, apID string, amount float64) error {
|
||||||
|
query := `UPDATE ilm_accounts_payable SET
|
||||||
|
paid_amount = paid_amount + ?,
|
||||||
|
unpaid_amount = unpaid_amount - ?,
|
||||||
|
status = CASE WHEN unpaid_amount - ? <= 0 THEN 'completed' ELSE status END,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ? AND unpaid_amount >= ?`
|
||||||
|
result, err := tx.ExecContext(ctx, query, amount, amount, amount, apID, amount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pay: %w", err)
|
||||||
|
}
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check rows affected: %w", err)
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return fmt.Errorf("payment failed: insufficient unpaid amount or record not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APRepo) TotalUnpaid(ctx context.Context, companyID string) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
`SELECT COALESCE(SUM(unpaid_amount), 0) FROM ilm_accounts_payable WHERE purchaser_id = ?`,
|
||||||
|
companyID,
|
||||||
|
).Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("total unpaid: %w", err)
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
47
purchaser-service/internal/repository/factory_repo.go
Normal file
47
purchaser-service/internal/repository/factory_repo.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FactoryRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactoryRepo(db *sql.DB) *FactoryRepo {
|
||||||
|
return &FactoryRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FactoryRepo) ListRelationships(ctx context.Context, companyID string) ([]model.CompanyRelationship, error) {
|
||||||
|
query := `SELECT cr.id, cr.purchaser_company_id, cr.factory_company_id,
|
||||||
|
cr.factory_type, cr.status, cr.created_at, cr.updated_at,
|
||||||
|
COALESCE(c.name, '') as factory_name
|
||||||
|
FROM ilm_company_relationship cr
|
||||||
|
LEFT JOIN ilm_company c ON c.id = cr.factory_company_id
|
||||||
|
WHERE cr.purchaser_company_id = ? AND cr.status = 'active'
|
||||||
|
ORDER BY cr.created_at DESC`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list relationships: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var rels []model.CompanyRelationship
|
||||||
|
for rows.Next() {
|
||||||
|
var rel model.CompanyRelationship
|
||||||
|
if err := rows.Scan(
|
||||||
|
&rel.ID, &rel.PurchaserCompanyID, &rel.FactoryCompanyID,
|
||||||
|
&rel.FactoryType, &rel.Status, &rel.CreatedAt, &rel.UpdatedAt,
|
||||||
|
&rel.FactoryName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan relationship: %w", err)
|
||||||
|
}
|
||||||
|
rels = append(rels, rel)
|
||||||
|
}
|
||||||
|
return rels, nil
|
||||||
|
}
|
||||||
324
purchaser-service/internal/repository/plan_repo.go
Normal file
324
purchaser-service/internal/repository/plan_repo.go
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlanRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlanRepo(db *sql.DB) *PlanRepo {
|
||||||
|
return &PlanRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) List(ctx context.Context, companyID string, p types.Pagination, status string) ([]model.ProductionPlan, int, error) {
|
||||||
|
countQuery := `SELECT COUNT(*) FROM ilm_production_plan WHERE purchaser_id = ?`
|
||||||
|
args := []interface{}{companyID}
|
||||||
|
|
||||||
|
if status != "" {
|
||||||
|
countQuery += " AND status = ?"
|
||||||
|
args = append(args, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int
|
||||||
|
if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("count plans: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT id, plan_code, purchaser_id, product_name, fabric_code, color, color_code,
|
||||||
|
target_quantity, completed_quantity, production_price, yarn_usage_per_meter,
|
||||||
|
status, remark, start_time, created_by, created_at, updated_at
|
||||||
|
FROM ilm_production_plan WHERE purchaser_id = ?`
|
||||||
|
|
||||||
|
queryArgs := []interface{}{companyID}
|
||||||
|
|
||||||
|
if status != "" {
|
||||||
|
query += " AND status = ?"
|
||||||
|
queryArgs = append(queryArgs, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " ORDER BY created_at DESC"
|
||||||
|
query += " LIMIT ? OFFSET ?"
|
||||||
|
queryArgs = append(queryArgs, p.PageSize, p.Offset())
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, queryArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("list plans: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var plans []model.ProductionPlan
|
||||||
|
for rows.Next() {
|
||||||
|
var plan model.ProductionPlan
|
||||||
|
if err := rows.Scan(
|
||||||
|
&plan.ID, &plan.PlanCode, &plan.PurchaserID, &plan.ProductName,
|
||||||
|
&plan.FabricCode, &plan.Color, &plan.ColorCode,
|
||||||
|
&plan.TargetQuantity, &plan.CompletedQuantity,
|
||||||
|
&plan.ProductionPrice, &plan.YarnUsagePerMeter,
|
||||||
|
&plan.Status, &plan.Remark, &plan.StartTime,
|
||||||
|
&plan.CreatedBy, &plan.CreatedAt, &plan.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("scan plan: %w", err)
|
||||||
|
}
|
||||||
|
plans = append(plans, plan)
|
||||||
|
}
|
||||||
|
return plans, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) Create(ctx context.Context, tx *sql.Tx, plan *model.ProductionPlan) error {
|
||||||
|
query := `INSERT INTO ilm_production_plan
|
||||||
|
(id, plan_code, purchaser_id, product_name, fabric_code, color, color_code,
|
||||||
|
target_quantity, completed_quantity, production_price, yarn_usage_per_meter,
|
||||||
|
status, remark, start_time, created_by, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
_, err := tx.ExecContext(ctx, query,
|
||||||
|
plan.ID, plan.PlanCode, plan.PurchaserID, plan.ProductName,
|
||||||
|
plan.FabricCode, plan.Color, plan.ColorCode,
|
||||||
|
plan.TargetQuantity, plan.CompletedQuantity,
|
||||||
|
plan.ProductionPrice, plan.YarnUsagePerMeter,
|
||||||
|
plan.Status, plan.Remark, plan.StartTime,
|
||||||
|
plan.CreatedBy, plan.CreatedAt, plan.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert plan: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) CreateYarnRatios(ctx context.Context, tx *sql.Tx, ratios []model.YarnRatio) error {
|
||||||
|
for _, ratio := range ratios {
|
||||||
|
query := `INSERT INTO ilm_yarn_ratio
|
||||||
|
(id, plan_id, yarn_name, yarn_type, ratio, amount_per_meter, total_amount, company_id, created_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?)`
|
||||||
|
_, err := tx.ExecContext(ctx, query,
|
||||||
|
ratio.ID, ratio.PlanID, ratio.YarnName, ratio.YarnType,
|
||||||
|
ratio.Ratio, ratio.AmountPerMeter, ratio.TotalAmount,
|
||||||
|
ratio.CompanyID, ratio.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert yarn ratio: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) GetByID(ctx context.Context, id string) (*model.ProductionPlan, error) {
|
||||||
|
query := `SELECT id, plan_code, purchaser_id, product_name, fabric_code, color, color_code,
|
||||||
|
target_quantity, completed_quantity, production_price, yarn_usage_per_meter,
|
||||||
|
status, remark, start_time, created_by, created_at, updated_at
|
||||||
|
FROM ilm_production_plan WHERE id = ?`
|
||||||
|
|
||||||
|
plan := &model.ProductionPlan{}
|
||||||
|
err := r.db.QueryRowContext(ctx, query, id).Scan(
|
||||||
|
&plan.ID, &plan.PlanCode, &plan.PurchaserID, &plan.ProductName,
|
||||||
|
&plan.FabricCode, &plan.Color, &plan.ColorCode,
|
||||||
|
&plan.TargetQuantity, &plan.CompletedQuantity,
|
||||||
|
&plan.ProductionPrice, &plan.YarnUsagePerMeter,
|
||||||
|
&plan.Status, &plan.Remark, &plan.StartTime,
|
||||||
|
&plan.CreatedBy, &plan.CreatedAt, &plan.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("get plan by id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load yarn ratios
|
||||||
|
yarnRows, err := r.db.QueryContext(ctx,
|
||||||
|
`SELECT id, plan_id, yarn_name, yarn_type, ratio, amount_per_meter, total_amount, company_id, created_at
|
||||||
|
FROM ilm_yarn_ratio WHERE plan_id = ? ORDER BY created_at`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get yarn ratios: %w", err)
|
||||||
|
}
|
||||||
|
defer yarnRows.Close()
|
||||||
|
|
||||||
|
for yarnRows.Next() {
|
||||||
|
var yr model.YarnRatio
|
||||||
|
if err := yarnRows.Scan(
|
||||||
|
&yr.ID, &yr.PlanID, &yr.YarnName, &yr.YarnType,
|
||||||
|
&yr.Ratio, &yr.AmountPerMeter, &yr.TotalAmount,
|
||||||
|
&yr.CompanyID, &yr.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan yarn ratio: %w", err)
|
||||||
|
}
|
||||||
|
plan.YarnRatios = append(plan.YarnRatios, yr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load factories
|
||||||
|
factoryRows, err := r.db.QueryContext(ctx,
|
||||||
|
`SELECT pf.id, pf.plan_id, pf.factory_id, pf.factory_type, pf.created_at,
|
||||||
|
COALESCE(c.name, '') as factory_name
|
||||||
|
FROM ilm_plan_factory pf
|
||||||
|
LEFT JOIN ilm_company c ON c.id = pf.factory_id
|
||||||
|
WHERE pf.plan_id = ? ORDER BY pf.created_at`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get plan factories: %w", err)
|
||||||
|
}
|
||||||
|
defer factoryRows.Close()
|
||||||
|
|
||||||
|
for factoryRows.Next() {
|
||||||
|
var pf model.PlanFactory
|
||||||
|
if err := factoryRows.Scan(
|
||||||
|
&pf.ID, &pf.PlanID, &pf.FactoryID, &pf.FactoryType,
|
||||||
|
&pf.CreatedAt, &pf.FactoryName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan plan factory: %w", err)
|
||||||
|
}
|
||||||
|
plan.Factories = append(plan.Factories, pf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) Update(ctx context.Context, plan *model.ProductionPlan) error {
|
||||||
|
query := `UPDATE ilm_production_plan SET
|
||||||
|
product_name = ?, fabric_code = ?, color = ?, color_code = ?,
|
||||||
|
target_quantity = ?, production_price = ?, yarn_usage_per_meter = ?,
|
||||||
|
status = ?, remark = ?, updated_at = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
plan.ProductName, plan.FabricCode, plan.Color, plan.ColorCode,
|
||||||
|
plan.TargetQuantity, plan.ProductionPrice, plan.YarnUsagePerMeter,
|
||||||
|
plan.Status, plan.Remark, time.Now(),
|
||||||
|
plan.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update plan: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) Delete(ctx context.Context, id, companyID string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`DELETE FROM ilm_production_plan WHERE id = ? AND purchaser_id = ?`,
|
||||||
|
id, companyID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete plan: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) AssignFactory(ctx context.Context, planID, factoryID, factoryType string) error {
|
||||||
|
id := uuid.New().String()
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO ilm_plan_factory (id, plan_id, factory_id, factory_type, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
id, planID, factoryID, factoryType, time.Now(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assign factory: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) GetSteps(ctx context.Context, planID string) ([]model.ProcessStep, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx,
|
||||||
|
`SELECT ps.id, ps.plan_id, ps.step_type, ps.status, ps.notes,
|
||||||
|
ps.operator_id, ps.company_id, ps.timestamp, ps.created_at,
|
||||||
|
COALESCE(su.real_name, '') as operator_name
|
||||||
|
FROM ilm_process_step ps
|
||||||
|
LEFT JOIN sys_user su ON su.user_id = ps.operator_id
|
||||||
|
WHERE ps.plan_id = ? ORDER BY ps.created_at`, planID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get steps: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var steps []model.ProcessStep
|
||||||
|
for rows.Next() {
|
||||||
|
var s model.ProcessStep
|
||||||
|
if err := rows.Scan(
|
||||||
|
&s.ID, &s.PlanID, &s.StepType, &s.Status, &s.Notes,
|
||||||
|
&s.OperatorID, &s.CompanyID, &s.Timestamp, &s.CreatedAt,
|
||||||
|
&s.OperatorName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan step: %w", err)
|
||||||
|
}
|
||||||
|
steps = append(steps, s)
|
||||||
|
}
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) GetInventory(ctx context.Context, planID string) ([]model.InventoryRecord, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx,
|
||||||
|
`SELECT ir.id, ir.plan_id, ir.warehouse_id, ir.quantity, ir.rolls,
|
||||||
|
ir.price_per_meter, ir.price_note, ir.warehouse_location,
|
||||||
|
ir.operator_id, ir.company_id, ir.created_at,
|
||||||
|
COALESCE(su.real_name, '') as operator_name
|
||||||
|
FROM ilm_inventory_record ir
|
||||||
|
LEFT JOIN sys_user su ON su.user_id = ir.operator_id
|
||||||
|
WHERE ir.plan_id = ? ORDER BY ir.created_at DESC`, planID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get inventory: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var records []model.InventoryRecord
|
||||||
|
for rows.Next() {
|
||||||
|
var rec model.InventoryRecord
|
||||||
|
if err := rows.Scan(
|
||||||
|
&rec.ID, &rec.PlanID, &rec.WarehouseID, &rec.Quantity, &rec.Rolls,
|
||||||
|
&rec.PricePerMeter, &rec.PriceNote, &rec.WarehouseLocation,
|
||||||
|
&rec.OperatorID, &rec.CompanyID, &rec.CreatedAt,
|
||||||
|
&rec.OperatorName,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan inventory record: %w", err)
|
||||||
|
}
|
||||||
|
records = append(records, rec)
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) CountByStatus(ctx context.Context, companyID string) (total, producing, completed int, err error) {
|
||||||
|
query := `SELECT
|
||||||
|
COUNT(*),
|
||||||
|
SUM(CASE WHEN status = 'producing' THEN 1 ELSE 0 END),
|
||||||
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)
|
||||||
|
FROM ilm_production_plan WHERE purchaser_id = ?`
|
||||||
|
err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &producing, &completed)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("count by status: %w", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PlanRepo) Recent(ctx context.Context, companyID string, limit int) ([]model.ProductionPlan, error) {
|
||||||
|
query := `SELECT id, plan_code, purchaser_id, product_name, fabric_code, color, color_code,
|
||||||
|
target_quantity, completed_quantity, production_price, yarn_usage_per_meter,
|
||||||
|
status, remark, start_time, created_by, created_at, updated_at
|
||||||
|
FROM ilm_production_plan WHERE purchaser_id = ?
|
||||||
|
ORDER BY created_at DESC LIMIT ?`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("recent plans: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var plans []model.ProductionPlan
|
||||||
|
for rows.Next() {
|
||||||
|
var plan model.ProductionPlan
|
||||||
|
if err := rows.Scan(
|
||||||
|
&plan.ID, &plan.PlanCode, &plan.PurchaserID, &plan.ProductName,
|
||||||
|
&plan.FabricCode, &plan.Color, &plan.ColorCode,
|
||||||
|
&plan.TargetQuantity, &plan.CompletedQuantity,
|
||||||
|
&plan.ProductionPrice, &plan.YarnUsagePerMeter,
|
||||||
|
&plan.Status, &plan.Remark, &plan.StartTime,
|
||||||
|
&plan.CreatedBy, &plan.CreatedAt, &plan.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan recent plan: %w", err)
|
||||||
|
}
|
||||||
|
plans = append(plans, plan)
|
||||||
|
}
|
||||||
|
return plans, nil
|
||||||
|
}
|
||||||
264
purchaser-service/internal/repository/product_repo.go
Normal file
264
purchaser-service/internal/repository/product_repo.go
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductRepo(db *sql.DB) *ProductRepo {
|
||||||
|
return &ProductRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) List(ctx context.Context, companyID string) ([]model.Product, error) {
|
||||||
|
query := `SELECT id, company_id, product_name, fabric_code, color, color_code,
|
||||||
|
weight, warp_weight, weft_weight, total_weight, yarn_usage_per_meter,
|
||||||
|
production_price, total_stock, raw_fabric_meters, raw_fabric_rolls,
|
||||||
|
image_url, yarn_types, yarn_ratios, created_at, updated_at
|
||||||
|
FROM products WHERE company_id = ? ORDER BY created_at DESC`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list products: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var products []model.Product
|
||||||
|
for rows.Next() {
|
||||||
|
var p model.Product
|
||||||
|
if err := rows.Scan(
|
||||||
|
&p.ID, &p.CompanyID, &p.ProductName, &p.FabricCode, &p.Color, &p.ColorCode,
|
||||||
|
&p.Weight, &p.WarpWeight, &p.WeftWeight, &p.TotalWeight,
|
||||||
|
&p.YarnUsagePerMeter, &p.ProductionPrice,
|
||||||
|
&p.TotalStock, &p.RawFabricMeters, &p.RawFabricRolls,
|
||||||
|
&p.ImageURL, &p.YarnTypes, &p.YarnRatios,
|
||||||
|
&p.CreatedAt, &p.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan product: %w", err)
|
||||||
|
}
|
||||||
|
products = append(products, p)
|
||||||
|
}
|
||||||
|
return products, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Create(ctx context.Context, p *model.Product) error {
|
||||||
|
query := `INSERT INTO products
|
||||||
|
(id, company_id, product_name, fabric_code, color, color_code,
|
||||||
|
weight, warp_weight, weft_weight, total_weight, yarn_usage_per_meter,
|
||||||
|
production_price, total_stock, raw_fabric_meters, raw_fabric_rolls,
|
||||||
|
image_url, yarn_types, yarn_ratios, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
p.ID, p.CompanyID, p.ProductName, p.FabricCode, p.Color, p.ColorCode,
|
||||||
|
p.Weight, p.WarpWeight, p.WeftWeight, p.TotalWeight,
|
||||||
|
p.YarnUsagePerMeter, p.ProductionPrice,
|
||||||
|
p.TotalStock, p.RawFabricMeters, p.RawFabricRolls,
|
||||||
|
p.ImageURL, p.YarnTypes, p.YarnRatios,
|
||||||
|
p.CreatedAt, p.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert product: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Update(ctx context.Context, p *model.Product) error {
|
||||||
|
query := `UPDATE products SET
|
||||||
|
product_name = ?, fabric_code = ?, color = ?, color_code = ?,
|
||||||
|
weight = ?, warp_weight = ?, weft_weight = ?, total_weight = ?,
|
||||||
|
yarn_usage_per_meter = ?, production_price = ?,
|
||||||
|
image_url = ?, yarn_types = ?, yarn_ratios = ?, updated_at = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
p.ProductName, p.FabricCode, p.Color, p.ColorCode,
|
||||||
|
p.Weight, p.WarpWeight, p.WeftWeight, p.TotalWeight,
|
||||||
|
p.YarnUsagePerMeter, p.ProductionPrice,
|
||||||
|
p.ImageURL, p.YarnTypes, p.YarnRatios, time.Now(),
|
||||||
|
p.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update product: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Delete(ctx context.Context, id, companyID string) error {
|
||||||
|
_, err := r.db.ExecContext(ctx,
|
||||||
|
`DELETE FROM products WHERE id = ? AND company_id = ?`,
|
||||||
|
id, companyID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete product: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Inbound(ctx context.Context, tx *sql.Tx, rec *model.ProductInventoryRecord) error {
|
||||||
|
query := `INSERT INTO ilm_product_inventory
|
||||||
|
(id, product_id, batch_no, rolls, meters, notes, operator_id, company_id, warehouse_id, created_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
_, err := tx.ExecContext(ctx, query,
|
||||||
|
rec.ID, rec.ProductID, rec.BatchNo, rec.Rolls, rec.Meters,
|
||||||
|
rec.Notes, rec.OperatorID, rec.CompanyID, rec.WarehouseID, rec.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert inbound record: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) UpdateStock(ctx context.Context, tx *sql.Tx, productID string, deltaMeters float64, deltaRolls int) error {
|
||||||
|
query := `UPDATE products SET
|
||||||
|
total_stock = total_stock + ?,
|
||||||
|
raw_fabric_meters = raw_fabric_meters + ?,
|
||||||
|
raw_fabric_rolls = raw_fabric_rolls + ?,
|
||||||
|
updated_at = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
_, err := tx.ExecContext(ctx, query, deltaMeters, deltaMeters, deltaRolls, time.Now(), productID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update stock: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Outbound(ctx context.Context, tx *sql.Tx, rec *model.ProductOutboundRecord) error {
|
||||||
|
query := `INSERT INTO ilm_product_outbound
|
||||||
|
(id, product_id, batch_no, rolls, meters, outbound_type, notes,
|
||||||
|
operator_id, company_id, recipient_company_id, recipient_company_name,
|
||||||
|
source_batch_id, washing_plan_id, created_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
_, err := tx.ExecContext(ctx, query,
|
||||||
|
rec.ID, rec.ProductID, rec.BatchNo, rec.Rolls, rec.Meters,
|
||||||
|
rec.OutboundType, rec.Notes,
|
||||||
|
rec.OperatorID, rec.CompanyID,
|
||||||
|
rec.RecipientCompanyID, rec.RecipientCompanyName,
|
||||||
|
rec.SourceBatchID, rec.WashingPlanID, rec.CreatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert outbound record: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Records(ctx context.Context, productID string) ([]model.ProductInventoryRecord, error) {
|
||||||
|
query := `SELECT id, product_id, batch_no, rolls, meters, notes,
|
||||||
|
operator_id, company_id, warehouse_id, created_at
|
||||||
|
FROM ilm_product_inventory
|
||||||
|
WHERE product_id = ? ORDER BY created_at DESC`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, productID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list records: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var records []model.ProductInventoryRecord
|
||||||
|
for rows.Next() {
|
||||||
|
var rec model.ProductInventoryRecord
|
||||||
|
if err := rows.Scan(
|
||||||
|
&rec.ID, &rec.ProductID, &rec.BatchNo, &rec.Rolls, &rec.Meters,
|
||||||
|
&rec.Notes, &rec.OperatorID, &rec.CompanyID, &rec.WarehouseID, &rec.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan record: %w", err)
|
||||||
|
}
|
||||||
|
records = append(records, rec)
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) UpdatePrice(ctx context.Context, tx *sql.Tx, productID string, newPrice float64) error {
|
||||||
|
_, err := tx.ExecContext(ctx,
|
||||||
|
`UPDATE products SET production_price = ?, updated_at = ? WHERE id = ?`,
|
||||||
|
newPrice, time.Now(), productID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update price: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) GetPrice(ctx context.Context, productID string) (*float64, error) {
|
||||||
|
var price *float64
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
`SELECT production_price FROM products WHERE id = ?`, productID,
|
||||||
|
).Scan(&price)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get price: %w", err)
|
||||||
|
}
|
||||||
|
return price, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) InsertPriceHistory(ctx context.Context, tx *sql.Tx, productID string, oldPrice *float64, newPrice float64, reason, changedBy string) error {
|
||||||
|
id := uuid.New().String()
|
||||||
|
var reasonPtr *string
|
||||||
|
if reason != "" {
|
||||||
|
reasonPtr = &reason
|
||||||
|
}
|
||||||
|
var changedByPtr *string
|
||||||
|
if changedBy != "" {
|
||||||
|
changedByPtr = &changedBy
|
||||||
|
}
|
||||||
|
_, err := tx.ExecContext(ctx,
|
||||||
|
`INSERT INTO ilm_product_price_history
|
||||||
|
(id, product_id, old_price, new_price, reason, changed_by, created_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?)`,
|
||||||
|
id, productID, oldPrice, newPrice, reasonPtr, changedByPtr, time.Now(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert price history: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) PriceHistory(ctx context.Context, productID string) ([]model.ProductPriceHistory, error) {
|
||||||
|
query := `SELECT id, product_id, old_price, new_price, reason, changed_by, created_at
|
||||||
|
FROM ilm_product_price_history
|
||||||
|
WHERE product_id = ? ORDER BY created_at DESC`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, productID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list price history: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var history []model.ProductPriceHistory
|
||||||
|
for rows.Next() {
|
||||||
|
var h model.ProductPriceHistory
|
||||||
|
if err := rows.Scan(
|
||||||
|
&h.ID, &h.ProductID, &h.OldPrice, &h.NewPrice,
|
||||||
|
&h.Reason, &h.ChangedBy, &h.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan price history: %w", err)
|
||||||
|
}
|
||||||
|
history = append(history, h)
|
||||||
|
}
|
||||||
|
return history, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) Count(ctx context.Context, companyID string) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
`SELECT COUNT(*) FROM products WHERE company_id = ?`, companyID,
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("count products: %w", err)
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepo) TotalStock(ctx context.Context, companyID string) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
`SELECT COALESCE(SUM(total_stock), 0) FROM products WHERE company_id = ?`, companyID,
|
||||||
|
).Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("total stock: %w", err)
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
86
purchaser-service/internal/repository/washing_plan_repo.go
Normal file
86
purchaser-service/internal/repository/washing_plan_repo.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WashingPlanRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWashingPlanRepo(db *sql.DB) *WashingPlanRepo {
|
||||||
|
return &WashingPlanRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WashingPlanRepo) Create(ctx context.Context, wp *model.WashingPlan) error {
|
||||||
|
query := `INSERT INTO ilm_washing_plan
|
||||||
|
(id, plan_code, company_id, product_id, washing_factory_id,
|
||||||
|
planned_meters, washing_price, estimated_shrinkage_rate,
|
||||||
|
estimated_washed_meters, estimated_total_cost,
|
||||||
|
status, notes, created_by, created_at, updated_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
|
||||||
|
_, err := r.db.ExecContext(ctx, query,
|
||||||
|
wp.ID, wp.PlanCode, wp.CompanyID, wp.ProductID, wp.WashingFactoryID,
|
||||||
|
wp.PlannedMeters, wp.WashingPrice, wp.EstimatedShrinkageRate,
|
||||||
|
wp.EstimatedWashedMeters, wp.EstimatedTotalCost,
|
||||||
|
wp.Status, wp.Notes, wp.CreatedBy, wp.CreatedAt, wp.UpdatedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert washing plan: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WashingPlanRepo) List(ctx context.Context, companyID string) ([]model.WashingPlan, error) {
|
||||||
|
query := `SELECT id, plan_code, company_id, product_id, washing_factory_id,
|
||||||
|
planned_meters, washing_price, estimated_shrinkage_rate,
|
||||||
|
estimated_washed_meters, estimated_total_cost,
|
||||||
|
actual_shrinkage_rate, actual_washed_meters, actual_total_cost,
|
||||||
|
washing_date, status, notes, created_by, created_at, updated_at
|
||||||
|
FROM ilm_washing_plan WHERE company_id = ?
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list washing plans: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var plans []model.WashingPlan
|
||||||
|
for rows.Next() {
|
||||||
|
var wp model.WashingPlan
|
||||||
|
if err := rows.Scan(
|
||||||
|
&wp.ID, &wp.PlanCode, &wp.CompanyID, &wp.ProductID, &wp.WashingFactoryID,
|
||||||
|
&wp.PlannedMeters, &wp.WashingPrice, &wp.EstimatedShrinkageRate,
|
||||||
|
&wp.EstimatedWashedMeters, &wp.EstimatedTotalCost,
|
||||||
|
&wp.ActualShrinkageRate, &wp.ActualWashedMeters, &wp.ActualTotalCost,
|
||||||
|
&wp.WashingDate, &wp.Status, &wp.Notes, &wp.CreatedBy,
|
||||||
|
&wp.CreatedAt, &wp.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan washing plan: %w", err)
|
||||||
|
}
|
||||||
|
plans = append(plans, wp)
|
||||||
|
}
|
||||||
|
return plans, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WashingPlanRepo) GeneratePlanCode(ctx context.Context) (string, error) {
|
||||||
|
today := time.Now().Format("20060102")
|
||||||
|
prefix := "WP" + today
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := r.db.QueryRowContext(ctx,
|
||||||
|
`SELECT COUNT(*) FROM ilm_washing_plan WHERE plan_code LIKE ?`,
|
||||||
|
prefix+"%",
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("generate plan code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s%03d", prefix, count+1), nil
|
||||||
|
}
|
||||||
41
purchaser-service/internal/service/ap_service.go
Normal file
41
purchaser-service/internal/service/ap_service.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APService struct {
|
||||||
|
repo *repository.APRepo
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPService(repo *repository.APRepo, db *sql.DB) *APService {
|
||||||
|
return &APService{repo: repo, db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APService) List(ctx context.Context, companyID string) ([]model.AccountsPayable, error) {
|
||||||
|
return s.repo.List(ctx, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APService) Pay(ctx context.Context, apID, companyID string, amount float64) error {
|
||||||
|
if amount <= 0 {
|
||||||
|
return fmt.Errorf("amount must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if err := s.repo.Pay(ctx, tx, apID, amount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
69
purchaser-service/internal/service/dashboard_service.go
Normal file
69
purchaser-service/internal/service/dashboard_service.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardStats struct {
|
||||||
|
TotalPlans int `json:"total_plans"`
|
||||||
|
ProducingPlans int `json:"producing_plans"`
|
||||||
|
CompletedPlans int `json:"completed_plans"`
|
||||||
|
TotalProducts int `json:"total_products"`
|
||||||
|
TotalStock float64 `json:"total_stock"`
|
||||||
|
UnpaidAmount float64 `json:"unpaid_amount"`
|
||||||
|
RecentPlans []model.ProductionPlan `json:"recent_plans"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardService struct {
|
||||||
|
planRepo *repository.PlanRepo
|
||||||
|
productRepo *repository.ProductRepo
|
||||||
|
apRepo *repository.APRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDashboardService(planRepo *repository.PlanRepo, productRepo *repository.ProductRepo, apRepo *repository.APRepo) *DashboardService {
|
||||||
|
return &DashboardService{
|
||||||
|
planRepo: planRepo,
|
||||||
|
productRepo: productRepo,
|
||||||
|
apRepo: apRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DashboardService) Stats(ctx context.Context, companyID string) (*DashboardStats, error) {
|
||||||
|
total, producing, completed, err := s.planRepo.CountByStatus(ctx, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
productCount, err := s.productRepo.Count(ctx, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalStock, err := s.productRepo.TotalStock(ctx, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unpaid, err := s.apRepo.TotalUnpaid(ctx, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recentPlans, err := s.planRepo.Recent(ctx, companyID, 5)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DashboardStats{
|
||||||
|
TotalPlans: total,
|
||||||
|
ProducingPlans: producing,
|
||||||
|
CompletedPlans: completed,
|
||||||
|
TotalProducts: productCount,
|
||||||
|
TotalStock: totalStock,
|
||||||
|
UnpaidAmount: unpaid,
|
||||||
|
RecentPlans: recentPlans,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
20
purchaser-service/internal/service/factory_service.go
Normal file
20
purchaser-service/internal/service/factory_service.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FactoryService struct {
|
||||||
|
repo *repository.FactoryRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactoryService(repo *repository.FactoryRepo) *FactoryService {
|
||||||
|
return &FactoryService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FactoryService) List(ctx context.Context, companyID string) ([]model.CompanyRelationship, error) {
|
||||||
|
return s.repo.ListRelationships(ctx, companyID)
|
||||||
|
}
|
||||||
213
purchaser-service/internal/service/plan_service.go
Normal file
213
purchaser-service/internal/service/plan_service.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
"github.com/muyuqingfeng/iloom/shared/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlanService struct {
|
||||||
|
repo *repository.PlanRepo
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlanService(repo *repository.PlanRepo, db *sql.DB) *PlanService {
|
||||||
|
return &PlanService{repo: repo, db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePlanRequest struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
TargetQuantity float64 `json:"target_quantity"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnUsagePerMeter *float64 `json:"yarn_usage_per_meter"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
YarnRatios []CreateYarnRatio `json:"yarn_ratios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateYarnRatio struct {
|
||||||
|
YarnName string `json:"yarn_name"`
|
||||||
|
YarnType string `json:"yarn_type"`
|
||||||
|
Ratio float64 `json:"ratio"`
|
||||||
|
AmountPerMeter float64 `json:"amount_per_meter"`
|
||||||
|
TotalAmount float64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePlanRequest struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
TargetQuantity float64 `json:"target_quantity"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnUsagePerMeter *float64 `json:"yarn_usage_per_meter"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) List(ctx context.Context, companyID string, page types.Pagination, status string) ([]model.ProductionPlan, int, error) {
|
||||||
|
page.Normalize()
|
||||||
|
return s.repo.List(ctx, companyID, page, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) Create(ctx context.Context, companyID, userID string, req CreatePlanRequest) (*model.ProductionPlan, error) {
|
||||||
|
now := time.Now()
|
||||||
|
planCode := fmt.Sprintf("PP%s%03d", now.Format("20060102"), now.UnixMilli()%1000)
|
||||||
|
|
||||||
|
var remarkPtr *string
|
||||||
|
if req.Remark != "" {
|
||||||
|
remarkPtr = &req.Remark
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := &model.ProductionPlan{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
PlanCode: planCode,
|
||||||
|
PurchaserID: companyID,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
TargetQuantity: req.TargetQuantity,
|
||||||
|
CompletedQuantity: 0,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
Status: string(types.PlanPending),
|
||||||
|
Remark: remarkPtr,
|
||||||
|
CreatedBy: &userID,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
var yarnRatios []model.YarnRatio
|
||||||
|
for _, yr := range req.YarnRatios {
|
||||||
|
var yarnTypePtr *string
|
||||||
|
if yr.YarnType != "" {
|
||||||
|
yarnTypePtr = &yr.YarnType
|
||||||
|
}
|
||||||
|
yarnRatios = append(yarnRatios, model.YarnRatio{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
PlanID: plan.ID,
|
||||||
|
YarnName: yr.YarnName,
|
||||||
|
YarnType: yarnTypePtr,
|
||||||
|
Ratio: yr.Ratio,
|
||||||
|
AmountPerMeter: yr.AmountPerMeter,
|
||||||
|
TotalAmount: yr.TotalAmount,
|
||||||
|
CompanyID: &companyID,
|
||||||
|
CreatedAt: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, tx, plan); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(yarnRatios) > 0 {
|
||||||
|
if err := s.repo.CreateYarnRatios(ctx, tx, yarnRatios); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, fmt.Errorf("commit tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.YarnRatios = yarnRatios
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) GetByID(ctx context.Context, id, companyID string) (*model.ProductionPlan, error) {
|
||||||
|
plan, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
return nil, fmt.Errorf("plan not found")
|
||||||
|
}
|
||||||
|
if plan.PurchaserID != companyID {
|
||||||
|
return nil, fmt.Errorf("plan not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
steps, err := s.repo.GetSteps(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
plan.ProcessSteps = steps
|
||||||
|
|
||||||
|
inventory, err := s.repo.GetInventory(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
plan.InventoryRecords = inventory
|
||||||
|
|
||||||
|
return plan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) Update(ctx context.Context, id, companyID string, req UpdatePlanRequest) error {
|
||||||
|
plan, err := s.repo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if plan == nil || plan.PurchaserID != companyID {
|
||||||
|
return fmt.Errorf("plan not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ProductName != "" {
|
||||||
|
plan.ProductName = req.ProductName
|
||||||
|
}
|
||||||
|
if req.FabricCode != "" {
|
||||||
|
plan.FabricCode = req.FabricCode
|
||||||
|
}
|
||||||
|
if req.Color != "" {
|
||||||
|
plan.Color = req.Color
|
||||||
|
}
|
||||||
|
if req.ColorCode != "" {
|
||||||
|
plan.ColorCode = req.ColorCode
|
||||||
|
}
|
||||||
|
if req.TargetQuantity > 0 {
|
||||||
|
plan.TargetQuantity = req.TargetQuantity
|
||||||
|
}
|
||||||
|
if req.ProductionPrice != nil {
|
||||||
|
plan.ProductionPrice = req.ProductionPrice
|
||||||
|
}
|
||||||
|
if req.YarnUsagePerMeter != nil {
|
||||||
|
plan.YarnUsagePerMeter = req.YarnUsagePerMeter
|
||||||
|
}
|
||||||
|
if req.Status != "" {
|
||||||
|
plan.Status = req.Status
|
||||||
|
}
|
||||||
|
if req.Remark != "" {
|
||||||
|
plan.Remark = &req.Remark
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.Update(ctx, plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) Delete(ctx context.Context, id, companyID string) error {
|
||||||
|
return s.repo.Delete(ctx, id, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) AssignFactory(ctx context.Context, planID, factoryID, factoryType string) error {
|
||||||
|
return s.repo.AssignFactory(ctx, planID, factoryID, factoryType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) GetSteps(ctx context.Context, planID string) ([]model.ProcessStep, error) {
|
||||||
|
return s.repo.GetSteps(ctx, planID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PlanService) GetInventory(ctx context.Context, planID string) ([]model.InventoryRecord, error) {
|
||||||
|
return s.repo.GetInventory(ctx, planID)
|
||||||
|
}
|
||||||
244
purchaser-service/internal/service/product_service.go
Normal file
244
purchaser-service/internal/service/product_service.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductService struct {
|
||||||
|
repo *repository.ProductRepo
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductService(repo *repository.ProductRepo, db *sql.DB) *ProductService {
|
||||||
|
return &ProductService{repo: repo, db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProductRequest struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnTypes []string `json:"yarn_types"`
|
||||||
|
YarnRatios []float64 `json:"yarn_ratios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProductRequest struct {
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
FabricCode string `json:"fabric_code"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
ColorCode string `json:"color_code"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"`
|
||||||
|
ProductionPrice *float64 `json:"production_price"`
|
||||||
|
YarnTypes []string `json:"yarn_types"`
|
||||||
|
YarnRatios []float64 `json:"yarn_ratios"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboundRequest struct {
|
||||||
|
BatchNo string `json:"batch_no"`
|
||||||
|
Rolls int `json:"rolls"`
|
||||||
|
Meters float64 `json:"meters"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
WarehouseID string `json:"warehouse_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundRequest struct {
|
||||||
|
BatchNo string `json:"batch_no"`
|
||||||
|
Rolls int `json:"rolls"`
|
||||||
|
Meters float64 `json:"meters"`
|
||||||
|
OutboundType string `json:"outbound_type"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
RecipientCompanyID string `json:"recipient_company_id"`
|
||||||
|
RecipientCompanyName string `json:"recipient_company_name"`
|
||||||
|
SourceBatchID string `json:"source_batch_id"`
|
||||||
|
WashingPlanID string `json:"washing_plan_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) List(ctx context.Context, companyID string) ([]model.Product, error) {
|
||||||
|
return s.repo.List(ctx, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) Create(ctx context.Context, companyID string, req CreateProductRequest) (*model.Product, error) {
|
||||||
|
now := time.Now()
|
||||||
|
p := &model.Product{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
CompanyID: companyID,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
Weight: req.Weight,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
TotalStock: 0,
|
||||||
|
RawFabricMeters: 0,
|
||||||
|
RawFabricRolls: 0,
|
||||||
|
YarnTypes: req.YarnTypes,
|
||||||
|
YarnRatios: req.YarnRatios,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) Update(ctx context.Context, id, companyID string, req UpdateProductRequest) error {
|
||||||
|
p := &model.Product{
|
||||||
|
ID: id,
|
||||||
|
ProductName: req.ProductName,
|
||||||
|
FabricCode: req.FabricCode,
|
||||||
|
Color: req.Color,
|
||||||
|
ColorCode: req.ColorCode,
|
||||||
|
Weight: req.Weight,
|
||||||
|
YarnUsagePerMeter: req.YarnUsagePerMeter,
|
||||||
|
ProductionPrice: req.ProductionPrice,
|
||||||
|
YarnTypes: req.YarnTypes,
|
||||||
|
YarnRatios: req.YarnRatios,
|
||||||
|
}
|
||||||
|
return s.repo.Update(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) Delete(ctx context.Context, id, companyID string) error {
|
||||||
|
return s.repo.Delete(ctx, id, companyID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) Inbound(ctx context.Context, productID, companyID, userID string, req InboundRequest) error {
|
||||||
|
now := time.Now()
|
||||||
|
var notesPtr *string
|
||||||
|
if req.Notes != "" {
|
||||||
|
notesPtr = &req.Notes
|
||||||
|
}
|
||||||
|
var warehousePtr *string
|
||||||
|
if req.WarehouseID != "" {
|
||||||
|
warehousePtr = &req.WarehouseID
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := &model.ProductInventoryRecord{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
ProductID: productID,
|
||||||
|
BatchNo: req.BatchNo,
|
||||||
|
Rolls: req.Rolls,
|
||||||
|
Meters: req.Meters,
|
||||||
|
Notes: notesPtr,
|
||||||
|
OperatorID: &userID,
|
||||||
|
CompanyID: &companyID,
|
||||||
|
WarehouseID: warehousePtr,
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if err := s.repo.Inbound(ctx, tx, rec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.repo.UpdateStock(ctx, tx, productID, req.Meters, req.Rolls); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) Outbound(ctx context.Context, productID, companyID, userID string, req OutboundRequest) error {
|
||||||
|
now := time.Now()
|
||||||
|
var notesPtr *string
|
||||||
|
if req.Notes != "" {
|
||||||
|
notesPtr = &req.Notes
|
||||||
|
}
|
||||||
|
var recipientIDPtr, recipientNamePtr, sourceBatchPtr, washingPlanPtr *string
|
||||||
|
if req.RecipientCompanyID != "" {
|
||||||
|
recipientIDPtr = &req.RecipientCompanyID
|
||||||
|
}
|
||||||
|
if req.RecipientCompanyName != "" {
|
||||||
|
recipientNamePtr = &req.RecipientCompanyName
|
||||||
|
}
|
||||||
|
if req.SourceBatchID != "" {
|
||||||
|
sourceBatchPtr = &req.SourceBatchID
|
||||||
|
}
|
||||||
|
if req.WashingPlanID != "" {
|
||||||
|
washingPlanPtr = &req.WashingPlanID
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundType := req.OutboundType
|
||||||
|
if outboundType == "" {
|
||||||
|
outboundType = "normal"
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := &model.ProductOutboundRecord{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
ProductID: productID,
|
||||||
|
BatchNo: req.BatchNo,
|
||||||
|
Rolls: req.Rolls,
|
||||||
|
Meters: req.Meters,
|
||||||
|
OutboundType: outboundType,
|
||||||
|
Notes: notesPtr,
|
||||||
|
OperatorID: &userID,
|
||||||
|
CompanyID: &companyID,
|
||||||
|
RecipientCompanyID: recipientIDPtr,
|
||||||
|
RecipientCompanyName: recipientNamePtr,
|
||||||
|
SourceBatchID: sourceBatchPtr,
|
||||||
|
WashingPlanID: washingPlanPtr,
|
||||||
|
CreatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if err := s.repo.Outbound(ctx, tx, rec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.repo.UpdateStock(ctx, tx, productID, -req.Meters, -req.Rolls); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) Records(ctx context.Context, productID string) ([]model.ProductInventoryRecord, error) {
|
||||||
|
return s.repo.Records(ctx, productID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) UpdatePrice(ctx context.Context, productID, companyID, userID string, newPrice float64, reason string) error {
|
||||||
|
oldPrice, err := s.repo.GetPrice(ctx, productID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if err := s.repo.UpdatePrice(ctx, tx, productID, newPrice); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.repo.InsertPriceHistory(ctx, tx, productID, oldPrice, newPrice, reason, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProductService) PriceHistory(ctx context.Context, productID string) ([]model.ProductPriceHistory, error) {
|
||||||
|
return s.repo.PriceHistory(ctx, productID)
|
||||||
|
}
|
||||||
76
purchaser-service/internal/service/washing_plan_service.go
Normal file
76
purchaser-service/internal/service/washing_plan_service.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/model"
|
||||||
|
"github.com/muyuqingfeng/iloom/purchaser-service/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WashingPlanService struct {
|
||||||
|
repo *repository.WashingPlanRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWashingPlanService(repo *repository.WashingPlanRepo) *WashingPlanService {
|
||||||
|
return &WashingPlanService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateWashingPlanRequest struct {
|
||||||
|
ProductID string `json:"product_id" binding:"required"`
|
||||||
|
WashingFactoryID string `json:"washing_factory_id"`
|
||||||
|
PlannedMeters float64 `json:"planned_meters" binding:"required,gt=0"`
|
||||||
|
WashingPrice float64 `json:"washing_price" binding:"required,gt=0"`
|
||||||
|
EstimatedShrinkageRate float64 `json:"estimated_shrinkage_rate"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WashingPlanService) Create(ctx context.Context, companyID, userID string, req CreateWashingPlanRequest) (*model.WashingPlan, error) {
|
||||||
|
planCode, err := s.repo.GeneratePlanCode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
estimatedWashedMeters := req.PlannedMeters * (1 - req.EstimatedShrinkageRate/100)
|
||||||
|
estimatedTotalCost := req.PlannedMeters * req.WashingPrice
|
||||||
|
|
||||||
|
var washingFactoryPtr *string
|
||||||
|
if req.WashingFactoryID != "" {
|
||||||
|
washingFactoryPtr = &req.WashingFactoryID
|
||||||
|
}
|
||||||
|
var notesPtr *string
|
||||||
|
if req.Notes != "" {
|
||||||
|
notesPtr = &req.Notes
|
||||||
|
}
|
||||||
|
|
||||||
|
wp := &model.WashingPlan{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
PlanCode: planCode,
|
||||||
|
CompanyID: companyID,
|
||||||
|
ProductID: req.ProductID,
|
||||||
|
WashingFactoryID: washingFactoryPtr,
|
||||||
|
PlannedMeters: req.PlannedMeters,
|
||||||
|
WashingPrice: req.WashingPrice,
|
||||||
|
EstimatedShrinkageRate: req.EstimatedShrinkageRate,
|
||||||
|
EstimatedWashedMeters: &estimatedWashedMeters,
|
||||||
|
EstimatedTotalCost: &estimatedTotalCost,
|
||||||
|
Status: "pending",
|
||||||
|
Notes: notesPtr,
|
||||||
|
CreatedBy: &userID,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, wp); err != nil {
|
||||||
|
return nil, fmt.Errorf("create washing plan: %w", err)
|
||||||
|
}
|
||||||
|
return wp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WashingPlanService) List(ctx context.Context, companyID string) ([]model.WashingPlan, error) {
|
||||||
|
return s.repo.List(ctx, companyID)
|
||||||
|
}
|
||||||
463
scripts/init-iloom.sql
Normal file
463
scripts/init-iloom.sql
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
-- iloom Business Tables for muyu_wms Database
|
||||||
|
-- All tables use ilm_ prefix to coexist with muyu-apiserver tables
|
||||||
|
-- This script should be run AFTER muyu's init.sql
|
||||||
|
|
||||||
|
USE `muyu_wms`;
|
||||||
|
|
||||||
|
-- ==================== iloom Roles & Permissions ====================
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `sys_role` (`role_id`, `role_name`, `role_key`, `role_desc`, `sort_order`, `status`) VALUES
|
||||||
|
('r_010', '采购商', 'purchaser', 'iloom purchaser role', 10, 1),
|
||||||
|
('r_011', '纺织厂', 'textile', 'iloom textile factory role', 11, 1),
|
||||||
|
('r_012', '水洗厂', 'washing', 'iloom washing factory role', 12, 1);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `casbin_rule` (`ptype`, `v0`, `v1`, `v2`) VALUES
|
||||||
|
('p', 'purchaser', '/api/v1/purchaser/*', '*'),
|
||||||
|
('p', 'purchaser', '/api/v1/common/*', '*'),
|
||||||
|
('p', 'purchaser', '/api/v1/auth/*', '*'),
|
||||||
|
('p', 'textile', '/api/v1/textile/*', '*'),
|
||||||
|
('p', 'textile', '/api/v1/common/*', '*'),
|
||||||
|
('p', 'textile', '/api/v1/auth/*', '*'),
|
||||||
|
('p', 'washing', '/api/v1/washing/*', '*'),
|
||||||
|
('p', 'washing', '/api/v1/common/*', '*'),
|
||||||
|
('p', 'washing', '/api/v1/auth/*', '*');
|
||||||
|
|
||||||
|
-- ==================== Company & Multi-tenancy ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_company` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'purchaser|textile|washing',
|
||||||
|
`address` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`phone` VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
`contact_person` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`status` TINYINT NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_type` (`type`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_company_member` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`user_id` VARCHAR(32) NOT NULL,
|
||||||
|
`role` VARCHAR(20) NOT NULL DEFAULT 'member' COMMENT 'owner|admin|member',
|
||||||
|
`is_master` TINYINT NOT NULL DEFAULT 0,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_company_user` (`company_id`, `user_id`),
|
||||||
|
KEY `idx_user_id` (`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_company_relationship` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`from_company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`to_company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`relationship_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'supplier|customer|partner',
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_relationship` (`from_company_id`, `to_company_id`, `relationship_type`),
|
||||||
|
KEY `idx_to_company` (`to_company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Product Extension ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_product_ext` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` VARCHAR(32) NOT NULL,
|
||||||
|
`company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`fabric_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`color_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`yarn_usage_per_meter` DECIMAL(10,4) DEFAULT NULL,
|
||||||
|
`current_price` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`warp_weight` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`weft_weight` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`total_weight` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`stock_meters` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`stock_rolls` INT NOT NULL DEFAULT 0,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_product_company` (`product_id`, `company_id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Production Plans ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_production_plan` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_code` VARCHAR(50) NOT NULL,
|
||||||
|
`purchaser_id` VARCHAR(36) NOT NULL COMMENT 'company_id of purchaser',
|
||||||
|
`product_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`fabric_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`color` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`color_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`target_quantity` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`completed_quantity` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`production_price` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`yarn_usage_per_meter` DECIMAL(10,4) DEFAULT NULL,
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft|producing|completed|cancelled',
|
||||||
|
`remark` TEXT,
|
||||||
|
`start_time` DATETIME DEFAULT NULL,
|
||||||
|
`created_by` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_plan_code` (`plan_code`),
|
||||||
|
KEY `idx_purchaser` (`purchaser_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_plan_factory` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL,
|
||||||
|
`factory_id` VARCHAR(36) NOT NULL,
|
||||||
|
`factory_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'textile|washing',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_plan` (`plan_id`),
|
||||||
|
KEY `idx_factory` (`factory_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_yarn_ratio` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL,
|
||||||
|
`yarn_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`yarn_type` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`ratio` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`amount_per_meter` DECIMAL(10,4) DEFAULT NULL,
|
||||||
|
`total_amount` DECIMAL(12,2) DEFAULT NULL,
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_plan` (`plan_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Production Process ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_process_step` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL,
|
||||||
|
`step_type` VARCHAR(30) NOT NULL DEFAULT '' COMMENT 'warping|sizing|weaving|inspection|finishing',
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|in_progress|completed|rejected',
|
||||||
|
`notes` TEXT,
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`timestamp` DATETIME DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_plan` (`plan_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Warehousing ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_warehouse` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`location` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`capacity` DECIMAL(12,2) DEFAULT NULL,
|
||||||
|
`status` TINYINT NOT NULL DEFAULT 1,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_inventory_record` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`warehouse_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`quantity` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`rolls` INT DEFAULT NULL,
|
||||||
|
`price_per_meter` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`price_note` TEXT,
|
||||||
|
`warehouse_location` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`record_type` VARCHAR(20) NOT NULL DEFAULT 'inbound' COMMENT 'inbound|outbound',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_plan` (`plan_id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Product Inventory (Purchaser) ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_product_inventory` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`product_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`batch_no` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`rolls` INT NOT NULL DEFAULT 0,
|
||||||
|
`meters` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`record_type` VARCHAR(20) NOT NULL DEFAULT 'inbound' COMMENT 'inbound|outbound',
|
||||||
|
`notes` TEXT,
|
||||||
|
`warehouse_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`plan_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`factory_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`source` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'self|textile',
|
||||||
|
`price_per_meter` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_product` (`product_id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_product_outbound` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`product_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`batch_no` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`rolls` INT NOT NULL DEFAULT 0,
|
||||||
|
`meters` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`outbound_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'sale|washing|transfer',
|
||||||
|
`notes` TEXT,
|
||||||
|
`washing_plan_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`recipient_company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_product` (`product_id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Price History ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_product_price_history` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`product_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`reason` TEXT,
|
||||||
|
`changed_by` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_product` (`product_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_production_price_history` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`price` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`reason` TEXT,
|
||||||
|
`changed_by` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_plan` (`plan_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Accounts Payable ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_accounts_payable` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`purchaser_id` VARCHAR(36) NOT NULL,
|
||||||
|
`factory_id` VARCHAR(36) NOT NULL,
|
||||||
|
`factory_type` VARCHAR(20) NOT NULL DEFAULT '',
|
||||||
|
`total_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`paid_amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|partial|paid',
|
||||||
|
`notes` TEXT,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_purchaser` (`purchaser_id`),
|
||||||
|
KEY `idx_factory` (`factory_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_ap_item` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`payable_id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`description` TEXT,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_payable` (`payable_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Washing Plans ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_washing_plan` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`plan_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`purchaser_id` VARCHAR(36) NOT NULL,
|
||||||
|
`washing_factory_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`product_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`fabric_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`color` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`quantity` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`unit_price` DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|accepted|processing|completed|rejected',
|
||||||
|
`notes` TEXT,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_purchaser` (`purchaser_id`),
|
||||||
|
KEY `idx_washing_factory` (`washing_factory_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Washing Process ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_washing_step` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`washing_plan_id` VARCHAR(36) NOT NULL,
|
||||||
|
`step_type` VARCHAR(30) NOT NULL DEFAULT '',
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
|
`notes` TEXT,
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`timestamp` DATETIME DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_washing_plan` (`washing_plan_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_washing_completion` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`washing_plan_id` VARCHAR(36) NOT NULL,
|
||||||
|
`completed_quantity` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`quality_notes` TEXT,
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`completed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_washing_plan` (`washing_plan_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Finished Products ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_finished_product` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`product_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`fabric_code` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`color` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`stock_meters` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`stock_rolls` INT NOT NULL DEFAULT 0,
|
||||||
|
`company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`washing_plan_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_finished_product_inventory` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`finished_product_id` VARCHAR(36) NOT NULL,
|
||||||
|
`batch_no` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`rolls` INT NOT NULL DEFAULT 0,
|
||||||
|
`meters` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`record_type` VARCHAR(20) NOT NULL DEFAULT 'inbound',
|
||||||
|
`notes` TEXT,
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_finished_product` (`finished_product_id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Yarn Stock ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_yarn_stock` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`yarn_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
`yarn_type` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`current_stock` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`unit` VARCHAR(20) NOT NULL DEFAULT 'kg',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_yarn_stock_record` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`yarn_stock_id` VARCHAR(36) NOT NULL,
|
||||||
|
`quantity` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`record_type` VARCHAR(20) NOT NULL DEFAULT 'in' COMMENT 'in|out',
|
||||||
|
`notes` TEXT,
|
||||||
|
`operator_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_yarn_stock` (`yarn_stock_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Payments ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_payment` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`payer_company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`payee_company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`payment_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'production|washing',
|
||||||
|
`plan_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending|confirmed|rejected',
|
||||||
|
`notes` TEXT,
|
||||||
|
`confirmed_at` DATETIME DEFAULT NULL,
|
||||||
|
`confirmed_by` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_payer` (`payer_company_id`),
|
||||||
|
KEY `idx_payee` (`payee_company_id`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ==================== Notifications & Misc ====================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_notification` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`user_id` VARCHAR(32) NOT NULL,
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`title` VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
|
`message` TEXT,
|
||||||
|
`type` VARCHAR(30) NOT NULL DEFAULT 'info' COMMENT 'info|warning|success|error',
|
||||||
|
`is_read` TINYINT NOT NULL DEFAULT 0,
|
||||||
|
`link` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_unread` (`user_id`, `is_read`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_share_link` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`code` VARCHAR(20) NOT NULL,
|
||||||
|
`company_id` VARCHAR(36) NOT NULL,
|
||||||
|
`created_by` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`resource_type` VARCHAR(30) NOT NULL DEFAULT '',
|
||||||
|
`resource_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`expires_at` DATETIME DEFAULT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_code` (`code`),
|
||||||
|
KEY `idx_company` (`company_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `ilm_audit_log` (
|
||||||
|
`id` VARCHAR(36) NOT NULL,
|
||||||
|
`user_id` VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
`company_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`action` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`resource_type` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`resource_id` VARCHAR(36) NOT NULL DEFAULT '',
|
||||||
|
`old_value` JSON DEFAULT NULL,
|
||||||
|
`new_value` JSON DEFAULT NULL,
|
||||||
|
`ip` VARCHAR(50) NOT NULL DEFAULT '',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user` (`user_id`),
|
||||||
|
KEY `idx_company` (`company_id`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
639
scripts/init-schemas.sql
Normal file
639
scripts/init-schemas.sql
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
-- iloom 微服务数据库初始化脚本
|
||||||
|
-- 创建 5 个 Schema,迁移表结构,建立跨 Schema 视图
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 1. 创建 Schema
|
||||||
|
-- =============================================
|
||||||
|
CREATE SCHEMA IF NOT EXISTS common;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS purchaser;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS textile;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS washing;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS audit;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 2. 创建枚举类型(在 public schema,所有 schema 共享)
|
||||||
|
-- =============================================
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE app_role AS ENUM ('purchaser', 'textile', 'washing');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE factory_type AS ENUM ('textile', 'washing');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE plan_status AS ENUM ('pending', 'producing', 'completed');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE step_status AS ENUM ('pending', 'active', 'completed', 'rejected');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE step_type AS ENUM ('confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE warehouse_type AS ENUM ('raw_fabric', 'fabric', 'finished', 'yarn');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE payment_status AS ENUM ('pending', 'completed');
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 3. common Schema 表
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.companies (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
role app_role NOT NULL,
|
||||||
|
address TEXT,
|
||||||
|
contact_phone TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.profiles (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
phone TEXT,
|
||||||
|
company_id UUID REFERENCES common.companies(id),
|
||||||
|
is_master BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
master_id UUID REFERENCES common.profiles(id),
|
||||||
|
display_name TEXT,
|
||||||
|
image_url TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.company_members (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
user_id UUID NOT NULL REFERENCES common.profiles(id),
|
||||||
|
role TEXT NOT NULL DEFAULT 'member',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.company_relationships (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
purchaser_company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
factory_company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
factory_type factory_type NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.notifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
recipient_id UUID NOT NULL,
|
||||||
|
sender_id UUID,
|
||||||
|
sender_company_id UUID REFERENCES common.companies(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
plan_id UUID,
|
||||||
|
is_read BOOLEAN DEFAULT false,
|
||||||
|
read_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.share_links (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL,
|
||||||
|
created_by UUID NOT NULL,
|
||||||
|
short_code TEXT NOT NULL UNIQUE,
|
||||||
|
factory_type TEXT NOT NULL,
|
||||||
|
hide_sensitive BOOLEAN DEFAULT false,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
clicked_at TIMESTAMPTZ,
|
||||||
|
click_count INT DEFAULT 0,
|
||||||
|
used_at TIMESTAMPTZ,
|
||||||
|
response_result TEXT,
|
||||||
|
reject_reason TEXT,
|
||||||
|
responded_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.user_feedback (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
contact TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS common.crash_reports (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID,
|
||||||
|
error_message TEXT NOT NULL,
|
||||||
|
error_stack TEXT,
|
||||||
|
component_stack TEXT,
|
||||||
|
url TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
user_description TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'new',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 4. purchaser Schema 表
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.production_plans (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_code TEXT NOT NULL,
|
||||||
|
purchaser_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
product_name TEXT NOT NULL,
|
||||||
|
fabric_code TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
color_code TEXT NOT NULL DEFAULT '',
|
||||||
|
target_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
completed_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
production_price NUMERIC,
|
||||||
|
yarn_usage_per_meter NUMERIC,
|
||||||
|
status plan_status NOT NULL DEFAULT 'pending',
|
||||||
|
remark TEXT,
|
||||||
|
start_time TIMESTAMPTZ,
|
||||||
|
created_by UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.plan_factories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id) ON DELETE CASCADE,
|
||||||
|
factory_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
factory_type factory_type NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.yarn_ratios (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id) ON DELETE CASCADE,
|
||||||
|
yarn_name TEXT NOT NULL,
|
||||||
|
yarn_type TEXT,
|
||||||
|
ratio NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
amount_per_meter NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
total_amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
company_id UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
product_name TEXT NOT NULL,
|
||||||
|
fabric_code TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
color_code TEXT NOT NULL DEFAULT '',
|
||||||
|
weight NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
warp_weight NUMERIC,
|
||||||
|
weft_weight NUMERIC,
|
||||||
|
total_weight NUMERIC,
|
||||||
|
yarn_usage_per_meter NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
production_price NUMERIC,
|
||||||
|
total_stock NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
raw_fabric_meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
raw_fabric_rolls INT NOT NULL DEFAULT 0,
|
||||||
|
image_url TEXT,
|
||||||
|
yarn_types TEXT[],
|
||||||
|
yarn_ratios NUMERIC[],
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.product_inventory_records (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
product_id UUID NOT NULL REFERENCES purchaser.products(id) ON DELETE CASCADE,
|
||||||
|
batch_no TEXT NOT NULL,
|
||||||
|
rolls INT NOT NULL DEFAULT 0,
|
||||||
|
meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
company_id UUID,
|
||||||
|
warehouse_id UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.product_outbound_records (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
product_id UUID NOT NULL REFERENCES purchaser.products(id) ON DELETE CASCADE,
|
||||||
|
batch_no TEXT NOT NULL DEFAULT '',
|
||||||
|
rolls INT NOT NULL DEFAULT 0,
|
||||||
|
meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
outbound_type TEXT NOT NULL DEFAULT 'normal',
|
||||||
|
notes TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
company_id UUID,
|
||||||
|
recipient_company_id UUID,
|
||||||
|
recipient_company_name TEXT,
|
||||||
|
source_batch_id UUID,
|
||||||
|
washing_plan_id UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.product_yarn_ratios (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
product_id UUID NOT NULL REFERENCES purchaser.products(id) ON DELETE CASCADE,
|
||||||
|
yarn_name TEXT NOT NULL,
|
||||||
|
yarn_type TEXT,
|
||||||
|
ratio NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.product_price_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
product_id UUID NOT NULL REFERENCES purchaser.products(id) ON DELETE CASCADE,
|
||||||
|
company_id UUID NOT NULL,
|
||||||
|
old_price NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
new_price NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
change_amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
change_reason TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
operator_name TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.production_price_history (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id) ON DELETE CASCADE,
|
||||||
|
old_price NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
new_price NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
change_amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
change_reason TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
operator_name TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.accounts_payable (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id),
|
||||||
|
purchaser_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
textile_factory_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
price_per_meter NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
total_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
total_amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
paid_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
paid_amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
unpaid_quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
unpaid_amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.accounts_payable_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
accounts_payable_id UUID NOT NULL REFERENCES purchaser.accounts_payable(id) ON DELETE CASCADE,
|
||||||
|
inventory_record_id UUID,
|
||||||
|
payment_id UUID,
|
||||||
|
quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
price_per_meter NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
rolls INT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
paid_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS purchaser.washing_plans (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_code TEXT NOT NULL,
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
product_id UUID NOT NULL REFERENCES purchaser.products(id),
|
||||||
|
washing_factory_id UUID REFERENCES common.companies(id),
|
||||||
|
planned_meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
washing_price NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
estimated_shrinkage_rate NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
estimated_washed_meters NUMERIC,
|
||||||
|
estimated_total_cost NUMERIC,
|
||||||
|
actual_shrinkage_rate NUMERIC,
|
||||||
|
actual_washed_meters NUMERIC,
|
||||||
|
actual_total_cost NUMERIC,
|
||||||
|
washing_date TIMESTAMPTZ,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
notes TEXT,
|
||||||
|
created_by UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 5. textile Schema 表
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS textile.plan_process_steps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id) ON DELETE CASCADE,
|
||||||
|
step_type step_type NOT NULL,
|
||||||
|
status step_status NOT NULL DEFAULT 'pending',
|
||||||
|
notes TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
company_id UUID,
|
||||||
|
timestamp TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS textile.warehouses (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type warehouse_type NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS textile.inventory_records (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id),
|
||||||
|
warehouse_id UUID NOT NULL REFERENCES textile.warehouses(id),
|
||||||
|
quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
rolls INT,
|
||||||
|
price_per_meter NUMERIC,
|
||||||
|
price_note TEXT,
|
||||||
|
warehouse_location TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
company_id UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS textile.yarn_stock (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
spec TEXT,
|
||||||
|
quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
unit TEXT NOT NULL DEFAULT 'kg',
|
||||||
|
min_stock NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
warehouse_id UUID REFERENCES textile.warehouses(id),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS textile.yarn_stock_records (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
yarn_stock_id UUID REFERENCES textile.yarn_stock(id),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
record_type TEXT NOT NULL,
|
||||||
|
quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
unit TEXT NOT NULL DEFAULT 'kg',
|
||||||
|
batch_no TEXT,
|
||||||
|
plan_id UUID,
|
||||||
|
notes TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS textile.payments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plan_id UUID NOT NULL REFERENCES purchaser.production_plans(id),
|
||||||
|
from_company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
to_company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
amount NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
quantity NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
price_per_meter NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
status payment_status NOT NULL DEFAULT 'pending',
|
||||||
|
paid_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 6. washing Schema 表
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS washing.washing_process_steps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
washing_plan_id UUID NOT NULL REFERENCES purchaser.washing_plans(id) ON DELETE CASCADE,
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
step_type TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
notes TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
timestamp TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS washing.washing_plan_completions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
washing_plan_id UUID NOT NULL REFERENCES purchaser.washing_plans(id),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
actual_shrinkage_rate NUMERIC NOT NULL,
|
||||||
|
actual_washed_meters NUMERIC NOT NULL,
|
||||||
|
actual_washing_cost NUMERIC,
|
||||||
|
unit_cost NUMERIC,
|
||||||
|
rolls INT NOT NULL DEFAULT 0,
|
||||||
|
washing_date DATE NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
completed_by UUID,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
finished_product_id UUID,
|
||||||
|
auto_imported BOOLEAN DEFAULT false,
|
||||||
|
warehouse_id UUID,
|
||||||
|
warehouse_location TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS washing.finished_products (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
product_name TEXT NOT NULL,
|
||||||
|
fabric_code TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
color_code TEXT,
|
||||||
|
weight NUMERIC,
|
||||||
|
shrinkage_rate NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
washed_meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
stock_meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
stock_rolls INT NOT NULL DEFAULT 0,
|
||||||
|
rolls INT,
|
||||||
|
washing_cost NUMERIC,
|
||||||
|
unit_cost NUMERIC,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
notes TEXT,
|
||||||
|
product_id UUID,
|
||||||
|
washing_plan_id UUID,
|
||||||
|
warehouse_id UUID,
|
||||||
|
warehouse_location TEXT,
|
||||||
|
created_by UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS washing.finished_product_inventory_records (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
finished_product_id UUID NOT NULL REFERENCES washing.finished_products(id) ON DELETE CASCADE,
|
||||||
|
company_id UUID NOT NULL REFERENCES common.companies(id),
|
||||||
|
record_type TEXT NOT NULL,
|
||||||
|
rolls INT NOT NULL DEFAULT 0,
|
||||||
|
meters NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
batch_no TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
operator_id UUID,
|
||||||
|
warehouse_id UUID,
|
||||||
|
warehouse_location TEXT,
|
||||||
|
washing_completion_id UUID,
|
||||||
|
related_order_id UUID,
|
||||||
|
related_order_type TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 7. audit Schema 表
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.audit_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID,
|
||||||
|
company_id UUID,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
resource_type TEXT NOT NULL,
|
||||||
|
resource_id UUID,
|
||||||
|
details JSONB,
|
||||||
|
ip_address TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.data_audit_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
table_name TEXT NOT NULL,
|
||||||
|
record_id TEXT NOT NULL,
|
||||||
|
operation TEXT NOT NULL,
|
||||||
|
old_values JSONB,
|
||||||
|
new_values JSONB,
|
||||||
|
changed_by UUID,
|
||||||
|
error_message TEXT,
|
||||||
|
validation_passed BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 8. 跨 Schema 只读视图
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- textile-service 需要读取分配给自己的计划
|
||||||
|
CREATE OR REPLACE VIEW textile.v_assigned_plans AS
|
||||||
|
SELECT
|
||||||
|
pp.id, pp.plan_code, pp.product_name, pp.fabric_code,
|
||||||
|
pp.color, pp.color_code, pp.target_quantity, pp.completed_quantity,
|
||||||
|
pp.production_price, pp.yarn_usage_per_meter, pp.status,
|
||||||
|
pp.remark, pp.start_time, pp.created_at, pp.updated_at,
|
||||||
|
pf.factory_id, pf.id AS plan_factory_id,
|
||||||
|
c.name AS purchaser_name
|
||||||
|
FROM purchaser.production_plans pp
|
||||||
|
JOIN purchaser.plan_factories pf ON pp.id = pf.plan_id
|
||||||
|
JOIN common.companies c ON pp.purchaser_id = c.id
|
||||||
|
WHERE pf.factory_type = 'textile';
|
||||||
|
|
||||||
|
-- washing-service 需要读取分配给自己的水洗计划
|
||||||
|
CREATE OR REPLACE VIEW washing.v_assigned_washing_plans AS
|
||||||
|
SELECT
|
||||||
|
wp.id, wp.plan_code, wp.company_id, wp.product_id,
|
||||||
|
wp.washing_factory_id, wp.planned_meters, wp.washing_price,
|
||||||
|
wp.estimated_shrinkage_rate, wp.estimated_washed_meters,
|
||||||
|
wp.estimated_total_cost, wp.actual_shrinkage_rate,
|
||||||
|
wp.actual_washed_meters, wp.actual_total_cost,
|
||||||
|
wp.washing_date, wp.status, wp.notes, wp.created_at, wp.updated_at,
|
||||||
|
p.product_name, p.fabric_code, p.color, p.color_code,
|
||||||
|
c.name AS purchaser_name
|
||||||
|
FROM purchaser.washing_plans wp
|
||||||
|
LEFT JOIN purchaser.products p ON wp.product_id = p.id
|
||||||
|
LEFT JOIN common.companies c ON wp.company_id = c.id;
|
||||||
|
|
||||||
|
-- purchaser-service 需要读取计划的流程步骤
|
||||||
|
CREATE OR REPLACE VIEW purchaser.v_plan_steps AS
|
||||||
|
SELECT
|
||||||
|
ps.id, ps.plan_id, ps.step_type, ps.status,
|
||||||
|
ps.notes, ps.operator_id, ps.company_id, ps.timestamp, ps.created_at,
|
||||||
|
pr.display_name AS operator_name
|
||||||
|
FROM textile.plan_process_steps ps
|
||||||
|
LEFT JOIN common.profiles pr ON ps.operator_id = pr.id;
|
||||||
|
|
||||||
|
-- purchaser-service 需要读取入库记录
|
||||||
|
CREATE OR REPLACE VIEW purchaser.v_plan_inventory AS
|
||||||
|
SELECT
|
||||||
|
ir.id, ir.plan_id, ir.warehouse_id, ir.quantity, ir.rolls,
|
||||||
|
ir.price_per_meter, ir.price_note, ir.warehouse_location,
|
||||||
|
ir.operator_id, ir.company_id, ir.created_at,
|
||||||
|
w.name AS warehouse_name,
|
||||||
|
pr.display_name AS operator_name
|
||||||
|
FROM textile.inventory_records ir
|
||||||
|
LEFT JOIN textile.warehouses w ON ir.warehouse_id = w.id
|
||||||
|
LEFT JOIN common.profiles pr ON ir.operator_id = pr.id;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 9. 索引
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_profiles_username ON common.profiles(username);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_profiles_company ON common.profiles(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_company_members_company ON common.company_members(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_purchaser ON common.company_relationships(purchaser_company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_factory ON common.company_relationships(factory_company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON common.notifications(recipient_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_share_links_code ON common.share_links(short_code);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_plans_purchaser ON purchaser.production_plans(purchaser_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_plans_status ON purchaser.production_plans(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_plan_factories_plan ON purchaser.plan_factories(plan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_plan_factories_factory ON purchaser.plan_factories(factory_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yarn_ratios_plan ON purchaser.yarn_ratios(plan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_products_company ON purchaser.products(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_inventory_product ON purchaser.product_inventory_records(product_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ap_purchaser ON purchaser.accounts_payable(purchaser_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_washing_plans_company ON purchaser.washing_plans(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_washing_plans_factory ON purchaser.washing_plans(washing_factory_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_process_steps_plan ON textile.plan_process_steps(plan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inventory_plan ON textile.inventory_records(plan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yarn_stock_company ON textile.yarn_stock(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_yarn_records_stock ON textile.yarn_stock_records(yarn_stock_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_payments_to ON textile.payments(to_company_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_washing_steps_plan ON washing.washing_process_steps(washing_plan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_completions_plan ON washing.washing_plan_completions(washing_plan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_finished_products_company ON washing.finished_products(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fp_inventory_product ON washing.finished_product_inventory_records(finished_product_id);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 10. 权限授予
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
GRANT USAGE ON SCHEMA common TO iloom;
|
||||||
|
GRANT USAGE ON SCHEMA purchaser TO iloom;
|
||||||
|
GRANT USAGE ON SCHEMA textile TO iloom;
|
||||||
|
GRANT USAGE ON SCHEMA washing TO iloom;
|
||||||
|
GRANT USAGE ON SCHEMA audit TO iloom;
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA common TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA purchaser TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA textile TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA washing TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA audit TO iloom;
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA common TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA purchaser TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA textile TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA washing TO iloom;
|
||||||
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA audit TO iloom;
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA common GRANT ALL ON TABLES TO iloom;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA purchaser GRANT ALL ON TABLES TO iloom;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA textile GRANT ALL ON TABLES TO iloom;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA washing GRANT ALL ON TABLES TO iloom;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA audit GRANT ALL ON TABLES TO iloom;
|
||||||
19
shared/cmd/bcryptcheck/main.go
Normal file
19
shared/cmd/bcryptcheck/main.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
hash := "$2a$10$S5XPWRXUnexAt9KJw2CefeHMa6aude7j5dm5qgWM1YMQAAwRgsGXa"
|
||||||
|
passwords := []string{"admin123", "muyu2026", "admin", "123456", "password", "Aa123456"}
|
||||||
|
for _, p := range passwords {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(p))
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("MATCH: %s\n", p)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%-12s → %v\n", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
shared/go.mod
Normal file
45
shared/go.mod
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
module github.com/muyuqingfeng/iloom/shared
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
golang.org/x/crypto v0.33.0
|
||||||
|
google.golang.org/grpc v1.72.0
|
||||||
|
google.golang.org/protobuf v1.36.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.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.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // 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-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
119
shared/go.sum
Normal file
119
shared/go.sum
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
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/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
|
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||||
|
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||||
|
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||||
|
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||||
|
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||||
|
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||||
|
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||||
|
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
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=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
12
shared/pkg/auth/claims.go
Normal file
12
shared/pkg/auth/claims.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
UserID string `json:"uid"`
|
||||||
|
CompanyID string `json:"cid"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
IsMaster bool `json:"master"`
|
||||||
|
Username string `json:"uname"`
|
||||||
|
}
|
||||||
91
shared/pkg/auth/jwt.go
Normal file
91
shared/pkg/auth/jwt.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultAccessTTL = 7200 * time.Second
|
||||||
|
refreshTTL = 30 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWTManager struct {
|
||||||
|
secret []byte
|
||||||
|
refreshSecret []byte
|
||||||
|
accessTTL time.Duration
|
||||||
|
refreshTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTManager(secret, refreshSecret string) *JWTManager {
|
||||||
|
return &JWTManager{
|
||||||
|
secret: []byte(secret),
|
||||||
|
refreshSecret: []byte(refreshSecret),
|
||||||
|
accessTTL: defaultAccessTTL,
|
||||||
|
refreshTTL: refreshTTL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) GenerateAccessToken(claims Claims) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims.RegisteredClaims = jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(m.accessTTL)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
Subject: claims.UserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(m.secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) GenerateRefreshToken(userID string) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims := jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(now.Add(m.refreshTTL)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
Subject: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(m.refreshSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) ParseAccessToken(tokenStr string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return m.secret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse access token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, fmt.Errorf("invalid access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *JWTManager) ParseRefreshToken(tokenStr string) (string, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
|
}
|
||||||
|
return m.refreshSecret, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*jwt.RegisteredClaims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return "", fmt.Errorf("invalid refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.Subject, nil
|
||||||
|
}
|
||||||
15
shared/pkg/auth/password.go
Normal file
15
shared/pkg/auth/password.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPassword(password, hash string) bool {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||||
|
}
|
||||||
71
shared/pkg/database/mysql.go
Normal file
71
shared/pkg/database/mysql.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
DBName string
|
||||||
|
MaxConns int
|
||||||
|
MinConns int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) DSN() string {
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci&loc=Local",
|
||||||
|
c.User, c.Password, c.Host, c.Port, c.DBName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(cfg Config) (*sql.DB, error) {
|
||||||
|
db, err := sql.Open("mysql", cfg.DSN())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.MaxConns > 0 {
|
||||||
|
db.SetMaxOpenConns(cfg.MaxConns)
|
||||||
|
}
|
||||||
|
if cfg.MinConns > 0 {
|
||||||
|
db.SetMaxIdleConns(cfg.MinConns)
|
||||||
|
}
|
||||||
|
db.SetConnMaxLifetime(30 * time.Minute)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, fmt.Errorf("ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBFromDSN(dsn string) (*sql.DB, error) {
|
||||||
|
db, err := sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.SetMaxOpenConns(25)
|
||||||
|
db.SetMaxIdleConns(5)
|
||||||
|
db.SetConnMaxLifetime(30 * time.Minute)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, fmt.Errorf("ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
1
shared/pkg/database/postgres.go
Normal file
1
shared/pkg/database/postgres.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package database
|
||||||
27
shared/pkg/database/tx.go
Normal file
27
shared/pkg/database/tx.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("begin tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fn(tx); err != nil {
|
||||||
|
if rbErr := tx.Rollback(); rbErr != nil {
|
||||||
|
return fmt.Errorf("rollback failed: %v (original: %w)", rbErr, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("commit tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
7
shared/pkg/event/bus.go
Normal file
7
shared/pkg/event/bus.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
type EventBus interface {
|
||||||
|
Publish(topic string, payload interface{}) error
|
||||||
|
Subscribe(topic string, handler func(payload []byte)) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
1
shared/pkg/event/pg_notify.go
Normal file
1
shared/pkg/event/pg_notify.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package event
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user