Compare commits

..

10 Commits

Author SHA1 Message Date
kae
028bb6cc0e edit dockerfiles for compatible deploy 2026-07-22 12:35:21 +09:00
Chenwei Jiang
eafcb9ea88 Merge branch 'feat/muyu-subsystem-integration' into 'main'
feat: integrate muyu warehouse subsystem

See merge request kihaneseifu/iloom!1
2026-07-04 02:34:20 +00:00
Chenwei Jiang
6398e7d0e9 feat: integrate muyu warehouse subsystem 2026-07-04 02:34:20 +00:00
Chever John
24c449ecae
security: remove hardcoded credentials and add WebSocket auth/CORS
- Clear default passwords in all service configs, require env vars
- Add JWT auth + role middleware to WebSocket endpoints
- Add origin whitelist to WebSocket upgrader (CORS protection)
- Fix goroutine leak in WS proxy (double errCh read)
- Update docker-compose to require secrets via ${VAR:?...} syntax
- Mark deprecated k8s configmap passwords as REPLACE_AT_DEPLOY_TIME

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 23:16:17 +08:00
Chever John
08b6c7cbeb
fix(ci): use host networking and custom DNS for DinD builds
DinD containers can't reach external services due to DNS/networking
issues. Fix by:
- Passing --dns 10.43.0.10 (CoreDNS) and --dns 223.5.5.5 (Alibaba)
  to DinD daemon
- Using --network host for docker build to use pod networking

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:28:16 +08:00
Chever John
9e25c799d5
fix: use Aliyun Alpine mirror in Dockerfiles
Alpine CDN (dl-cdn.alpinelinux.org) unreachable from DinD containers.
Switch to mirrors.aliyun.com for apk package downloads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:24:59 +08:00
Chever John
75f8dca36b
fix(ci): stagger builds in stages and add retry for Harbor load
Split 5 parallel build jobs into 3 stages (2+2+1) to avoid
overwhelming Harbor with concurrent image pulls. Add retry: 2
for transient failures.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:20:08 +08:00
Chever John
475589b23f
fix: use Harbor base images and Chinese Go proxy for CI builds
Docker Hub and proxy.golang.org are unreachable from k3s cluster
(GFW). Switch all Dockerfiles to pull base images from Harbor
(harbor-in-k3s.cheverjohn.me/library/) and use goproxy.cn for
Go module downloads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:17:36 +08:00
Chever John
6568d33c02
fix(ci): add Docker daemon wait and TLS env vars for DinD
Set DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH for k8s runner
DinD sidecar communication. Add 'until docker info' wait loop to
ensure daemon is ready before build starts.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:10:56 +08:00
Chever John
7226dfd394
feat: add GitLab CI pipeline for multi-service Docker build
Build all 5 iloom services (gateway, auth, purchaser, textile, washing)
as Docker images and push to Harbor registry.
- main branch: tagged as main-<sha>
- git tag v*: tagged with version number

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 10:01:47 +08:00
34 changed files with 946 additions and 147 deletions

View File

@ -1,18 +1,20 @@
# iloom 微服务环境变量配置 # iloom 微服务环境变量配置
# Copy this file to .env and fill in real values. NEVER commit .env to git.
# MySQL (共享 muyu_wms 数据库) # MySQL (共享 muyu_wms 数据库)
DB_DSN=root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 MYSQL_ROOT_PASSWORD=your_mysql_password_here
DB_DSN=root:your_password@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4
# JWT (复用 muyu 认证) # JWT (复用 muyu 认证)
JWT_SECRET=muyu-wms-jwt-secret-key-2026 JWT_SECRET=your_jwt_secret_here
JWT_REFRESH_SECRET=muyu-wms-jwt-refresh-2026 JWT_REFRESH_SECRET=your_jwt_refresh_secret_here
# muyu gRPC 地址 # muyu gRPC 地址
MUYU_SYSTEM_RPC_ADDR=system-rpc:9001 MUYU_SYSTEM_RPC_ADDR=system-rpc:9001
MUYU_INVENTORY_RPC_ADDR=inventory-rpc:9002 MUYU_INVENTORY_RPC_ADDR=inventory-rpc:9002
# 服务间调用密钥 # 服务间调用密钥
INTERNAL_SERVICE_KEY=internal-service-shared-secret INTERNAL_SERVICE_KEY=your_internal_service_key_here
# 服务端口 # 服务端口
GATEWAY_PORT=8080 GATEWAY_PORT=8080

73
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,73 @@
stages:
- build-1
- build-2
- build-3
variables:
HARBOR_REGISTRY: harbor-in-k3s.cheverjohn.me
HARBOR_PROJECT: iloom
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_HOST: tcp://localhost:2376
DOCKER_TLS_VERIFY: "1"
DOCKER_CERT_PATH: /certs/client
.build-template: &build-template
image: docker:27
services:
- name: docker:27-dind
command: ["--dns", "10.43.0.10", "--dns", "223.5.5.5"]
retry: 2
before_script:
- until docker info >/dev/null 2>&1; do echo "Waiting for Docker daemon..."; sleep 2; done
- echo "${HARBOR_PASS}" | docker login ${HARBOR_REGISTRY} -u "${HARBOR_USER}" --password-stdin
script:
- |
if [ -n "$CI_COMMIT_TAG" ]; then
TAG="$CI_COMMIT_TAG"
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}
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
stage: build-1
variables:
IMAGE_NAME: iloom-gateway
DOCKERFILE_PATH: gateway/Dockerfile
build-auth-service:
<<: *build-template
stage: build-1
variables:
IMAGE_NAME: iloom-auth-service
DOCKERFILE_PATH: auth-service/Dockerfile
build-purchaser-service:
<<: *build-template
stage: build-2
variables:
IMAGE_NAME: iloom-purchaser-service
DOCKERFILE_PATH: purchaser-service/Dockerfile
build-textile-service:
<<: *build-template
stage: build-2
variables:
IMAGE_NAME: iloom-textile-service
DOCKERFILE_PATH: textile-service/Dockerfile
build-washing-service:
<<: *build-template
stage: build-3
variables:
IMAGE_NAME: iloom-washing-service
DOCKERFILE_PATH: washing-service/Dockerfile

View File

@ -1,11 +1,16 @@
FROM golang:1.24-alpine AS builder ARG BASE_REGISTRY=harbor-in-k3s.cheverjohn.me/library/
FROM ${BASE_REGISTRY}golang:1.24-alpine AS builder
ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
WORKDIR /app WORKDIR /app
COPY shared/ ./shared/ COPY shared/ ./shared/
COPY auth-service/ ./auth-service/ COPY auth-service/ ./auth-service/
WORKDIR /app/auth-service WORKDIR /app/auth-service
RUN go build -o /auth-service ./cmd/ RUN go build -o /auth-service ./cmd/
FROM alpine:3.20 FROM ${BASE_REGISTRY}alpine:3.20
ARG APK_MIRROR=mirrors.aliyun.com
RUN if [ -n "$APK_MIRROR" ]; then sed -i "s|dl-cdn.alpinelinux.org|$APK_MIRROR|g" /etc/apk/repositories; fi
RUN apk add --no-cache wget RUN apk add --no-cache wget
COPY --from=builder /auth-service /auth-service COPY --from=builder /auth-service /auth-service
EXPOSE 8081 EXPOSE 8081

View File

@ -9,6 +9,7 @@ import (
"github.com/muyuqingfeng/iloom/auth-service/internal/handler" "github.com/muyuqingfeng/iloom/auth-service/internal/handler"
"github.com/muyuqingfeng/iloom/auth-service/internal/repository" "github.com/muyuqingfeng/iloom/auth-service/internal/repository"
"github.com/muyuqingfeng/iloom/auth-service/internal/service" "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/auth"
"github.com/muyuqingfeng/iloom/shared/pkg/database" "github.com/muyuqingfeng/iloom/shared/pkg/database"
sgrpc "github.com/muyuqingfeng/iloom/shared/pkg/grpc" sgrpc "github.com/muyuqingfeng/iloom/shared/pkg/grpc"
@ -32,6 +33,17 @@ func main() {
jwtMgr := auth.NewJWTManager(cfg.JWTSecret, cfg.JWTRefreshSecret) 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) profileRepo := repository.NewProfileRepo(db)
companyRepo := repository.NewCompanyRepo(db) companyRepo := repository.NewCompanyRepo(db)
memberRepo := repository.NewMemberRepo(db) memberRepo := repository.NewMemberRepo(db)
@ -48,7 +60,7 @@ func main() {
companyH := handler.NewCompanyHandler(companySvc) companyH := handler.NewCompanyHandler(companySvc)
memberH := handler.NewMemberHandler(memberSvc) memberH := handler.NewMemberHandler(memberSvc)
notifH := handler.NewNotificationHandler(notifSvc) notifH := handler.NewNotificationHandler(notifSvc)
commonH := handler.NewCommonHandler(shareLinkRepo, auditLogRepo) commonH := handler.NewCommonHandler(shareLinkRepo, auditLogRepo, minioStorage)
r := gin.New() r := gin.New()
r.Use( r.Use(
@ -83,6 +95,7 @@ func main() {
commonGroup.GET("/share-links/:code", commonH.GetShareLink) commonGroup.GET("/share-links/:code", commonH.GetShareLink)
commonGroup.POST("/feedback", commonH.Feedback) commonGroup.POST("/feedback", commonH.Feedback)
commonGroup.POST("/crash-reports", commonH.CrashReport) commonGroup.POST("/crash-reports", commonH.CrashReport)
commonGroup.POST("/upload", commonH.Upload)
} }
addr := ":" + cfg.Port addr := ":" + cfg.Port

View File

@ -11,6 +11,7 @@ replace github.com/muyuqingfeng/iloom/shared => ../shared
require ( require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/minio/minio-go/v7 v7.0.82
) )
require ( require (
@ -19,21 +20,26 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // 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/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // 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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // 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/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/json-iterator/go v1.1.12 // 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/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect

View File

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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 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-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 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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 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/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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.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.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@ -8,15 +8,25 @@ type Config struct {
JWTSecret string JWTSecret string
JWTRefreshSecret string JWTRefreshSecret string
SystemRPCAddr string SystemRPCAddr string
MinIOEndpoint string
MinIOAccessKey string
MinIOSecretKey string
MinIOBucket string
MinIOUseSSL bool
} }
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8081"), Port: getEnv("PORT", "8081"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
JWTSecret: getEnv("JWT_SECRET", "muyu-wms-jwt-secret-key-2026"), JWTSecret: getEnv("JWT_SECRET", ""),
JWTRefreshSecret: getEnv("JWT_REFRESH_SECRET", "muyu-wms-jwt-refresh-2026"), JWTRefreshSecret: getEnv("JWT_REFRESH_SECRET", ""),
SystemRPCAddr: getEnv("MUYU_SYSTEM_RPC_ADDR", "127.0.0.1:9001"), 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",
} }
} }

View File

@ -1,20 +1,24 @@
package handler package handler
import ( import (
"fmt"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/muyuqingfeng/iloom/auth-service/internal/repository" "github.com/muyuqingfeng/iloom/auth-service/internal/repository"
"github.com/muyuqingfeng/iloom/auth-service/internal/storage"
"github.com/muyuqingfeng/iloom/shared/pkg/response" "github.com/muyuqingfeng/iloom/shared/pkg/response"
) )
type CommonHandler struct { type CommonHandler struct {
shareLinkRepo *repository.ShareLinkRepo shareLinkRepo *repository.ShareLinkRepo
auditLogRepo *repository.AuditLogRepo auditLogRepo *repository.AuditLogRepo
storage *storage.MinIOStorage
} }
func NewCommonHandler(slRepo *repository.ShareLinkRepo, alRepo *repository.AuditLogRepo) *CommonHandler { func NewCommonHandler(slRepo *repository.ShareLinkRepo, alRepo *repository.AuditLogRepo, st *storage.MinIOStorage) *CommonHandler {
return &CommonHandler{shareLinkRepo: slRepo, auditLogRepo: alRepo} return &CommonHandler{shareLinkRepo: slRepo, auditLogRepo: alRepo, storage: st}
} }
type createShareLinkReq struct { type createShareLinkReq struct {
@ -132,3 +136,48 @@ func (h *CommonHandler) CrashReport(c *gin.Context) {
response.OK(c, gin.H{"message": "crash report received"}) 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})
}

View File

@ -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"
}
}

View File

@ -6,11 +6,15 @@ services:
build: build:
context: . context: .
dockerfile: gateway/Dockerfile dockerfile: gateway/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${GATEWAY_PORT:-8080}:8080" - "${GATEWAY_PORT:-8080}:8080"
environment: environment:
- PORT=8080 - PORT=8080
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- AUTH_SERVICE_URL=http://auth-service:8081 - AUTH_SERVICE_URL=http://auth-service:8081
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
- TEXTILE_SERVICE_URL=http://textile-service:8083 - TEXTILE_SERVICE_URL=http://textile-service:8083
@ -33,13 +37,17 @@ services:
build: build:
context: . context: .
dockerfile: auth-service/Dockerfile dockerfile: auth-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${AUTH_SERVICE_PORT:-8081}:8081" - "${AUTH_SERVICE_PORT:-8081}:8081"
environment: environment:
- PORT=8081 - PORT=8081
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-muyu-wms-jwt-refresh-2026} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:?Set JWT_REFRESH_SECRET in .env}
- MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001 - MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8081/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8081/health"]
@ -54,12 +62,16 @@ services:
build: build:
context: . context: .
dockerfile: purchaser-service/Dockerfile dockerfile: purchaser-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${PURCHASER_SERVICE_PORT:-8082}:8082" - "${PURCHASER_SERVICE_PORT:-8082}:8082"
environment: environment:
- PORT=8082 - PORT=8082
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002 - MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8082/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8082/health"]
@ -74,12 +86,16 @@ services:
build: build:
context: . context: .
dockerfile: textile-service/Dockerfile dockerfile: textile-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${TEXTILE_SERVICE_PORT:-8083}:8083" - "${TEXTILE_SERVICE_PORT:-8083}:8083"
environment: environment:
- PORT=8083 - PORT=8083
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8083/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8083/health"]
@ -94,12 +110,16 @@ services:
build: build:
context: . context: .
dockerfile: washing-service/Dockerfile dockerfile: washing-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${WASHING_SERVICE_PORT:-8084}:8084" - "${WASHING_SERVICE_PORT:-8084}:8084"
environment: environment:
- PORT=8084 - PORT=8084
- DB_DSN=root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4 - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8084/health"] test: ["CMD", "wget", "-qO-", "http://localhost:8084/health"]
@ -112,8 +132,10 @@ services:
frontend: frontend:
build: build:
context: . context: ./iloom-flatten
dockerfile: iloom-flatten/Dockerfile dockerfile: Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
ports: ports:
- "${FRONTEND_PORT:-3015}:3015" - "${FRONTEND_PORT:-3015}:3015"
depends_on: depends_on:

View File

@ -6,7 +6,7 @@
# 集成模式(与 muyu-apiserver 联合): # 集成模式(与 muyu-apiserver 联合):
# 1. cd ../muyu-apiserver/deploy && docker compose up # 1. cd ../muyu-apiserver/deploy && docker compose up
# 2. 设置环境变量指向 muyu 的服务: # 2. 设置环境变量指向 muyu 的服务:
# export DB_DSN="root:muyu2026@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4" # export DB_DSN="root:yourpassword@tcp(muyu-mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4"
# export MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001 # export MUYU_SYSTEM_RPC_ADDR=muyu-system-rpc:9001
# export MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002 # export MUYU_INVENTORY_RPC_ADDR=muyu-inventory-rpc:9002
# 3. docker compose --profile integrated up # 3. docker compose --profile integrated up
@ -15,7 +15,7 @@ services:
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
environment: environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-muyu2026} MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}
MYSQL_DATABASE: ${MYSQL_DATABASE:-muyu_wms} MYSQL_DATABASE: ${MYSQL_DATABASE:-muyu_wms}
TZ: Asia/Shanghai TZ: Asia/Shanghai
ports: ports:
@ -26,7 +26,7 @@ services:
- ./scripts/init-iloom.sql:/docker-entrypoint-initdb.d/02-iloom.sql - ./scripts/init-iloom.sql:/docker-entrypoint-initdb.d/02-iloom.sql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-muyu2026}"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 10 retries: 10
@ -35,11 +35,15 @@ services:
build: build:
context: . context: .
dockerfile: gateway/Dockerfile dockerfile: gateway/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${GATEWAY_PORT:-8080}:8080" - "${GATEWAY_PORT:-8080}:8080"
environment: environment:
- PORT=8080 - PORT=8080
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- AUTH_SERVICE_URL=http://auth-service:8081 - AUTH_SERVICE_URL=http://auth-service:8081
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
- TEXTILE_SERVICE_URL=http://textile-service:8083 - TEXTILE_SERVICE_URL=http://textile-service:8083
@ -59,13 +63,17 @@ services:
build: build:
context: . context: .
dockerfile: auth-service/Dockerfile dockerfile: auth-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${AUTH_SERVICE_PORT:-8081}:8081" - "${AUTH_SERVICE_PORT:-8081}:8081"
environment: environment:
- PORT=8081 - PORT=8081
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- JWT_SECRET=${JWT_SECRET:-muyu-wms-jwt-secret-key-2026} - JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-muyu-wms-jwt-refresh-2026} - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:?Set JWT_REFRESH_SECRET in .env}
- MUYU_SYSTEM_RPC_ADDR=${MUYU_SYSTEM_RPC_ADDR:-system-rpc:9001} - MUYU_SYSTEM_RPC_ADDR=${MUYU_SYSTEM_RPC_ADDR:-system-rpc:9001}
depends_on: depends_on:
mysql: mysql:
@ -80,12 +88,16 @@ services:
build: build:
context: . context: .
dockerfile: purchaser-service/Dockerfile dockerfile: purchaser-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${PURCHASER_SERVICE_PORT:-8082}:8082" - "${PURCHASER_SERVICE_PORT:-8082}:8082"
environment: environment:
- PORT=8082 - PORT=8082
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- MUYU_INVENTORY_RPC_ADDR=${MUYU_INVENTORY_RPC_ADDR:-inventory-rpc:9002} - MUYU_INVENTORY_RPC_ADDR=${MUYU_INVENTORY_RPC_ADDR:-inventory-rpc:9002}
depends_on: depends_on:
mysql: mysql:
@ -100,12 +112,16 @@ services:
build: build:
context: . context: .
dockerfile: textile-service/Dockerfile dockerfile: textile-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${TEXTILE_SERVICE_PORT:-8083}:8083" - "${TEXTILE_SERVICE_PORT:-8083}:8083"
environment: environment:
- PORT=8083 - PORT=8083
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
depends_on: depends_on:
mysql: mysql:
@ -120,12 +136,16 @@ services:
build: build:
context: . context: .
dockerfile: washing-service/Dockerfile dockerfile: washing-service/Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
GOPROXY: ${GOPROXY-https://goproxy.cn,direct}
APK_MIRROR: ${APK_MIRROR-mirrors.aliyun.com}
ports: ports:
- "${WASHING_SERVICE_PORT:-8084}:8084" - "${WASHING_SERVICE_PORT:-8084}:8084"
environment: environment:
- PORT=8084 - PORT=8084
- DB_DSN=${DB_DSN:-root:muyu2026@tcp(mysql:3306)/muyu_wms?parseTime=true&charset=utf8mb4} - DB_DSN=${DB_DSN:?Set DB_DSN in .env}
- INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:-internal-service-shared-secret} - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY:?Set INTERNAL_SERVICE_KEY in .env}
- PURCHASER_SERVICE_URL=http://purchaser-service:8082 - PURCHASER_SERVICE_URL=http://purchaser-service:8082
depends_on: depends_on:
mysql: mysql:
@ -138,8 +158,10 @@ services:
frontend: frontend:
build: build:
context: . context: ./iloom-flatten
dockerfile: iloom-flatten/Dockerfile dockerfile: Dockerfile
args:
BASE_REGISTRY: ${BASE_REGISTRY-harbor-in-k3s.cheverjohn.me/library/}
ports: ports:
- "${FRONTEND_PORT:-3015}:3015" - "${FRONTEND_PORT:-3015}:3015"
depends_on: depends_on:

View File

@ -1,11 +1,16 @@
FROM golang:1.24-alpine AS builder ARG BASE_REGISTRY=harbor-in-k3s.cheverjohn.me/library/
FROM ${BASE_REGISTRY}golang:1.24-alpine AS builder
ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
WORKDIR /app WORKDIR /app
COPY shared/ ./shared/ COPY shared/ ./shared/
COPY gateway/ ./gateway/ COPY gateway/ ./gateway/
WORKDIR /app/gateway WORKDIR /app/gateway
RUN go build -o /gateway ./cmd/ RUN go build -o /gateway ./cmd/
FROM alpine:3.20 FROM ${BASE_REGISTRY}alpine:3.20
ARG APK_MIRROR=mirrors.aliyun.com
RUN if [ -n "$APK_MIRROR" ]; then sed -i "s|dl-cdn.alpinelinux.org|$APK_MIRROR|g" /etc/apk/repositories; fi
RUN apk add --no-cache wget RUN apk add --no-cache wget
COPY --from=builder /gateway /gateway COPY --from=builder /gateway /gateway
EXPOSE 8080 EXPOSE 8080

View File

@ -29,8 +29,17 @@ func main() {
v1 := r.Group("/api/v1") v1 := r.Group("/api/v1")
{ {
// /api/v1/auth/* -> AUTH_SERVICE_URL (no auth required) // /api/v1/auth/* -> AUTH_SERVICE_URL
v1.Any("/auth/*path", proxy.NewReverseProxy(cfg.AuthServiceURL)) // 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) // /api/v1/common/* -> AUTH_SERVICE_URL (JWT required)
common := v1.Group("/common", middleware.AuthRequired(cfg.JWTSecret)) common := v1.Group("/common", middleware.AuthRequired(cfg.JWTSecret))
@ -56,12 +65,31 @@ func main() {
middleware.RoleRequired("washing"), middleware.RoleRequired("washing"),
) )
washing.Any("/*path", proxy.NewReverseProxy(cfg.WashingServiceURL)) 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 // WebSocket proxy — route by role (JWT + role check applied)
r.GET("/ws/v1/purchaser", proxy.NewWSProxy(cfg.PurchaserServiceURL)) r.GET("/ws/v1/purchaser",
r.GET("/ws/v1/textile", proxy.NewWSProxy(cfg.TextileServiceURL)) middleware.AuthRequired(cfg.JWTSecret),
r.GET("/ws/v1/washing", proxy.NewWSProxy(cfg.WashingServiceURL)) middleware.RoleRequired("purchaser"),
proxy.NewWSProxy(cfg.PurchaserServiceURL, cfg.AllowOrigins),
)
r.GET("/ws/v1/textile",
middleware.AuthRequired(cfg.JWTSecret),
middleware.RoleRequired("textile"),
proxy.NewWSProxy(cfg.TextileServiceURL, cfg.AllowOrigins),
)
r.GET("/ws/v1/washing",
middleware.AuthRequired(cfg.JWTSecret),
middleware.RoleRequired("washing"),
proxy.NewWSProxy(cfg.WashingServiceURL, cfg.AllowOrigins),
)
addr := ":" + cfg.Port addr := ":" + cfg.Port
log.Printf("gateway listening on %s", addr) log.Printf("gateway listening on %s", addr)

View File

@ -12,17 +12,19 @@ type Config struct {
PurchaserServiceURL string PurchaserServiceURL string
TextileServiceURL string TextileServiceURL string
WashingServiceURL string WashingServiceURL string
MuyuGatewayURL string
AllowOrigins []string AllowOrigins []string
} }
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8080"), Port: getEnv("PORT", "8080"),
JWTSecret: getEnv("JWT_SECRET", "muyu-wms-jwt-secret-key-2026"), JWTSecret: getEnv("JWT_SECRET", ""),
AuthServiceURL: getEnv("AUTH_SERVICE_URL", "http://localhost:8081"), AuthServiceURL: getEnv("AUTH_SERVICE_URL", "http://localhost:8081"),
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
TextileServiceURL: getEnv("TEXTILE_SERVICE_URL", "http://localhost:8083"), TextileServiceURL: getEnv("TEXTILE_SERVICE_URL", "http://localhost:8083"),
WashingServiceURL: getEnv("WASHING_SERVICE_URL", "http://localhost:8084"), WashingServiceURL: getEnv("WASHING_SERVICE_URL", "http://localhost:8084"),
MuyuGatewayURL: getEnv("MUYU_GATEWAY_URL", "http://localhost:8888"),
AllowOrigins: strings.Split(getEnv("ALLOW_ORIGINS", "*"), ","), AllowOrigins: strings.Split(getEnv("ALLOW_ORIGINS", "*"), ","),
} }
} }

View File

@ -11,11 +11,29 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var upgrader = websocket.Upgrader{ func newUpgrader(allowedOrigins []string) websocket.Upgrader {
CheckOrigin: func(r *http.Request) bool { return true }, originSet := make(map[string]bool, len(allowedOrigins))
for _, o := range allowedOrigins {
originSet[strings.TrimSpace(o)] = true
}
return websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if originSet["*"] {
return true
}
origin := r.Header.Get("Origin")
if origin == "" {
return false
}
return originSet[origin]
},
}
} }
func NewWSProxy(targetURL string) gin.HandlerFunc { func NewWSProxy(targetURL string, allowedOrigins []string) gin.HandlerFunc {
upgrader := newUpgrader(allowedOrigins)
return func(c *gin.Context) { return func(c *gin.Context) {
clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) clientConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
@ -50,6 +68,7 @@ func NewWSProxy(targetURL string) gin.HandlerFunc {
go pumpMessages(backendConn, clientConn, errCh) go pumpMessages(backendConn, clientConn, errCh)
<-errCh <-errCh
<-errCh
} }
} }

@ -1 +1 @@
Subproject commit 3093ae18c25f6d654e1c0c3a6cd546c04a5438d1 Subproject commit d085054437c10ef8bf09c983d115e97b0946881d

View File

@ -21,6 +21,17 @@ data:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; 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/ { location /ws/ {
@ -33,12 +44,23 @@ data:
proxy_read_timeout 86400; proxy_read_timeout 86400;
} }
# SPA fallback — index.html must never be cached
location / { location / {
try_files $uri $uri/ /index.html; 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; expires 30d;
add_header Cache-Control "public, immutable"; 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";
}
} }

View File

@ -1,3 +1,7 @@
## DEPRECATED: These raw K8s manifests are superseded by iloom-deploy/charts/.
## DataSource values must be injected via Kubernetes Secrets at deploy time.
## See iloom-deploy/charts/infra/templates/secrets.yaml for the Helm-based approach.
##
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
@ -13,7 +17,7 @@ data:
- etcd:2379 - etcd:2379
Key: system.rpc Key: system.rpc
DataSource: root:muyu2026@tcp(mysql:3306)/muyu_wms?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai DataSource: REPLACE_AT_DEPLOY_TIME
Cache: Cache:
- Host: redis:6379 - Host: redis:6379
@ -38,7 +42,7 @@ data:
- etcd:2379 - etcd:2379
Key: inventory.rpc Key: inventory.rpc
DataSource: root:muyu2026@tcp(mysql:3306)/muyu_wms?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai DataSource: REPLACE_AT_DEPLOY_TIME
Cache: Cache:
- Host: redis:6379 - Host: redis:6379
@ -60,7 +64,7 @@ data:
Port: 8888 Port: 8888
Auth: Auth:
AccessSecret: muyu-wms-jwt-secret-key-2026 AccessSecret: REPLACE_AT_DEPLOY_TIME
AccessExpire: 7200 AccessExpire: 7200
SystemRpc: SystemRpc:

97
k8s/10-infra/minio.yaml Normal file
View File

@ -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

View File

@ -60,6 +60,20 @@ spec:
key: JWT_REFRESH_SECRET key: JWT_REFRESH_SECRET
- name: MUYU_SYSTEM_RPC_ADDR - name: MUYU_SYSTEM_RPC_ADDR
value: "muyu-system-rpc:9001" 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: resources:
requests: requests:
cpu: 50m cpu: 50m

View File

@ -1,11 +1,16 @@
FROM golang:1.24-alpine AS builder ARG BASE_REGISTRY=harbor-in-k3s.cheverjohn.me/library/
FROM ${BASE_REGISTRY}golang:1.24-alpine AS builder
ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
WORKDIR /app WORKDIR /app
COPY shared/ ./shared/ COPY shared/ ./shared/
COPY purchaser-service/ ./purchaser-service/ COPY purchaser-service/ ./purchaser-service/
WORKDIR /app/purchaser-service WORKDIR /app/purchaser-service
RUN go build -o /purchaser-service ./cmd/ RUN go build -o /purchaser-service ./cmd/
FROM alpine:3.20 FROM ${BASE_REGISTRY}alpine:3.20
ARG APK_MIRROR=mirrors.aliyun.com
RUN if [ -n "$APK_MIRROR" ]; then sed -i "s|dl-cdn.alpinelinux.org|$APK_MIRROR|g" /etc/apk/repositories; fi
RUN apk add --no-cache wget RUN apk add --no-cache wget
COPY --from=builder /purchaser-service /purchaser-service COPY --from=builder /purchaser-service /purchaser-service
EXPOSE 8082 EXPOSE 8082

View File

@ -12,7 +12,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8082"), Port: getEnv("PORT", "8082"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"), InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
InventoryRPCAddr: getEnv("MUYU_INVENTORY_RPC_ADDR", "127.0.0.1:9002"), InventoryRPCAddr: getEnv("MUYU_INVENTORY_RPC_ADDR", "127.0.0.1:9002"),
} }

View File

@ -20,27 +20,43 @@ func NewProductHandler(svc *service.ProductService, wsHub *websocket.Hub) *Produ
} }
type CreateProductReq struct { type CreateProductReq struct {
ProductName string `json:"product_name" binding:"required"` ProductName string `json:"product_name"`
FabricCode string `json:"fabric_code" binding:"required"` FabricCode string `json:"fabric_code" binding:"required"`
Color string `json:"color" binding:"required"` Color string `json:"color"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` WarpWeight *float64 `json:"warp_weight"`
ProductionPrice *float64 `json:"production_price"` WeftWeight *float64 `json:"weft_weight"`
YarnTypes []string `json:"yarn_types"` TotalWeight *float64 `json:"total_weight"`
YarnRatios []float64 `json:"yarn_ratios"` 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 { type UpdateProductReq struct {
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
FabricCode string `json:"fabric_code"` FabricCode string `json:"fabric_code"`
Color string `json:"color"` Color string `json:"color"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` WarpWeight *float64 `json:"warp_weight"`
ProductionPrice *float64 `json:"production_price"` WeftWeight *float64 `json:"weft_weight"`
YarnTypes []string `json:"yarn_types"` TotalWeight *float64 `json:"total_weight"`
YarnRatios []float64 `json:"yarn_ratios"` 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 { type InboundReq struct {
@ -97,16 +113,30 @@ func (h *ProductHandler) Create(c *gin.Context) {
return 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{ svcReq := service.CreateProductRequest{
ProductName: req.ProductName, ProductName: req.ProductName,
FabricCode: req.FabricCode, FabricCode: req.FabricCode,
Color: req.Color, Color: req.Color,
ColorCode: req.ColorCode, ColorCode: req.ColorCode,
Weight: req.Weight, Weight: req.Weight,
WarpWeight: req.WarpWeight,
WeftWeight: req.WeftWeight,
TotalWeight: req.TotalWeight,
YarnUsagePerMeter: req.YarnUsagePerMeter, YarnUsagePerMeter: req.YarnUsagePerMeter,
ProductionPrice: req.ProductionPrice, ProductionPrice: req.ProductionPrice,
ImageURL: req.ImageURL,
ProductType: req.ProductType,
YarnTypes: req.YarnTypes, YarnTypes: req.YarnTypes,
YarnRatios: req.YarnRatios, YarnRatios: yarnRatios,
} }
product, err := h.svc.Create(c.Request.Context(), companyID, svcReq) product, err := h.svc.Create(c.Request.Context(), companyID, svcReq)
@ -143,16 +173,30 @@ func (h *ProductHandler) Update(c *gin.Context) {
return 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{ svcReq := service.UpdateProductRequest{
ProductName: req.ProductName, ProductName: req.ProductName,
FabricCode: req.FabricCode, FabricCode: req.FabricCode,
Color: req.Color, Color: req.Color,
ColorCode: req.ColorCode, ColorCode: req.ColorCode,
Weight: req.Weight, Weight: req.Weight,
WarpWeight: req.WarpWeight,
WeftWeight: req.WeftWeight,
TotalWeight: req.TotalWeight,
YarnUsagePerMeter: req.YarnUsagePerMeter, YarnUsagePerMeter: req.YarnUsagePerMeter,
ProductionPrice: req.ProductionPrice, ProductionPrice: req.ProductionPrice,
ImageURL: req.ImageURL,
ProductType: req.ProductType,
YarnTypes: req.YarnTypes, YarnTypes: req.YarnTypes,
YarnRatios: req.YarnRatios, YarnRatios: yarnRatios,
} }
if err := h.svc.Update(c.Request.Context(), id, companyID, svcReq); err != nil { if err := h.svc.Update(c.Request.Context(), id, companyID, svcReq); err != nil {

View File

@ -1,28 +1,94 @@
package model 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 { type Product struct {
ID string `json:"id" db:"id"` ID string `json:"id" db:"id"`
CompanyID string `json:"company_id" db:"company_id"` CompanyID string `json:"company_id" db:"company_id"`
ProductName string `json:"product_name" db:"product_name"` ProductName string `json:"product_name" db:"product_name"`
FabricCode string `json:"fabric_code" db:"fabric_code"` FabricCode string `json:"fabric_code" db:"fabric_code"`
Color string `json:"color" db:"color"` Color string `json:"color" db:"color"`
ColorCode string `json:"color_code" db:"color_code"` ColorCode string `json:"color_code" db:"color_code"`
Weight float64 `json:"weight" db:"weight"` Weight float64 `json:"weight" db:"weight"`
WarpWeight *float64 `json:"warp_weight,omitempty" db:"warp_weight"` WarpWeight *float64 `json:"warp_weight,omitempty" db:"warp_weight"`
WeftWeight *float64 `json:"weft_weight,omitempty" db:"weft_weight"` WeftWeight *float64 `json:"weft_weight,omitempty" db:"weft_weight"`
TotalWeight *float64 `json:"total_weight,omitempty" db:"total_weight"` TotalWeight *float64 `json:"total_weight,omitempty" db:"total_weight"`
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter" db:"yarn_usage_per_meter"` YarnUsagePerMeter float64 `json:"yarn_usage_per_meter" db:"yarn_usage_per_meter"`
ProductionPrice *float64 `json:"production_price,omitempty" db:"production_price"` ProductionPrice *float64 `json:"production_price,omitempty" db:"production_price"`
TotalStock float64 `json:"total_stock" db:"total_stock"` TotalStock float64 `json:"total_stock" db:"total_stock"`
RawFabricMeters float64 `json:"raw_fabric_meters" db:"raw_fabric_meters"` RawFabricMeters float64 `json:"raw_fabric_meters" db:"raw_fabric_meters"`
RawFabricRolls int `json:"raw_fabric_rolls" db:"raw_fabric_rolls"` RawFabricRolls int `json:"raw_fabric_rolls" db:"raw_fabric_rolls"`
ImageURL *string `json:"image_url,omitempty" db:"image_url"` ImageURL *string `json:"image_url,omitempty" db:"image_url"`
YarnTypes []string `json:"yarn_types,omitempty" db:"yarn_types"` ProductType string `json:"product_type" db:"product_type"`
YarnRatios []float64 `json:"yarn_ratios,omitempty" db:"yarn_ratios"` YarnTypes JSONStringSlice `json:"yarn_types,omitempty" db:"yarn_types"`
CreatedAt time.Time `json:"created_at" db:"created_at"` YarnRatios JSONYarnRatios `json:"yarn_ratios,omitempty" db:"yarn_ratios"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
} }
type ProductInventoryRecord struct { type ProductInventoryRecord struct {

View File

@ -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) { func (r *PlanRepo) CountByStatus(ctx context.Context, companyID string) (total, producing, completed int, err error) {
query := `SELECT query := `SELECT
COUNT(*), COUNT(*),
SUM(CASE WHEN status = 'producing' THEN 1 ELSE 0 END), COALESCE(SUM(CASE WHEN status = 'producing' THEN 1 ELSE 0 END), 0),
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0)
FROM ilm_production_plan WHERE purchaser_id = ?` FROM ilm_production_plan WHERE purchaser_id = ?`
err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &producing, &completed) err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &producing, &completed)
if err != nil { if err != nil {

View File

@ -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, query := `SELECT id, company_id, product_name, fabric_code, color, color_code,
weight, warp_weight, weft_weight, total_weight, yarn_usage_per_meter, weight, warp_weight, weft_weight, total_weight, yarn_usage_per_meter,
production_price, total_stock, raw_fabric_meters, raw_fabric_rolls, 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` FROM products WHERE company_id = ? ORDER BY created_at DESC`
rows, err := r.db.QueryContext(ctx, query, companyID) 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.Weight, &p.WarpWeight, &p.WeftWeight, &p.TotalWeight,
&p.YarnUsagePerMeter, &p.ProductionPrice, &p.YarnUsagePerMeter, &p.ProductionPrice,
&p.TotalStock, &p.RawFabricMeters, &p.RawFabricRolls, &p.TotalStock, &p.RawFabricMeters, &p.RawFabricRolls,
&p.ImageURL, &p.YarnTypes, &p.YarnRatios, &p.ImageURL, &p.ProductType, &p.YarnTypes, &p.YarnRatios,
&p.CreatedAt, &p.UpdatedAt, &p.CreatedAt, &p.UpdatedAt,
); err != nil { ); err != nil {
return nil, fmt.Errorf("scan product: %w", err) 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, (id, company_id, product_name, fabric_code, color, color_code,
weight, warp_weight, weft_weight, total_weight, yarn_usage_per_meter, weight, warp_weight, weft_weight, total_weight, yarn_usage_per_meter,
production_price, total_stock, raw_fabric_meters, raw_fabric_rolls, 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)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)` VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
_, err := r.db.ExecContext(ctx, query, _, err := r.db.ExecContext(ctx, query,
p.ID, p.CompanyID, p.ProductName, p.FabricCode, p.Color, p.ColorCode, p.ID, p.CompanyID, p.ProductName, p.FabricCode, p.Color, p.ColorCode,
p.Weight, p.WarpWeight, p.WeftWeight, p.TotalWeight, p.Weight, p.WarpWeight, p.WeftWeight, p.TotalWeight,
p.YarnUsagePerMeter, p.ProductionPrice, p.YarnUsagePerMeter, p.ProductionPrice,
p.TotalStock, p.RawFabricMeters, p.RawFabricRolls, p.TotalStock, p.RawFabricMeters, p.RawFabricRolls,
p.ImageURL, p.YarnTypes, p.YarnRatios, p.ImageURL, p.ProductType, p.YarnTypes, p.YarnRatios,
p.CreatedAt, p.UpdatedAt, p.CreatedAt, p.UpdatedAt,
) )
if err != nil { 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 = ?, product_name = ?, fabric_code = ?, color = ?, color_code = ?,
weight = ?, warp_weight = ?, weft_weight = ?, total_weight = ?, weight = ?, warp_weight = ?, weft_weight = ?, total_weight = ?,
yarn_usage_per_meter = ?, production_price = ?, 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 = ?` WHERE id = ?`
_, err := r.db.ExecContext(ctx, query, _, err := r.db.ExecContext(ctx, query,
p.ProductName, p.FabricCode, p.Color, p.ColorCode, p.ProductName, p.FabricCode, p.Color, p.ColorCode,
p.Weight, p.WarpWeight, p.WeftWeight, p.TotalWeight, p.Weight, p.WarpWeight, p.WeftWeight, p.TotalWeight,
p.YarnUsagePerMeter, p.ProductionPrice, p.YarnUsagePerMeter, p.ProductionPrice,
p.ImageURL, p.YarnTypes, p.YarnRatios, time.Now(), p.ImageURL, p.ProductType, p.YarnTypes, p.YarnRatios, time.Now(),
p.ID, p.ID,
) )
if err != nil { if err != nil {

View File

@ -20,28 +20,44 @@ func NewProductService(repo *repository.ProductRepo, db *sql.DB) *ProductService
return &ProductService{repo: repo, db: db} 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 { type CreateProductRequest struct {
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
FabricCode string `json:"fabric_code"` FabricCode string `json:"fabric_code"`
Color string `json:"color"` Color string `json:"color"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` WarpWeight *float64 `json:"warp_weight"`
ProductionPrice *float64 `json:"production_price"` WeftWeight *float64 `json:"weft_weight"`
YarnTypes []string `json:"yarn_types"` TotalWeight *float64 `json:"total_weight"`
YarnRatios []float64 `json:"yarn_ratios"` 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 { type UpdateProductRequest struct {
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
FabricCode string `json:"fabric_code"` FabricCode string `json:"fabric_code"`
Color string `json:"color"` Color string `json:"color"`
ColorCode string `json:"color_code"` ColorCode string `json:"color_code"`
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
YarnUsagePerMeter float64 `json:"yarn_usage_per_meter"` WarpWeight *float64 `json:"warp_weight"`
ProductionPrice *float64 `json:"production_price"` WeftWeight *float64 `json:"weft_weight"`
YarnTypes []string `json:"yarn_types"` TotalWeight *float64 `json:"total_weight"`
YarnRatios []float64 `json:"yarn_ratios"` 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 { 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) { func (s *ProductService) Create(ctx context.Context, companyID string, req CreateProductRequest) (*model.Product, error) {
now := time.Now() 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{ p := &model.Product{
ID: uuid.New().String(), ID: uuid.New().String(),
CompanyID: companyID, CompanyID: companyID,
@ -78,13 +109,18 @@ func (s *ProductService) Create(ctx context.Context, companyID string, req Creat
Color: req.Color, Color: req.Color,
ColorCode: req.ColorCode, ColorCode: req.ColorCode,
Weight: req.Weight, Weight: req.Weight,
WarpWeight: req.WarpWeight,
WeftWeight: req.WeftWeight,
TotalWeight: req.TotalWeight,
YarnUsagePerMeter: req.YarnUsagePerMeter, YarnUsagePerMeter: req.YarnUsagePerMeter,
ProductionPrice: req.ProductionPrice, ProductionPrice: req.ProductionPrice,
ImageURL: req.ImageURL,
ProductType: productType,
TotalStock: 0, TotalStock: 0,
RawFabricMeters: 0, RawFabricMeters: 0,
RawFabricRolls: 0, RawFabricRolls: 0,
YarnTypes: req.YarnTypes, YarnTypes: model.JSONStringSlice(req.YarnTypes),
YarnRatios: req.YarnRatios, YarnRatios: yarnRatios,
CreatedAt: now, CreatedAt: now,
UpdatedAt: 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 { 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{ p := &model.Product{
ID: id, ID: id,
ProductName: req.ProductName, ProductName: req.ProductName,
@ -103,10 +153,15 @@ func (s *ProductService) Update(ctx context.Context, id, companyID string, req U
Color: req.Color, Color: req.Color,
ColorCode: req.ColorCode, ColorCode: req.ColorCode,
Weight: req.Weight, Weight: req.Weight,
WarpWeight: req.WarpWeight,
WeftWeight: req.WeftWeight,
TotalWeight: req.TotalWeight,
YarnUsagePerMeter: req.YarnUsagePerMeter, YarnUsagePerMeter: req.YarnUsagePerMeter,
ProductionPrice: req.ProductionPrice, ProductionPrice: req.ProductionPrice,
YarnTypes: req.YarnTypes, ImageURL: req.ImageURL,
YarnRatios: req.YarnRatios, ProductType: productType,
YarnTypes: model.JSONStringSlice(req.YarnTypes),
YarnRatios: yarnRatios,
} }
return s.repo.Update(ctx, p) return s.repo.Update(ctx, p)
} }

112
scripts/deploy.sh Executable file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Deploy iloom (backend services + frontend) in integrated mode against a running muyu-apiserver stack.
#
# Prerequisites:
# - muyu-apiserver already running (cd ../muyu-apiserver/deploy && docker compose up -d)
# - .env present in repo root (copy from .env.example and fill in secrets)
#
# Usage: ./scripts/deploy.sh [--no-mirror]
# --no-mirror Build images against upstream registries (Docker Hub, proxy.golang.org,
# official Alpine CDN) instead of the Harbor/Aliyun/goproxy.cn mirrors that
# the Dockerfiles use by default.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
MUYU_NETWORK="deploy_default"
MUYU_MYSQL_CONTAINER="muyu-mysql"
SERVICES=(gateway auth-service purchaser-service textile-service washing-service frontend)
COMPOSE_FILE="docker-compose.integrated.yml"
NO_MIRROR=false
for arg in "$@"; do
case "$arg" in
--no-mirror) NO_MIRROR=true ;;
*) ;;
esac
done
log() { printf '\n==> %s\n' "$1"; }
die() { printf '\nERROR: %s\n' "$1" >&2; exit 1; }
[ -f .env ] || die ".env not found in $ROOT_DIR. Copy .env.example to .env and fill in real values first."
while IFS='=' read -r key value; do
[[ -z "$key" || "$key" == \#* ]] && continue
export "$key=$value"
done < <(grep -v '^\s*#' .env | grep '=')
[ -n "${DB_DSN:-}" ] || die "DB_DSN is not set in .env"
[ -n "${JWT_SECRET:-}" ] || die "JWT_SECRET is not set in .env"
[ -n "${JWT_REFRESH_SECRET:-}" ] || die "JWT_REFRESH_SECRET is not set in .env"
[ -n "${INTERNAL_SERVICE_KEY:-}" ] || die "INTERNAL_SERVICE_KEY is not set in .env"
if [ "$NO_MIRROR" = true ]; then
log "Disabling build mirrors (--no-mirror): using upstream registries"
export BASE_REGISTRY=""
export GOPROXY="https://proxy.golang.org,direct"
export APK_MIRROR=""
fi
log "Checking muyu-apiserver dependencies"
docker network inspect "$MUYU_NETWORK" >/dev/null 2>&1 || \
die "Docker network '$MUYU_NETWORK' not found. Start muyu-apiserver first: cd ../muyu-apiserver/deploy && docker compose up -d"
docker inspect -f '{{.State.Running}}' "$MUYU_MYSQL_CONTAINER" 2>/dev/null | grep -q true || \
die "Container '$MUYU_MYSQL_CONTAINER' is not running. Start muyu-apiserver first."
for c in muyu-system-rpc muyu-inventory-rpc; do
docker inspect -f '{{.State.Running}}' "$c" >/dev/null 2>&1 || die "Container '$c' is not running."
done
echo "muyu-apiserver stack is up (network '$MUYU_NETWORK' found, mysql/system-rpc/inventory-rpc running)."
log "Applying iloom schema to shared muyu_wms database (idempotent)"
docker exec -i "$MUYU_MYSQL_CONTAINER" mysql -uroot -p"${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}" < scripts/init-iloom.sql
echo "iloom tables (ilm_*) ensured in muyu_wms."
log "Building and starting iloom services: ${SERVICES[*]}"
docker compose -f "$COMPOSE_FILE" up -d --build "${SERVICES[@]}"
log "Waiting for services to become healthy"
port_for() {
case "$1" in
gateway) echo "${GATEWAY_PORT:-8080}" ;;
auth-service) echo "${AUTH_SERVICE_PORT:-8081}" ;;
purchaser-service) echo "${PURCHASER_SERVICE_PORT:-8082}" ;;
textile-service) echo "${TEXTILE_SERVICE_PORT:-8083}" ;;
washing-service) echo "${WASHING_SERVICE_PORT:-8084}" ;;
frontend) echo "${FRONTEND_PORT:-3015}" ;;
esac
}
path_for() {
case "$1" in
frontend) echo "/" ;;
*) echo "/health" ;;
esac
}
all_ok=true
for svc in "${SERVICES[@]}"; do
port="$(port_for "$svc")"
path="$(path_for "$svc")"
ok=false
for _ in $(seq 1 30); do
if curl -fsS -o /dev/null "http://localhost:${port}${path}" 2>/dev/null; then
ok=true
break
fi
sleep 2
done
if [ "$ok" = true ]; then
echo " [OK] $svc (http://localhost:${port}${path})"
else
echo " [FAIL] $svc (http://localhost:${port}${path}) did not become healthy in time"
all_ok=false
fi
done
if [ "$all_ok" = true ]; then
log "Deploy complete. All services healthy."
else
log "Deploy finished with failures. Check logs: docker compose -f $COMPOSE_FILE logs -f <service>"
exit 1
fi

View File

@ -1,11 +1,16 @@
FROM golang:1.24-alpine AS builder ARG BASE_REGISTRY=harbor-in-k3s.cheverjohn.me/library/
FROM ${BASE_REGISTRY}golang:1.24-alpine AS builder
ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
WORKDIR /app WORKDIR /app
COPY shared/ ./shared/ COPY shared/ ./shared/
COPY textile-service/ ./textile-service/ COPY textile-service/ ./textile-service/
WORKDIR /app/textile-service WORKDIR /app/textile-service
RUN go build -o /textile-service ./cmd/ RUN go build -o /textile-service ./cmd/
FROM alpine:3.20 FROM ${BASE_REGISTRY}alpine:3.20
ARG APK_MIRROR=mirrors.aliyun.com
RUN if [ -n "$APK_MIRROR" ]; then sed -i "s|dl-cdn.alpinelinux.org|$APK_MIRROR|g" /etc/apk/repositories; fi
RUN apk add --no-cache wget RUN apk add --no-cache wget
COPY --from=builder /textile-service /textile-service COPY --from=builder /textile-service /textile-service
EXPOSE 8083 EXPOSE 8083

View File

@ -12,7 +12,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8083"), Port: getEnv("PORT", "8083"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"), InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
} }

View File

@ -91,8 +91,8 @@ func (r *PlanRepo) CountByFactory(ctx context.Context, factoryCompanyID string)
query := ` query := `
SELECT SELECT
COUNT(*), COUNT(*),
SUM(CASE WHEN pp.status IN ('pending', 'in_progress') THEN 1 ELSE 0 END), COALESCE(SUM(CASE WHEN pp.status IN ('pending', 'in_progress') THEN 1 ELSE 0 END), 0),
SUM(CASE WHEN pp.status = 'completed' THEN 1 ELSE 0 END) COALESCE(SUM(CASE WHEN pp.status = 'completed' THEN 1 ELSE 0 END), 0)
FROM ilm_production_plan pp FROM ilm_production_plan pp
JOIN ilm_plan_factory pf ON pp.id = pf.plan_id JOIN ilm_plan_factory pf ON pp.id = pf.plan_id
WHERE pf.factory_id = ? AND pf.factory_type = 'textile'` WHERE pf.factory_id = ? AND pf.factory_type = 'textile'`

View File

@ -1,11 +1,16 @@
FROM golang:1.24-alpine AS builder ARG BASE_REGISTRY=harbor-in-k3s.cheverjohn.me/library/
FROM ${BASE_REGISTRY}golang:1.24-alpine AS builder
ARG GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=${GOPROXY}
WORKDIR /app WORKDIR /app
COPY shared/ ./shared/ COPY shared/ ./shared/
COPY washing-service/ ./washing-service/ COPY washing-service/ ./washing-service/
WORKDIR /app/washing-service WORKDIR /app/washing-service
RUN go build -o /washing-service ./cmd/ RUN go build -o /washing-service ./cmd/
FROM alpine:3.20 FROM ${BASE_REGISTRY}alpine:3.20
ARG APK_MIRROR=mirrors.aliyun.com
RUN if [ -n "$APK_MIRROR" ]; then sed -i "s|dl-cdn.alpinelinux.org|$APK_MIRROR|g" /etc/apk/repositories; fi
RUN apk add --no-cache wget RUN apk add --no-cache wget
COPY --from=builder /washing-service /washing-service COPY --from=builder /washing-service /washing-service
EXPOSE 8084 EXPOSE 8084

View File

@ -12,7 +12,7 @@ type Config struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Port: getEnv("PORT", "8084"), Port: getEnv("PORT", "8084"),
DBDSN: getEnv("DB_DSN", "root:muyu2026@tcp(127.0.0.1:3306)/muyu_wms?parseTime=true&charset=utf8mb4"), DBDSN: getEnv("DB_DSN", ""),
InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"), InternalServiceKey: getEnv("INTERNAL_SERVICE_KEY", "dev-internal-key"),
PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"), PurchaserServiceURL: getEnv("PURCHASER_SERVICE_URL", "http://localhost:8082"),
} }

View File

@ -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) { func (r *PlanRepo) CountByStatus(ctx context.Context, companyID string) (total, pending, completed int, err error) {
query := `SELECT query := `SELECT
COUNT(*), COUNT(*),
SUM(CASE WHEN status IN ('pending', 'washing') THEN 1 ELSE 0 END), COALESCE(SUM(CASE WHEN status IN ('pending', 'washing') THEN 1 ELSE 0 END), 0),
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0)
FROM ilm_washing_plan WHERE washing_factory_id = ?` FROM ilm_washing_plan WHERE washing_factory_id = ?`
err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &pending, &completed) err = r.db.QueryRowContext(ctx, query, companyID).Scan(&total, &pending, &completed)
if err != nil { if err != nil {