From 6398e7d0e958e722ecaa7ecec7e2a0bdcfbbc9f4 Mon Sep 17 00:00:00 2001 From: Chenwei Jiang Date: Sat, 4 Jul 2026 02:34:20 +0000 Subject: [PATCH] feat: integrate muyu warehouse subsystem --- .gitlab-ci.yml | 12 +- auth-service/cmd/main.go | 15 ++- auth-service/go.mod | 10 +- auth-service/go.sum | 21 +++- auth-service/internal/config/config.go | 10 ++ auth-service/internal/handler/common.go | 53 ++++++++- auth-service/internal/storage/minio.go | 101 ++++++++++++++++ gateway/cmd/main.go | 20 +++- gateway/internal/config/config.go | 2 + k8s/02-configmaps/frontend-nginx.yaml | 24 +++- k8s/10-infra/minio.yaml | 97 ++++++++++++++++ k8s/30-iloom/auth-service.yaml | 14 +++ purchaser-service/internal/handler/product.go | 84 ++++++++++---- purchaser-service/internal/model/product.go | 108 ++++++++++++++---- .../internal/repository/plan_repo.go | 4 +- .../internal/repository/product_repo.go | 14 +-- .../internal/service/product_service.go | 99 ++++++++++++---- .../internal/repository/plan_repo.go | 4 +- .../internal/repository/plan_repo.go | 4 +- 19 files changed, 604 insertions(+), 92 deletions(-) create mode 100644 auth-service/internal/storage/minio.go create mode 100644 k8s/10-infra/minio.yaml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 939e9e7..5f2a03c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,19 +19,23 @@ variables: retry: 2 before_script: - until docker info >/dev/null 2>&1; do echo "Waiting for Docker daemon..."; sleep 2; done - - echo "$HARBOR_PASSWORD" | docker login $HARBOR_REGISTRY -u "$HARBOR_USER" --password-stdin + - echo "${HARBOR_PASS}" | docker login ${HARBOR_REGISTRY} -u "${HARBOR_USER}" --password-stdin script: - | if [ -n "$CI_COMMIT_TAG" ]; then TAG="$CI_COMMIT_TAG" - else + elif [ "$CI_COMMIT_BRANCH" = "main" ]; then TAG="main-${CI_COMMIT_SHORT_SHA}" + else + TAG="feat-${CI_COMMIT_SHORT_SHA}" fi - - docker build --network host -t $HARBOR_REGISTRY/$HARBOR_PROJECT/$IMAGE_NAME:$TAG -f $DOCKERFILE_PATH . - - docker push $HARBOR_REGISTRY/$HARBOR_PROJECT/$IMAGE_NAME:$TAG + - docker build --network host -t ${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${IMAGE_NAME}:${TAG} -f ${DOCKERFILE_PATH} . + - docker push ${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${IMAGE_NAME}:${TAG} rules: - if: $CI_COMMIT_BRANCH == "main" - if: $CI_COMMIT_TAG =~ /^v/ + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH =~ /^feat\// build-gateway: <<: *build-template diff --git a/auth-service/cmd/main.go b/auth-service/cmd/main.go index 3cf9a8c..815598c 100644 --- a/auth-service/cmd/main.go +++ b/auth-service/cmd/main.go @@ -9,6 +9,7 @@ import ( "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/auth-service/internal/storage" "github.com/muyuqingfeng/iloom/shared/pkg/auth" "github.com/muyuqingfeng/iloom/shared/pkg/database" sgrpc "github.com/muyuqingfeng/iloom/shared/pkg/grpc" @@ -32,6 +33,17 @@ func main() { jwtMgr := auth.NewJWTManager(cfg.JWTSecret, cfg.JWTRefreshSecret) + minioStorage, err := storage.NewMinIOStorage(storage.MinIOConfig{ + Endpoint: cfg.MinIOEndpoint, + AccessKey: cfg.MinIOAccessKey, + SecretKey: cfg.MinIOSecretKey, + Bucket: cfg.MinIOBucket, + UseSSL: cfg.MinIOUseSSL, + }) + if err != nil { + log.Printf("warning: minio init failed: %v (file upload disabled)", err) + } + profileRepo := repository.NewProfileRepo(db) companyRepo := repository.NewCompanyRepo(db) memberRepo := repository.NewMemberRepo(db) @@ -48,7 +60,7 @@ func main() { companyH := handler.NewCompanyHandler(companySvc) memberH := handler.NewMemberHandler(memberSvc) notifH := handler.NewNotificationHandler(notifSvc) - commonH := handler.NewCommonHandler(shareLinkRepo, auditLogRepo) + commonH := handler.NewCommonHandler(shareLinkRepo, auditLogRepo, minioStorage) r := gin.New() r.Use( @@ -83,6 +95,7 @@ func main() { commonGroup.GET("/share-links/:code", commonH.GetShareLink) commonGroup.POST("/feedback", commonH.Feedback) commonGroup.POST("/crash-reports", commonH.CrashReport) + commonGroup.POST("/upload", commonH.Upload) } addr := ":" + cfg.Port diff --git a/auth-service/go.mod b/auth-service/go.mod index 5f84f04..592045b 100644 --- a/auth-service/go.mod +++ b/auth-service/go.mod @@ -11,6 +11,7 @@ replace github.com/muyuqingfeng/iloom/shared => ../shared require ( github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 + github.com/minio/minio-go/v7 v7.0.82 ) require ( @@ -19,21 +20,26 @@ require ( 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/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ini/ini v1.67.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/goccy/go-json v0.10.3 // 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/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/md5-simd v1.1.2 // 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/rs/xid v1.6.0 // 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 diff --git a/auth-service/go.sum b/auth-service/go.sum index bc9a3ab..3cbb8b5 100644 --- a/auth-service/go.sum +++ b/auth-service/go.sum @@ -11,12 +11,16 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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= @@ -31,8 +35,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx 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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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= @@ -44,14 +48,21 @@ 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/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 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/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/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/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.82 h1:tWfICLhmp2aFPXL8Tli0XDTHj2VB/fNf0PC1f/i1gRo= +github.com/minio/minio-go/v7 v7.0.82/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= 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= @@ -61,6 +72,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 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/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 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= diff --git a/auth-service/internal/config/config.go b/auth-service/internal/config/config.go index ab67feb..a9d60d3 100644 --- a/auth-service/internal/config/config.go +++ b/auth-service/internal/config/config.go @@ -8,6 +8,11 @@ type Config struct { JWTSecret string JWTRefreshSecret string SystemRPCAddr string + MinIOEndpoint string + MinIOAccessKey string + MinIOSecretKey string + MinIOBucket string + MinIOUseSSL bool } func Load() *Config { @@ -17,6 +22,11 @@ func Load() *Config { JWTSecret: getEnv("JWT_SECRET", ""), JWTRefreshSecret: getEnv("JWT_REFRESH_SECRET", ""), SystemRPCAddr: getEnv("MUYU_SYSTEM_RPC_ADDR", "127.0.0.1:9001"), + MinIOEndpoint: getEnv("MINIO_ENDPOINT", "minio:9000"), + MinIOAccessKey: getEnv("MINIO_ACCESS_KEY", "minioadmin"), + MinIOSecretKey: getEnv("MINIO_SECRET_KEY", "minioadmin123"), + MinIOBucket: getEnv("MINIO_BUCKET", "iloom-uploads"), + MinIOUseSSL: getEnv("MINIO_USE_SSL", "false") == "true", } } diff --git a/auth-service/internal/handler/common.go b/auth-service/internal/handler/common.go index 343c4f4..9975e00 100644 --- a/auth-service/internal/handler/common.go +++ b/auth-service/internal/handler/common.go @@ -1,20 +1,24 @@ package handler import ( + "fmt" + "strings" "time" "github.com/gin-gonic/gin" "github.com/muyuqingfeng/iloom/auth-service/internal/repository" + "github.com/muyuqingfeng/iloom/auth-service/internal/storage" "github.com/muyuqingfeng/iloom/shared/pkg/response" ) type CommonHandler struct { shareLinkRepo *repository.ShareLinkRepo auditLogRepo *repository.AuditLogRepo + storage *storage.MinIOStorage } -func NewCommonHandler(slRepo *repository.ShareLinkRepo, alRepo *repository.AuditLogRepo) *CommonHandler { - return &CommonHandler{shareLinkRepo: slRepo, auditLogRepo: alRepo} +func NewCommonHandler(slRepo *repository.ShareLinkRepo, alRepo *repository.AuditLogRepo, st *storage.MinIOStorage) *CommonHandler { + return &CommonHandler{shareLinkRepo: slRepo, auditLogRepo: alRepo, storage: st} } type createShareLinkReq struct { @@ -132,3 +136,48 @@ func (h *CommonHandler) CrashReport(c *gin.Context) { response.OK(c, gin.H{"message": "crash report received"}) } + +const maxUploadSize = 10 << 20 // 10 MB + +func (h *CommonHandler) Upload(c *gin.Context) { + if h.storage == nil { + response.InternalError(c, "file storage not configured") + return + } + + if err := c.Request.ParseMultipartForm(maxUploadSize); err != nil { + response.BadRequest(c, fmt.Sprintf("file too large (max %dMB)", maxUploadSize>>20)) + return + } + + file, header, err := c.Request.FormFile("file") + if err != nil { + response.BadRequest(c, "missing file field") + return + } + defer file.Close() + + // Validate content type + contentType := header.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + response.BadRequest(c, "only image files are allowed") + return + } + + // Validate size + if header.Size > maxUploadSize { + response.BadRequest(c, fmt.Sprintf("file too large (max %dMB)", maxUploadSize>>20)) + return + } + + pathPrefix := c.DefaultPostForm("path_prefix", "products") + + objectPath, err := h.storage.Upload(c.Request.Context(), pathPrefix, header.Filename, file, header.Size, contentType) + if err != nil { + response.InternalError(c, "upload failed: "+err.Error()) + return + } + + url := "/uploads/" + objectPath + response.OK(c, gin.H{"url": url}) +} diff --git a/auth-service/internal/storage/minio.go b/auth-service/internal/storage/minio.go new file mode 100644 index 0000000..64007d7 --- /dev/null +++ b/auth-service/internal/storage/minio.go @@ -0,0 +1,101 @@ +package storage + +import ( + "context" + "fmt" + "io" + "log" + "path/filepath" + "strings" + + "github.com/google/uuid" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +type MinIOStorage struct { + client *minio.Client + bucket string +} + +type MinIOConfig struct { + Endpoint string + AccessKey string + SecretKey string + Bucket string + UseSSL bool +} + +func NewMinIOStorage(cfg MinIOConfig) (*MinIOStorage, error) { + client, err := minio.New(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""), + Secure: cfg.UseSSL, + }) + if err != nil { + return nil, fmt.Errorf("create minio client: %w", err) + } + + ctx := context.Background() + exists, err := client.BucketExists(ctx, cfg.Bucket) + if err != nil { + return nil, fmt.Errorf("check bucket: %w", err) + } + if !exists { + if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{}); err != nil { + return nil, fmt.Errorf("create bucket: %w", err) + } + log.Printf("created minio bucket: %s", cfg.Bucket) + + // Set bucket policy to allow public read + policy := fmt.Sprintf(`{ + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"AWS": ["*"]}, + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::%s/*"] + }] + }`, cfg.Bucket) + if err := client.SetBucketPolicy(ctx, cfg.Bucket, policy); err != nil { + log.Printf("warning: set bucket policy: %v", err) + } + } + + return &MinIOStorage{client: client, bucket: cfg.Bucket}, nil +} + +// Upload stores a file in MinIO and returns the object path (without bucket prefix). +// The returned path can be used to construct the public URL: /uploads/{path} +func (s *MinIOStorage) Upload(ctx context.Context, pathPrefix string, filename string, reader io.Reader, size int64, contentType string) (string, error) { + ext := filepath.Ext(filename) + if ext == "" { + ext = guessExt(contentType) + } + objectName := fmt.Sprintf("%s/%s%s", pathPrefix, uuid.New().String(), ext) + + _, err := s.client.PutObject(ctx, s.bucket, objectName, reader, size, minio.PutObjectOptions{ + ContentType: contentType, + }) + if err != nil { + return "", fmt.Errorf("put object: %w", err) + } + + return objectName, nil +} + +func guessExt(contentType string) string { + switch { + case strings.HasPrefix(contentType, "image/jpeg"): + return ".jpg" + case strings.HasPrefix(contentType, "image/png"): + return ".png" + case strings.HasPrefix(contentType, "image/gif"): + return ".gif" + case strings.HasPrefix(contentType, "image/webp"): + return ".webp" + case strings.HasPrefix(contentType, "image/svg"): + return ".svg" + default: + return ".bin" + } +} diff --git a/gateway/cmd/main.go b/gateway/cmd/main.go index 9a0fc57..08b3c7d 100644 --- a/gateway/cmd/main.go +++ b/gateway/cmd/main.go @@ -29,8 +29,17 @@ func main() { v1 := r.Group("/api/v1") { - // /api/v1/auth/* -> AUTH_SERVICE_URL (no auth required) - v1.Any("/auth/*path", proxy.NewReverseProxy(cfg.AuthServiceURL)) + // /api/v1/auth/* -> AUTH_SERVICE_URL + // Public auth endpoints (no JWT required) + authPublic := v1.Group("/auth") + authPublic.POST("/register", proxy.NewReverseProxy(cfg.AuthServiceURL)) + authPublic.POST("/login", proxy.NewReverseProxy(cfg.AuthServiceURL)) + authPublic.POST("/logout", proxy.NewReverseProxy(cfg.AuthServiceURL)) + authPublic.POST("/refresh", proxy.NewReverseProxy(cfg.AuthServiceURL)) + + // Protected auth endpoints (JWT required to set X-User-ID) + authProtected := v1.Group("/auth", middleware.AuthRequired(cfg.JWTSecret)) + authProtected.GET("/me", proxy.NewReverseProxy(cfg.AuthServiceURL)) // /api/v1/common/* -> AUTH_SERVICE_URL (JWT required) common := v1.Group("/common", middleware.AuthRequired(cfg.JWTSecret)) @@ -56,6 +65,13 @@ func main() { middleware.RoleRequired("washing"), ) washing.Any("/*path", proxy.NewReverseProxy(cfg.WashingServiceURL)) + + // /api/v1/muyu/* -> MUYU_GATEWAY_URL (muyu warehouse subsystem) + // muyu-gateway handles its own authentication internally + muyu := v1.Group("/muyu", + middleware.AuthRequired(cfg.JWTSecret), + ) + muyu.Any("/*path", proxy.NewReverseProxy(cfg.MuyuGatewayURL)) } // WebSocket proxy — route by role (JWT + role check applied) diff --git a/gateway/internal/config/config.go b/gateway/internal/config/config.go index 67035e3..afc4e40 100644 --- a/gateway/internal/config/config.go +++ b/gateway/internal/config/config.go @@ -12,6 +12,7 @@ type Config struct { PurchaserServiceURL string TextileServiceURL string WashingServiceURL string + MuyuGatewayURL string AllowOrigins []string } @@ -23,6 +24,7 @@ func Load() *Config { PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), TextileServiceURL: getEnv("TEXTILE_SERVICE_URL", "http://localhost:8083"), WashingServiceURL: getEnv("WASHING_SERVICE_URL", "http://localhost:8084"), + MuyuGatewayURL: getEnv("MUYU_GATEWAY_URL", "http://localhost:8888"), AllowOrigins: strings.Split(getEnv("ALLOW_ORIGINS", "*"), ","), } } diff --git a/k8s/02-configmaps/frontend-nginx.yaml b/k8s/02-configmaps/frontend-nginx.yaml index c1bcb70..fc1457f 100644 --- a/k8s/02-configmaps/frontend-nginx.yaml +++ b/k8s/02-configmaps/frontend-nginx.yaml @@ -21,6 +21,17 @@ data: 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; + client_max_body_size 10m; + } + + location ^~ /uploads/ { + resolver 10.43.0.10 valid=30s; + set $minio_host "minio.iloom.svc.cluster.local"; + rewrite ^/uploads/(.*)$ /iloom-uploads/$1 break; + proxy_pass http://$minio_host:9000; + proxy_set_header Host $minio_host:9000; + expires 7d; + add_header Cache-Control "public"; } location /ws/ { @@ -33,12 +44,23 @@ data: proxy_read_timeout 86400; } + # SPA fallback — index.html must never be cached location / { try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; } - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + # Hashed static assets (e.g. main.decafe71.js) — safe to cache long-term + location ~* \.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]\.(js|css)$ { expires 30d; add_header Cache-Control "public, immutable"; } + + # Other static assets (images, fonts) — moderate cache + location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 7d; + add_header Cache-Control "public"; + } } diff --git a/k8s/10-infra/minio.yaml b/k8s/10-infra/minio.yaml new file mode 100644 index 0000000..0b15df1 --- /dev/null +++ b/k8s/10-infra/minio.yaml @@ -0,0 +1,97 @@ +apiVersion: v1 +kind: Secret +metadata: + name: minio-secret + namespace: iloom +type: Opaque +stringData: + MINIO_ROOT_USER: "minioadmin" + MINIO_ROOT_PASSWORD: "minioadmin123" +--- +apiVersion: v1 +kind: Service +metadata: + name: minio + namespace: iloom +spec: + clusterIP: None + ports: + - name: api + port: 9000 + targetPort: 9000 + - name: console + port: 9001 + targetPort: 9001 + selector: + app: minio +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: minio + namespace: iloom +spec: + serviceName: minio + replicas: 1 + selector: + matchLabels: + app: minio + template: + metadata: + labels: + app: minio + spec: + imagePullSecrets: + - name: harbor-pull-secret + containers: + - name: minio + image: harbor-in-k3s.cheverjohn.me/iloom/minio:latest + args: + - server + - /data + - --console-address + - ":9001" + ports: + - containerPort: 9000 + - containerPort: 9001 + env: + - name: MINIO_ROOT_USER + valueFrom: + secretKeyRef: + name: minio-secret + key: MINIO_ROOT_USER + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: minio-secret + key: MINIO_ROOT_PASSWORD + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + readinessProbe: + httpGet: + path: /minio/health/ready + port: 9000 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /minio/health/live + port: 9000 + initialDelaySeconds: 20 + periodSeconds: 30 + volumeMounts: + - name: data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi diff --git a/k8s/30-iloom/auth-service.yaml b/k8s/30-iloom/auth-service.yaml index 5f37c90..82d55d2 100644 --- a/k8s/30-iloom/auth-service.yaml +++ b/k8s/30-iloom/auth-service.yaml @@ -60,6 +60,20 @@ spec: key: JWT_REFRESH_SECRET - name: MUYU_SYSTEM_RPC_ADDR value: "muyu-system-rpc:9001" + - name: MINIO_ENDPOINT + value: "minio:9000" + - name: MINIO_ACCESS_KEY + valueFrom: + secretKeyRef: + name: minio-secret + key: MINIO_ROOT_USER + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: minio-secret + key: MINIO_ROOT_PASSWORD + - name: MINIO_BUCKET + value: "iloom-uploads" resources: requests: cpu: 50m diff --git a/purchaser-service/internal/handler/product.go b/purchaser-service/internal/handler/product.go index 1c98afe..aa06d01 100644 --- a/purchaser-service/internal/handler/product.go +++ b/purchaser-service/internal/handler/product.go @@ -20,27 +20,43 @@ func NewProductHandler(svc *service.ProductService, wsHub *websocket.Hub) *Produ } 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"` + ProductName string `json:"product_name"` + FabricCode string `json:"fabric_code" binding:"required"` + Color string `json:"color"` + ColorCode string `json:"color_code"` + Weight float64 `json:"weight"` + WarpWeight *float64 `json:"warp_weight"` + WeftWeight *float64 `json:"weft_weight"` + TotalWeight *float64 `json:"total_weight"` + YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` + ProductionPrice *float64 `json:"production_price"` + ImageURL *string `json:"image_url"` + ProductType string `json:"product_type"` + YarnTypes []string `json:"yarn_types"` + YarnRatios []YarnRatioReq `json:"yarn_ratios"` +} + +type YarnRatioReq struct { + YarnName string `json:"yarn_name"` + YarnType string `json:"yarn_type"` + Ratio float64 `json:"ratio"` } 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"` + ProductName string `json:"product_name"` + FabricCode string `json:"fabric_code"` + Color string `json:"color"` + ColorCode string `json:"color_code"` + Weight float64 `json:"weight"` + WarpWeight *float64 `json:"warp_weight"` + WeftWeight *float64 `json:"weft_weight"` + TotalWeight *float64 `json:"total_weight"` + YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` + ProductionPrice *float64 `json:"production_price"` + ImageURL *string `json:"image_url"` + ProductType string `json:"product_type"` + YarnTypes []string `json:"yarn_types"` + YarnRatios []YarnRatioReq `json:"yarn_ratios"` } type InboundReq struct { @@ -97,16 +113,30 @@ func (h *ProductHandler) Create(c *gin.Context) { return } + yarnRatios := make([]service.YarnRatio, len(req.YarnRatios)) + for i, yr := range req.YarnRatios { + yarnRatios[i] = service.YarnRatio{ + YarnName: yr.YarnName, + YarnType: yr.YarnType, + Ratio: yr.Ratio, + } + } + svcReq := service.CreateProductRequest{ ProductName: req.ProductName, FabricCode: req.FabricCode, Color: req.Color, ColorCode: req.ColorCode, Weight: req.Weight, + WarpWeight: req.WarpWeight, + WeftWeight: req.WeftWeight, + TotalWeight: req.TotalWeight, YarnUsagePerMeter: req.YarnUsagePerMeter, ProductionPrice: req.ProductionPrice, + ImageURL: req.ImageURL, + ProductType: req.ProductType, YarnTypes: req.YarnTypes, - YarnRatios: req.YarnRatios, + YarnRatios: yarnRatios, } product, err := h.svc.Create(c.Request.Context(), companyID, svcReq) @@ -143,16 +173,30 @@ func (h *ProductHandler) Update(c *gin.Context) { return } + yarnRatios := make([]service.YarnRatio, len(req.YarnRatios)) + for i, yr := range req.YarnRatios { + yarnRatios[i] = service.YarnRatio{ + YarnName: yr.YarnName, + YarnType: yr.YarnType, + Ratio: yr.Ratio, + } + } + svcReq := service.UpdateProductRequest{ ProductName: req.ProductName, FabricCode: req.FabricCode, Color: req.Color, ColorCode: req.ColorCode, Weight: req.Weight, + WarpWeight: req.WarpWeight, + WeftWeight: req.WeftWeight, + TotalWeight: req.TotalWeight, YarnUsagePerMeter: req.YarnUsagePerMeter, ProductionPrice: req.ProductionPrice, + ImageURL: req.ImageURL, + ProductType: req.ProductType, YarnTypes: req.YarnTypes, - YarnRatios: req.YarnRatios, + YarnRatios: yarnRatios, } if err := h.svc.Update(c.Request.Context(), id, companyID, svcReq); err != nil { diff --git a/purchaser-service/internal/model/product.go b/purchaser-service/internal/model/product.go index 224e3c4..59543ed 100644 --- a/purchaser-service/internal/model/product.go +++ b/purchaser-service/internal/model/product.go @@ -1,28 +1,94 @@ package model -import "time" +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" +) + +type ProductYarnRatio struct { + YarnName string `json:"yarn_name"` + YarnType string `json:"yarn_type"` + Ratio float64 `json:"ratio"` +} + +type JSONStringSlice []string + +func (s *JSONStringSlice) Scan(src interface{}) error { + if src == nil { + *s = nil + return nil + } + var b []byte + switch v := src.(type) { + case []byte: + b = v + case string: + b = []byte(v) + default: + return fmt.Errorf("unsupported type: %T", src) + } + return json.Unmarshal(b, s) +} + +func (s JSONStringSlice) Value() (driver.Value, error) { + if s == nil { + return nil, nil + } + b, err := json.Marshal(s) + return string(b), err +} + +type JSONYarnRatios []ProductYarnRatio + +func (r *JSONYarnRatios) Scan(src interface{}) error { + if src == nil { + *r = nil + return nil + } + var b []byte + switch v := src.(type) { + case []byte: + b = v + case string: + b = []byte(v) + default: + return fmt.Errorf("unsupported type: %T", src) + } + return json.Unmarshal(b, r) +} + +func (r JSONYarnRatios) Value() (driver.Value, error) { + if r == nil { + return nil, nil + } + b, err := json.Marshal(r) + return string(b), err +} 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"` + 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"` + ProductType string `json:"product_type" db:"product_type"` + YarnTypes JSONStringSlice `json:"yarn_types,omitempty" db:"yarn_types"` + YarnRatios JSONYarnRatios `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 { diff --git a/purchaser-service/internal/repository/plan_repo.go b/purchaser-service/internal/repository/plan_repo.go index 209437e..ddb19ef 100644 --- a/purchaser-service/internal/repository/plan_repo.go +++ b/purchaser-service/internal/repository/plan_repo.go @@ -282,8 +282,8 @@ func (r *PlanRepo) GetInventory(ctx context.Context, planID string) ([]model.Inv 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) + COALESCE(SUM(CASE WHEN status = 'producing' THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0) FROM ilm_production_plan WHERE purchaser_id = ?` err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &producing, &completed) if err != nil { diff --git a/purchaser-service/internal/repository/product_repo.go b/purchaser-service/internal/repository/product_repo.go index dcf5896..f0db6da 100644 --- a/purchaser-service/internal/repository/product_repo.go +++ b/purchaser-service/internal/repository/product_repo.go @@ -22,7 +22,7 @@ func (r *ProductRepo) List(ctx context.Context, companyID string) ([]model.Produ 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 + image_url, product_type, 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) @@ -39,7 +39,7 @@ func (r *ProductRepo) List(ctx context.Context, companyID string) ([]model.Produ &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.ImageURL, &p.ProductType, &p.YarnTypes, &p.YarnRatios, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, fmt.Errorf("scan product: %w", err) @@ -54,14 +54,14 @@ func (r *ProductRepo) Create(ctx context.Context, p *model.Product) error { (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 (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)` + image_url, product_type, 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.ImageURL, p.ProductType, p.YarnTypes, p.YarnRatios, p.CreatedAt, p.UpdatedAt, ) if err != nil { @@ -75,13 +75,13 @@ func (r *ProductRepo) Update(ctx context.Context, p *model.Product) error { 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 = ? + image_url = ?, product_type = ?, 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.ImageURL, p.ProductType, p.YarnTypes, p.YarnRatios, time.Now(), p.ID, ) if err != nil { diff --git a/purchaser-service/internal/service/product_service.go b/purchaser-service/internal/service/product_service.go index 144188f..20cb69d 100644 --- a/purchaser-service/internal/service/product_service.go +++ b/purchaser-service/internal/service/product_service.go @@ -20,28 +20,44 @@ func NewProductService(repo *repository.ProductRepo, db *sql.DB) *ProductService return &ProductService{repo: repo, db: db} } +type YarnRatio struct { + YarnName string `json:"yarn_name"` + YarnType string `json:"yarn_type"` + Ratio float64 `json:"ratio"` +} + 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"` + ProductName string `json:"product_name"` + FabricCode string `json:"fabric_code"` + Color string `json:"color"` + ColorCode string `json:"color_code"` + Weight float64 `json:"weight"` + WarpWeight *float64 `json:"warp_weight"` + WeftWeight *float64 `json:"weft_weight"` + TotalWeight *float64 `json:"total_weight"` + YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` + ProductionPrice *float64 `json:"production_price"` + ImageURL *string `json:"image_url"` + ProductType string `json:"product_type"` + YarnTypes []string `json:"yarn_types"` + YarnRatios []YarnRatio `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"` + ProductName string `json:"product_name"` + FabricCode string `json:"fabric_code"` + Color string `json:"color"` + ColorCode string `json:"color_code"` + Weight float64 `json:"weight"` + WarpWeight *float64 `json:"warp_weight"` + WeftWeight *float64 `json:"weft_weight"` + TotalWeight *float64 `json:"total_weight"` + YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` + ProductionPrice *float64 `json:"production_price"` + ImageURL *string `json:"image_url"` + ProductType string `json:"product_type"` + YarnTypes []string `json:"yarn_types"` + YarnRatios []YarnRatio `json:"yarn_ratios"` } type InboundRequest struct { @@ -70,6 +86,21 @@ func (s *ProductService) List(ctx context.Context, companyID string) ([]model.Pr func (s *ProductService) Create(ctx context.Context, companyID string, req CreateProductRequest) (*model.Product, error) { now := time.Now() + + yarnRatios := make(model.JSONYarnRatios, len(req.YarnRatios)) + for i, yr := range req.YarnRatios { + yarnRatios[i] = model.ProductYarnRatio{ + YarnName: yr.YarnName, + YarnType: yr.YarnType, + Ratio: yr.Ratio, + } + } + + productType := req.ProductType + if productType == "" { + productType = "raw_fabric" + } + p := &model.Product{ ID: uuid.New().String(), CompanyID: companyID, @@ -78,13 +109,18 @@ func (s *ProductService) Create(ctx context.Context, companyID string, req Creat Color: req.Color, ColorCode: req.ColorCode, Weight: req.Weight, + WarpWeight: req.WarpWeight, + WeftWeight: req.WeftWeight, + TotalWeight: req.TotalWeight, YarnUsagePerMeter: req.YarnUsagePerMeter, ProductionPrice: req.ProductionPrice, + ImageURL: req.ImageURL, + ProductType: productType, TotalStock: 0, RawFabricMeters: 0, RawFabricRolls: 0, - YarnTypes: req.YarnTypes, - YarnRatios: req.YarnRatios, + YarnTypes: model.JSONStringSlice(req.YarnTypes), + YarnRatios: yarnRatios, CreatedAt: now, UpdatedAt: now, } @@ -96,6 +132,20 @@ func (s *ProductService) Create(ctx context.Context, companyID string, req Creat } func (s *ProductService) Update(ctx context.Context, id, companyID string, req UpdateProductRequest) error { + yarnRatios := make(model.JSONYarnRatios, len(req.YarnRatios)) + for i, yr := range req.YarnRatios { + yarnRatios[i] = model.ProductYarnRatio{ + YarnName: yr.YarnName, + YarnType: yr.YarnType, + Ratio: yr.Ratio, + } + } + + productType := req.ProductType + if productType == "" { + productType = "raw_fabric" + } + p := &model.Product{ ID: id, ProductName: req.ProductName, @@ -103,10 +153,15 @@ func (s *ProductService) Update(ctx context.Context, id, companyID string, req U Color: req.Color, ColorCode: req.ColorCode, Weight: req.Weight, + WarpWeight: req.WarpWeight, + WeftWeight: req.WeftWeight, + TotalWeight: req.TotalWeight, YarnUsagePerMeter: req.YarnUsagePerMeter, ProductionPrice: req.ProductionPrice, - YarnTypes: req.YarnTypes, - YarnRatios: req.YarnRatios, + ImageURL: req.ImageURL, + ProductType: productType, + YarnTypes: model.JSONStringSlice(req.YarnTypes), + YarnRatios: yarnRatios, } return s.repo.Update(ctx, p) } diff --git a/textile-service/internal/repository/plan_repo.go b/textile-service/internal/repository/plan_repo.go index 9851740..78209ce 100644 --- a/textile-service/internal/repository/plan_repo.go +++ b/textile-service/internal/repository/plan_repo.go @@ -91,8 +91,8 @@ func (r *PlanRepo) CountByFactory(ctx context.Context, factoryCompanyID string) query := ` SELECT COUNT(*), - SUM(CASE WHEN pp.status IN ('pending', 'in_progress') THEN 1 ELSE 0 END), - SUM(CASE WHEN pp.status = 'completed' THEN 1 ELSE 0 END) + COALESCE(SUM(CASE WHEN pp.status IN ('pending', 'in_progress') THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN pp.status = 'completed' THEN 1 ELSE 0 END), 0) FROM ilm_production_plan pp JOIN ilm_plan_factory pf ON pp.id = pf.plan_id WHERE pf.factory_id = ? AND pf.factory_type = 'textile'` diff --git a/washing-service/internal/repository/plan_repo.go b/washing-service/internal/repository/plan_repo.go index e5a45ff..639d2ee 100644 --- a/washing-service/internal/repository/plan_repo.go +++ b/washing-service/internal/repository/plan_repo.go @@ -140,8 +140,8 @@ func (r *PlanRepo) Completed(ctx context.Context, companyID string) ([]model.Was func (r *PlanRepo) CountByStatus(ctx context.Context, companyID string) (total, pending, completed int, err error) { query := `SELECT COUNT(*), - SUM(CASE WHEN status IN ('pending', 'washing') THEN 1 ELSE 0 END), - SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) + COALESCE(SUM(CASE WHEN status IN ('pending', 'washing') THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0) FROM ilm_washing_plan WHERE washing_factory_id = ?` err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &pending, &completed) if err != nil {