┌─────────────────────────────────────────────────────────────────────────────┐
│ Internet / VPN │
└────────────────────────────────┬────────────────────────────────────────────┘
│
┌─────────────▼────────────────┐
│ Load Balancer (NGINX) │
│ - SSL Termination │
│ - Layer 7 LB │
└─────────────┬────────────────┘
│
┌─────────────────────────────────┼────────────────────────────────────────────┐
│ Kubernetes Cluster │ │
│ │ │
│ ┌──────────────────────────────▼─────────────────────────────────────┐ │
│ │ Ingress Controller │ │
│ │ (nginx-ingress) │ │
│ └──────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼───────────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────────┐ ┌────────▼──────────┐ ┌────────▼──────────┐ │
│ │ API Service │ │ API Service │ │ API Service │ │
│ │ Pod 1 │ │ Pod 2 │ │ Pod 3 │ │
│ │ (Spring Boot) │ │ (Spring Boot) │ │ (Spring Boot) │ │
│ └──────┬──────────┘ └────────┬──────────┘ └────────┬──────────┘ │
│ │ │ │ │
│ └───────────────────────┼───────────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ┌────────▼────────┐ ┌──────▼─────────┐ ┌───▼────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ Kafka Connect │ │
│ │ StatefulSet │ │ Cluster │ │ Deployment │ │
│ │ (3 replicas) │ │ (6 pods) │ │ (3 workers) │ │
│ └─────────────────┘ └────────────────┘ └───┬────────────┘ │
│ │ │
│ ┌──────────────▼─────────────┐ │
│ │ Kafka Cluster │ │
│ │ StatefulSet │ │
│ │ (3 brokers) │ │
│ └────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────────▼────────┐ ┌──▼──────────┐ ┌▼────────────┐
│ Source DBs │ │ Source DBs │ │ Target DBs │
│ (MySQL) │ │ (Oracle) │ │ (PostgreSQL)│
└─────────────────┘ └─────────────┘ └─────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DMZ Zone (Public Network) │
│ ┌─────────────┐ │
│ │ Load Balancer│ (Public IP) │
│ └──────┬──────┘ │
└─────────┼───────────────────────────────────────────────────┘
│
┌─────────▼───────────────────────────────────────────────────┐
│ Application Zone (Private Network: 10.1.0.0/16) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ K8s Nodes (10.1.1.0/24) │ │
│ │ - node-1: 10.1.1.10 │ │
│ │ - node-2: 10.1.1.11 │ │
│ │ - node-3: 10.1.1.12 │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
┌─────────▼───────────────────────────────────────────────────┐
│ Data Zone (Private Network: 10.2.0.0/16) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ PostgreSQL Cluster (10.2.1.0/24) │ │
│ │ - pg-master: 10.2.1.10 │ │
│ │ - pg-slave1: 10.2.1.11 │ │
│ │ - pg-slave2: 10.2.1.12 │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Redis Cluster (10.2.2.0/24) │ │
│ │ - redis-1: 10.2.2.10 │ │
│ │ - redis-2: 10.2.2.11 │ │
│ │ - redis-3: 10.2.2.12 │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Kafka Cluster (10.2.3.0/24) │ │
│ │ - kafka-1: 10.2.3.10 │ │
│ │ - kafka-2: 10.2.3.11 │ │
│ │ - kafka-3: 10.2.3.12 │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
# namespaces.yaml
apiVersion: v1
kind: Namespace
metadata:
name: db-sync-prod
labels:
name: db-sync-prod
environment: production
---
apiVersion: v1
kind: Namespace
metadata:
name: db-sync-staging
labels:
name: db-sync-staging
environment: staging# api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: db-sync-api
namespace: db-sync-prod
labels:
app: db-sync-api
version: v1.0.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: db-sync-api
template:
metadata:
labels:
app: db-sync-api
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
serviceAccountName: db-sync-api
containers:
- name: api
image: your-registry.com/db-sync-api:v1.0.0
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: actuator
containerPort: 8081
protocol: TCP
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_OPTS
value: "-Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: db-sync-config
key: db.host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-sync-secrets
key: db.password
- name: KAFKA_BOOTSTRAP_SERVERS
valueFrom:
configMapKeyRef:
name: db-sync-config
key: kafka.bootstrap.servers
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: db-sync-config
key: redis.host
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: logs
mountPath: /app/logs
volumes:
- name: config
configMap:
name: db-sync-config
- name: logs
emptyDir: {}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- db-sync-api
topologyKey: kubernetes.io/hostname# api-service.yaml
apiVersion: v1
kind: Service
metadata:
name: db-sync-api
namespace: db-sync-prod
labels:
app: db-sync-api
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
- name: actuator
port: 8081
targetPort: 8081
protocol: TCP
selector:
app: db-sync-api
sessionAffinity: None# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: db-sync-ingress
namespace: db-sync-prod
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/limit-rps: "10"
spec:
tls:
- hosts:
- api.dbsync.example.com
secretName: dbsync-tls-secret
rules:
- host: api.dbsync.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: db-sync-api
port:
number: 80# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: db-sync-config
namespace: db-sync-prod
data:
application.yaml: |
server:
port: 8080
management:
server:
port: 8081
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
spring:
application:
name: db-sync-api
datasource:
url: jdbc:postgresql://${DB_HOST}:5432/dbsync_metadata
username: dbsync_user
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS}
data:
redis:
host: ${REDIS_HOST}
port: 6379
database: 0
timeout: 3000ms
db.host: "postgresql-primary.db-sync-prod.svc.cluster.local"
kafka.bootstrap.servers: "kafka-0.kafka-headless.db-sync-prod.svc.cluster.local:9092,kafka-1.kafka-headless.db-sync-prod.svc.cluster.local:9092,kafka-2.kafka-headless.db-sync-prod.svc.cluster.local:9092"
redis.host: "redis-cluster.db-sync-prod.svc.cluster.local"
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-sync-secrets
namespace: db-sync-prod
type: Opaque
stringData:
db.password: "your-secure-password"
redis.password: "your-redis-password"
jwt.secret: "your-jwt-secret-key"# kafka-connect-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: kafka-connect
namespace: db-sync-prod
labels:
app: kafka-connect
spec:
replicas: 3
selector:
matchLabels:
app: kafka-connect
template:
metadata:
labels:
app: kafka-connect
spec:
containers:
- name: kafka-connect
image: debezium/connect:2.5
ports:
- containerPort: 8083
name: rest-api
env:
- name: BOOTSTRAP_SERVERS
value: "kafka-0.kafka-headless:9092,kafka-1.kafka-headless:9092,kafka-2.kafka-headless:9092"
- name: GROUP_ID
value: "db-sync-connect-cluster"
- name: CONFIG_STORAGE_TOPIC
value: "db-sync-connect-configs"
- name: OFFSET_STORAGE_TOPIC
value: "db-sync-connect-offsets"
- name: STATUS_STORAGE_TOPIC
value: "db-sync-connect-status"
- name: CONFIG_STORAGE_REPLICATION_FACTOR
value: "3"
- name: OFFSET_STORAGE_REPLICATION_FACTOR
value: "3"
- name: STATUS_STORAGE_REPLICATION_FACTOR
value: "3"
- name: CONNECT_KEY_CONVERTER
value: "org.apache.kafka.connect.json.JsonConverter"
- name: CONNECT_VALUE_CONVERTER
value: "org.apache.kafka.connect.json.JsonConverter"
- name: CONNECT_KEY_CONVERTER_SCHEMAS_ENABLE
value: "false"
- name: CONNECT_VALUE_CONVERTER_SCHEMAS_ENABLE
value: "false"
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /
port: 8083
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8083
initialDelaySeconds: 30
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: kafka-connect
namespace: db-sync-prod
spec:
type: ClusterIP
ports:
- port: 8083
targetPort: 8083
selector:
app: kafka-connect# postgresql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: db-sync-prod
spec:
serviceName: postgresql-headless
replicas: 3
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: postgres:15-alpine
ports:
- containerPort: 5432
name: postgresql
env:
- name: POSTGRES_DB
value: "dbsync_metadata"
- name: POSTGRES_USER
value: "dbsync_user"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-sync-secrets
key: db.password
- name: PGDATA
value: "/var/lib/postgresql/data/pgdata"
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "ssd-storage"
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-headless
namespace: db-sync-prod
spec:
type: ClusterIP
clusterIP: None
ports:
- port: 5432
targetPort: 5432
selector:
app: postgresql
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-primary
namespace: db-sync-prod
spec:
type: ClusterIP
ports:
- port: 5432
targetPort: 5432
selector:
app: postgresql
role: primary# kafka-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
namespace: db-sync-prod
spec:
serviceName: kafka-headless
replicas: 3
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
spec:
containers:
- name: kafka
image: confluentinc/cp-kafka:7.5.0
ports:
- containerPort: 9092
name: plaintext
- containerPort: 9093
name: ssl
env:
- name: KAFKA_BROKER_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: KAFKA_ZOOKEEPER_CONNECT
value: "zookeeper:2181"
- name: KAFKA_ADVERTISED_LISTENERS
value: "PLAINTEXT://$(POD_NAME).kafka-headless:9092"
- name: KAFKA_LISTENERS
value: "PLAINTEXT://0.0.0.0:9092"
- name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR
value: "3"
- name: KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR
value: "3"
- name: KAFKA_TRANSACTION_STATE_LOG_MIN_ISR
value: "2"
- name: KAFKA_MIN_INSYNC_REPLICAS
value: "2"
- name: KAFKA_LOG_RETENTION_HOURS
value: "168" # 7天
- name: KAFKA_LOG_SEGMENT_BYTES
value: "1073741824" # 1GB
volumeMounts:
- name: data
mountPath: /var/lib/kafka/data
resources:
requests:
memory: "4Gi"
cpu: "2000m"
limits:
memory: "8Gi"
cpu: "4000m"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "ssd-storage"
resources:
requests:
storage: 500Giversion: '3.8'
services:
# PostgreSQL元数据库
postgres:
image: postgres:15-alpine
container_name: dbsync-postgres
environment:
POSTGRES_DB: dbsync_metadata
POSTGRES_USER: dbsync_user
POSTGRES_PASSWORD: dbsync_pass
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d
networks:
- dbsync-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dbsync_user -d dbsync_metadata"]
interval: 10s
timeout: 5s
retries: 5
# Redis
redis:
image: redis:7-alpine
container_name: dbsync-redis
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- dbsync-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
# Zookeeper(Kafka依赖)
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
container_name: dbsync-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
volumes:
- zookeeper-data:/var/lib/zookeeper/data
- zookeeper-logs:/var/lib/zookeeper/log
networks:
- dbsync-network
# Kafka
kafka:
image: confluentinc/cp-kafka:7.5.0
container_name: dbsync-kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
- "9093:9093"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:9093
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
volumes:
- kafka-data:/var/lib/kafka/data
networks:
- dbsync-network
healthcheck:
test: ["CMD-SHELL", "kafka-broker-api-versions --bootstrap-server localhost:9092"]
interval: 10s
timeout: 10s
retries: 5
# Kafka Connect
kafka-connect:
image: debezium/connect:2.5
container_name: dbsync-kafka-connect
depends_on:
kafka:
condition: service_healthy
ports:
- "8083:8083"
environment:
BOOTSTRAP_SERVERS: kafka:9092
GROUP_ID: db-sync-connect-cluster
CONFIG_STORAGE_TOPIC: db-sync-connect-configs
OFFSET_STORAGE_TOPIC: db-sync-connect-offsets
STATUS_STORAGE_TOPIC: db-sync-connect-status
CONFIG_STORAGE_REPLICATION_FACTOR: 1
OFFSET_STORAGE_REPLICATION_FACTOR: 1
STATUS_STORAGE_REPLICATION_FACTOR: 1
CONNECT_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
CONNECT_KEY_CONVERTER_SCHEMAS_ENABLE: false
CONNECT_VALUE_CONVERTER_SCHEMAS_ENABLE: false
networks:
- dbsync-network
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8083/ || exit 1"]
interval: 10s
timeout: 5s
retries: 5
# Spring Boot API
api:
build:
context: ../db-sync-api
dockerfile: Dockerfile
container_name: dbsync-api
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
kafka:
condition: service_healthy
kafka-connect:
condition: service_healthy
ports:
- "8080:8080"
- "8081:8081"
environment:
SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/dbsync_metadata
SPRING_DATASOURCE_USERNAME: dbsync_user
SPRING_DATASOURCE_PASSWORD: dbsync_pass
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
SPRING_DATA_REDIS_HOST: redis
KAFKA_CONNECT_URL: http://kafka-connect:8083
networks:
- dbsync-network
volumes:
- ./logs:/app/logs
# Prometheus(监控)
prometheus:
image: prom/prometheus:latest
container_name: dbsync-prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
networks:
- dbsync-network
# Grafana(可视化)
grafana:
image: grafana/grafana:latest
container_name: dbsync-grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana-data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
networks:
- dbsync-network
depends_on:
- prometheus
networks:
dbsync-network:
driver: bridge
volumes:
postgres-data:
redis-data:
zookeeper-data:
zookeeper-logs:
kafka-data:
prometheus-data:
grafana-data:#!/bin/bash
# start.sh
echo "Starting DB Sync Platform..."
# 创建必要的目录
mkdir -p logs
mkdir -p init-scripts
mkdir -p monitoring/grafana/dashboards
mkdir -p monitoring/grafana/datasources
# 启动所有服务
docker-compose up -d
# 等待服务启动
echo "Waiting for services to be ready..."
sleep 30
# 检查服务状态
docker-compose ps
echo "DB Sync Platform started successfully!"
echo "API: http://localhost:8080"
echo "Kafka Connect: http://localhost:8083"
echo "Prometheus: http://localhost:9090"
echo "Grafana: http://localhost:3000 (admin/admin)"# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Spring Boot API
- job_name: 'db-sync-api'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['api:8081']
labels:
application: 'db-sync-api'
environment: 'prod'
# Kafka Connect
- job_name: 'kafka-connect'
static_configs:
- targets: ['kafka-connect:8083']
# PostgreSQL Exporter
- job_name: 'postgresql'
static_configs:
- targets: ['postgres-exporter:9187']
# Redis Exporter
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
# Kafka Exporter
- job_name: 'kafka'
static_configs:
- targets: ['kafka-exporter:9308']详见 monitoring/grafana/dashboards/db-sync-dashboard.json
# .gitlab-ci.yml
stages:
- build
- test
- package
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_REGISTRY: "your-registry.com"
IMAGE_NAME: "db-sync-api"
# Maven编译
build:
stage: build
image: maven:3.9-openjdk-17
script:
- mvn clean compile
cache:
paths:
- .m2/repository
only:
- branches
# 单元测试
test:
stage: test
image: maven:3.9-openjdk-17
script:
- mvn test
artifacts:
reports:
junit: '**/target/surefire-reports/TEST-*.xml'
coverage: '/Total.*?(\d+\.?\d*)%/'
only:
- branches
# 打包Docker镜像
package:
stage: package
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY
- docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA .
- docker tag $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA $DOCKER_REGISTRY/$IMAGE_NAME:latest
- docker push $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
- docker push $DOCKER_REGISTRY/$IMAGE_NAME:latest
only:
- main
- develop
# 部署到Kubernetes
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context staging
- kubectl set image deployment/db-sync-api api=$DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA -n db-sync-staging
- kubectl rollout status deployment/db-sync-api -n db-sync-staging
environment:
name: staging
only:
- develop
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context production
- kubectl set image deployment/db-sync-api api=$DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA -n db-sync-prod
- kubectl rollout status deployment/db-sync-api -n db-sync-prod
environment:
name: production
when: manual
only:
- main文档版本:v1.0 最后更新:2025-01-30