Files
starRiverProperty/docs/superpowers/plans/2026-05-01-v2-test-env-setup.md
T
反编译工作区 8b15445328 feat: add service config templates and extraction script
Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
2026-05-01 19:38:01 +08:00

40 KiB
Raw Blame History

V2 全系统功能测试环境搭建 — 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 搭建 cw-elevator-application V2 (v2.0.7) 的全系统集成功能测试环境,一键搭建 17 个组件并在 15-20 分钟内完成全部功能验证。

Architecture: Docker Compose 管理基础组件 (Consul+Redis+Kafka+Nginx)Bash 脚本编排 13 个本机 Java 服务,复用现有 MySQL (192.168.3.12:3307),配置模板驱动 + 环境变量统一管理。

Tech Stack: Bash 4+, Docker Compose v2, JDK 8, Maven 3.5+, Python 3.8+ (pytest), MySQL 5.7

关联 Spec: docs/superpowers/specs/2026-05-01-v2-test-env-setup-design.md


文件映射

产出文件 职责
源码/scripts/test-env/config/env.sh 统一环境变量 (IP/端口/密码/路径)
源码/scripts/test-env/docker-compose.infra.yml 基础组件容器编排 (Consul+Redis+Kafka+Nginx)
源码/scripts/test-env/prepare-db.sh 数据库恢复 (11 个库从 data_backup/)
源码/scripts/test-env/prepare-services.sh 解压 tar.gz + 从模板生成配置
源码/scripts/test-env/build-elevator-v2.sh 编译 V2 电梯应用
源码/scripts/test-env/config/service-templates/elevator-v2.properties V2 电梯配置模板
源码/scripts/test-env/config/service-templates/crk-std.properties CRK 后端配置模板
源码/scripts/test-env/config/service-templates/alarm.properties 报警应用配置模板
源码/scripts/test-env/config/service-templates/ninca-common.properties ninca-common 配置模板
源码/scripts/test-env/config/service-templates/component-org.properties component-org 配置模板
源码/scripts/test-env/start-all.sh 按拓扑序启动全部服务
源码/scripts/test-env/stop-all.sh 按逆序停止全部服务
源码/scripts/test-env/health-check.sh 探活检查
源码/scripts/test-env/verify-functional.sh 功能验证 (对拍+策略+联动)
源码/scripts/test-env/setup.sh 主入口脚本 (编排所有 Phase)

Task 1: 创建目录结构和 env.sh

Files:

  • Create: 源码/scripts/test-env/config/env.sh

  • Create: 源码/scripts/test-env/config/service-templates/

  • Step 1: 创建目录结构

mkdir -p 源码/scripts/test-env/config/service-templates
  • Step 2: 编写 env.sh

编辑 源码/scripts/test-env/config/env.sh,内容如下:

#!/bin/bash
# V2 测试环境 — 统一环境变量
# 所有脚本 source 此文件获取统一配置

set -euo pipefail

# ============================================
# 路径
# ============================================
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
STAR_CENTER="$REPO_ROOT/../星中心"
DATA_BACKUP="$REPO_ROOT/../data_backup"
TEST_ENV_DIR="$REPO_ROOT/scripts/test-env"
SERVICE_DIR="$TEST_ENV_DIR/services"
LOG_DIR="$TEST_ENV_DIR/logs"

# ============================================
# Java
# ============================================
JAVA_HOME="${DEPLOY_JDK8:-/usr/lib/jvm/java-8-openjdk-amd64}"
JAVA="$JAVA_HOME/bin/java"
JAVA_OPTS_HEAVY="-Xmx3072m -Xms3072m -Xmn1024m"   # CRK, alarm, elevator
JAVA_OPTS_LIGHT="-Xmx2048m -Xms512m"                # common, org, portal
JAVA_OPTS_DEBUG="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n"

# ============================================
# 基础设施地址
# ============================================
MYSQL_HOST=192.168.3.12
MYSQL_PORT=3307
MYSQL_USER=root
MYSQL_PASS=123456
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASS="1qaz!QAZ"
CONSUL_HOST=127.0.0.1
CONSUL_PORT=8500
KAFKA_HOST=127.0.0.1
KAFKA_PORT=9092
ZK_HOST=127.0.0.1
ZK_PORT=2181

# ============================================
# 服务端口
# ============================================
PORT_ELEVATOR_V2=18081
PORT_ELEVATOR_V1=18080
PORT_CRK_STD=16106
PORT_CRK_MGMT=16114
PORT_ALARM=17011
PORT_ALARM_MGMT=17211
PORT_CWOS_PORTAL=33008
PORT_COMPONENT_ORG=33011
PORT_NINCA_COMMON=33010
PORT_CWOS_MANAGER=3721
PORT_SYSTEM_API=3333
PORT_SNAP_APP=33012
PORT_VEHICLE_APP=33013
PORT_PERSON_FILE=33014
PORT_MONITOR_APP=33015
PORT_NGINX=8090

# ============================================
# 数据库名
# ============================================
DB_ELEVATOR="cw-elevator-application"
DB_CRK="ninca_crk_std"
DB_ALARM="alarm_deploy"
DB_MANAGER="cwos_manager"
DB_PORTAL="cwos_portal"
DB_COMMON="ninca_common"
DB_COMPONENT_ORG="component-organization"
DB_ODS="ods"
DB_THIRDPARTY="cloudwalk_device_thirdparty"
DB_G="g"
DB_P="p"
DB_12="12"
DB_34="34"

# ============================================
# Maven
# ============================================
MVN="mvn"
MVN_OPTS="-DskipTests -Dformatter-maven-plugin.version=2.16.0"
ELEVATOR_POM="$REPO_ROOT/maven-cw-elevator-application/pom.xml"

# ============================================
# 日志颜色
# ============================================
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
log_info()  { echo -e "${BLUE}[INFO]${NC}  $*"; }
log_ok()    { echo -e "${GREEN}[OK]${NC}    $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
  • Step 3: 测试 env.sh 可被 source
bash -c 'source 源码/scripts/test-env/config/env.sh && echo "MYSQL=$MYSQL_HOST:$MYSQL_PORT"'
# 预期: MYSQL=192.168.3.12:3307
  • Step 4: Commit
git add 源码/scripts/test-env/config/env.sh
git commit -m "feat: add test environment unified config (env.sh)"

Task 2: Docker Compose — 合并基础组件

Files:

  • Create: 源码/scripts/test-env/docker-compose.infra.yml

  • Step 1: 编写合并后的 docker-compose.infra.yml

# V2 测试环境 — 基础组件 (Consul + Redis + Kafka + ZK + Nginx)
# 合并自: deploy/consul-docker/docker-compose.yml, docker-compose.frontend-local.yml
version: '3.8'

services:
  consul:
    image: hashicorp/consul:1.22
    container_name: v2test-consul
    restart: unless-stopped
    ports:
      - "${CONSUL_PORT:-8500}:8500"
    command: >
      agent -server -bootstrap-expect=1 -ui
      -client=0.0.0.0 -bind=0.0.0.0
      -data-dir=/consul/data
    volumes:
      - consul-data:/consul/data

  redis:
    image: redis:7-alpine
    container_name: v2test-redis
    restart: unless-stopped
    ports:
      - "${REDIS_PORT:-6379}:6379"
    command: redis-server --requirepass "${REDIS_PASS:-1qaz!QAZ}"

  zookeeper:
    image: bitnami/zookeeper:3.9
    container_name: v2test-zookeeper
    restart: unless-stopped
    ports:
      - "${ZK_PORT:-2181}:2181"
    environment:
      ALLOW_ANONYMOUS_LOGIN: "yes"

  kafka:
    image: bitnami/kafka:3.6
    container_name: v2test-kafka
    restart: unless-stopped
    ports:
      - "${KAFKA_PORT:-9092}:9092"
    environment:
      KAFKA_CFG_NODE_ID: 1
      KAFKA_CFG_PROCESS_ROLES: "broker,controller"
      KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "1@localhost:9093"
      KAFKA_CFG_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093"
      KAFKA_CFG_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092"
      KAFKA_CFG_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
      KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT"
      KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true"
    depends_on:
      - zookeeper

  nginx:
    image: nginx:alpine
    container_name: v2test-nginx
    restart: unless-stopped
    ports:
      - "${PORT_NGINX:-8090}:80"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ../../frontend:/data/cwos/frontend:ro
      - ../../nginx.frontend-local.conf:/etc/nginx/conf.d/default.conf:ro

volumes:
  consul-data:
  • Step 2: 验证 Docker Compose 语法
cd 源码/scripts/test-env
source config/env.sh
docker compose -f docker-compose.infra.yml config --quiet
# 预期: 无输出 (语法正确)
  • Step 3: 启动并验证所有容器
docker compose -f 源码/scripts/test-env/docker-compose.infra.yml up -d
# 等待启动
sleep 15

# 验证 Consul
curl -sf http://127.0.0.1:8500/v1/status/leader && echo "Consul OK"

# 验证 Redis
docker exec v2test-redis redis-cli -a 1qaz!QAZ PING | grep PONG && echo "Redis OK"

# 验证 Kafka
docker exec v2test-kafka kafka-topics.sh --bootstrap-server localhost:9092 --list && echo "Kafka OK"

# 验证 Nginx
curl -sf http://localhost:8090/ && echo "Nginx OK"
  • Step 4: 清理并 commit
docker compose -f 源码/scripts/test-env/docker-compose.infra.yml down
git add 源码/scripts/test-env/docker-compose.infra.yml
git commit -m "feat: add Docker Compose for test infra (Consul+Redis+Kafka+ZK+Nginx)"

Task 3: prepare-db.sh — 数据库恢复

Files:

  • Create: 源码/scripts/test-env/prepare-db.sh

  • Step 1: 编写数据库恢复脚本

#!/bin/bash
# prepare-db.sh — 恢复 11 个数据库 SQL 备份到 MySQL
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"

log_info "Phase 2: Database preparation"
log_info "Target: $MYSQL_HOST:$MYSQL_PORT (user: $MYSQL_USER)"

MYSQL_CMD="mysql -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p${MYSQL_PASS}"

# 检查 MySQL 连通性
if ! $MYSQL_CMD -e "SELECT 1" &>/dev/null; then
  log_error "Cannot connect to MySQL at $MYSQL_HOST:$MYSQL_PORT"
  exit 1
fi
log_ok "MySQL connection OK"

# 数据库名 → 备份文件映射
declare -A DB_MAP=(
  ["$DB_ELEVATOR"]="12_2026_04_23_17_28_33.sql.gz"
  ["$DB_ALARM"]="alarm_deploy_2026_04_23_17_28_33.sql.gz"
  ["$DB_MANAGER"]="cwos_manager_2026_04_23_17_28_33.sql.gz"
  ["$DB_PORTAL"]="cwos_portal_2026_04_23_17_28_33.sql.gz"
  ["$DB_COMMON"]="ninca_common_2026_04_23_17_28_33.sql.gz"
  ["$DB_COMPONENT_ORG"]="component-organization_2026_04_23_17_28_33.sql.gz"
  ["$DB_ODS"]="ods_2026_04_23_17_28_33.sql.gz"
  ["$DB_THIRDPARTY"]="cloudwalk_device_thirdparty_2026_04_23_17_28_33.sql.gz"
  ["$DB_G"]="g_2026_04_23_17_28_33.sql.gz"
  ["$DB_P"]="p_2026_04_23_17_28_33.sql.gz"
)

for db_name in "${!DB_MAP[@]}"; do
  backup_file="${DB_MAP[$db_name]}"
  backup_path="$DATA_BACKUP/$backup_file"

  if [[ ! -f "$backup_path" ]]; then
    log_warn "Backup not found: $backup_path — skipping $db_name"
    continue
  fi

  log_info "Restoring $db_name from $backup_file ($(du -h "$backup_path" | cut -f1))..."

  # 建库 (如不存在)
  $MYSQL_CMD -e "CREATE DATABASE IF NOT EXISTS \`$db_name\` DEFAULT CHARACTER SET utf8mb4;"

  # 导入
  zcat "$backup_path" | $MYSQL_CMD "$db_name" 2>&1 | tail -1

  if [[ ${PIPESTATUS[0]} -eq 0 ]]; then
    log_ok "  $db_name restored successfully"
  else
    log_error "  $db_name restore FAILED"
  fi
done

# 电梯应用还需要第二个备份 (34_*.sql.gz)
if [[ -f "$DATA_BACKUP/34_2026_04_23_17_28_33.sql.gz" ]]; then
  log_info "Restoring elevator DB partition 34..."
  zcat "$DATA_BACKUP/34_2026_04_23_17_28_33.sql.gz" | $MYSQL_CMD "$DB_ELEVATOR"
  log_ok "  $DB_ELEVATOR partition 34 restored"
fi

# 执行 V2.0.7 DDL (tenant_visitor_floor_policy)
log_info "Applying V2.0.7 DDL (tenant_visitor_floor_policy)..."
$MYSQL_CMD "$DB_ELEVATOR" < "$REPO_ROOT/docs/sql/tenant_visitor_floor_policy.sql"
log_ok "  V2 DDL applied"

log_info "Database preparation complete"
  • Step 2: 测试脚本 (dry-run: 仅检查连通性)
bash 源码/scripts/test-env/prepare-db.sh
# 预期: MySQL connection OK, 然后恢复各个库
  • Step 3: Commit
git add 源码/scripts/test-env/prepare-db.sh
git commit -m "feat: add database restoration script for 11 databases"

Task 4: 配置模板 + prepare-services.sh

Files:

  • Create: 源码/scripts/test-env/config/service-templates/elevator-v2.properties

  • Create: 源码/scripts/test-env/config/service-templates/crk-std.properties

  • Create: 源码/scripts/test-env/config/service-templates/alarm.properties

  • Create: 源码/scripts/test-env/config/service-templates/ninca-common.properties

  • Create: 源码/scripts/test-env/config/service-templates/component-org.properties

  • Create: 源码/scripts/test-env/prepare-services.sh

  • Step 1: 创建 V2 电梯配置模板

源码/scripts/test-env/config/service-templates/elevator-v2.properties:

# V2 elevator test config — auto-generated from template
# Variables: __MYSQL_HOST__ __MYSQL_PORT__ __REDIS_HOST__ __REDIS_PASS__
#             __CONSUL_HOST__ __CONSUL_PORT__ __KAFKA_HOST__ __KAFKA_PORT__
#             __CRK_HOST__ __CRK_PORT__
server.port=18081
spring.application.name=elevator-app
spring.profiles.active=access-control

# Consul
spring.cloud.consul.host=__CONSUL_HOST__
spring.cloud.consul.port=__CONSUL_PORT__
spring.cloud.consul.enabled=true
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.enabled=false

# MySQL (ShardingSphere)
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://__MYSQL_HOST__:__MYSQL_PORT__/cw-elevator-application?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456

# Redis
spring.redis.host=__REDIS_HOST__
spring.redis.port=6379
spring.redis.password=__REDIS_PASS__
spring.redis.database=5

# Kafka
cloudwalk.event.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
cloudwalk.event.group-id=cw-elevator-application-test

# Feign targets
feign.device.name=cwos-portal
feign.cwos-portal.name=cwos-portal
feign.ninca-crk-std.name=ninca-crk-std
ninca-crk-std.ribbon.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList
ninca-crk-std.ribbon.listOfServers=__CRK_HOST__:__CRK_PORT__
ninca-crk-std.ip=__CRK_HOST__:__CRK_PORT__

# Other
feign.hystrix.enable=true
feign.okhttp.enable=true
ribbon.ReadTimeout=10000
ribbon.ConnectTimeout=10000
elevator.application.key=xinghewan
elevator.application.time=600
elevator.application.keyA=5B7DEF88FF04
floor.building.id=605560539791228928
sendRecord.boolean=false
  • Step 2: 创建 CRK 后端配置模板

源码/scripts/test-env/config/service-templates/crk-std.properties:

# CRK-std test config
spring.application.name=ninca-crk-std
server.port=__CRK_PORT__
spring.profiles.active=smart-attendance,visitor-management,access-control,conference-attendance

# Consul
spring.cloud.consul.host=__CONSUL_HOST__
spring.cloud.consul.port=__CONSUL_PORT__
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.ip-address=127.0.0.1

# MySQL
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://__MYSQL_HOST__:__MYSQL_PORT__/ninca_crk_std?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456

# Redis
spring.redis.host=__REDIS_HOST__
spring.redis.port=6379
spring.redis.password=__REDIS_PASS__
spring.redis.database=5

# Kafka
kafka.producer.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
kafka.consumer.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
spring.kafka.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
spring.kafka.consumer.group-id=crk_std_test
cloudwalk.event.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
cloudwalk.event.group-id=crk_std

# Feign service names
feign.device.name=cwos-portal
feign.resource.name=cwos-portal
feign.cwos-portal.name=cwos-portal
feign.portal.name=cwos-portal
feign.component-organization.name=ninca-common-component-organization
feign.davinci-portal.name=cwos-portal
feign.ninca-common.name=ninca-common
feign.elevator.name=elevator-app

# Quartz
quartz.driver=com.mysql.jdbc.Driver
quartz.url=jdbc:mysql://__MYSQL_HOST__:__MYSQL_PORT__/ninca_crk_std?zeroDateTimeBehavior=convertToNull&useUnicode=TRUE&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
quartz.user=root
quartz.password=123456

# Management
management.port=__CRK_MGMT_PORT__
management.context-path=/actuator
management.security.enabled=false

xinhewan.businessid=2524639890ba4f2cba9ba1a4eeaa4015
push.method=1
sendRecord.boolean=false
  • Step 3: 创建 alarm 配置模板

源码/scripts/test-env/config/service-templates/alarm.properties:

# Alarm app test config
spring.application.name=ninca-qk-alarm-app
server.port=__ALARM_PORT__
spring.main.allow-bean-definition-overriding=true

# Consul
spring.cloud.consul.host=__CONSUL_HOST__
spring.cloud.consul.port=__CONSUL_PORT__
spring.cloud.consul.discovery.register=true
spring.cloud.consul.discovery.ip-address=127.0.0.1

# MySQL
spring.datasource.url=jdbc:mysql://__MYSQL_HOST__:__MYSQL_PORT__/alarm_deploy?useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

# Redis
spring.redis.host=__REDIS_HOST__
spring.redis.port=6379
spring.redis.password=__REDIS_PASS__
spring.redis.database=7

# Kafka
kafka.producer.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
kafka.consumer.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
spring.kafka.bootstrap-servers=__KAFKA_HOST__:__KAFKA_PORT__
spring.kafka.consumer.group-id=alarm_test

# Feign names
cloudwalk.alarm-app.feign.name.cwos-portal=cwos-portal
cloudwalk.alarm-app.feign.name.component-organization=ninca-common-component-organization
cloudwalk.alarm-app.feign.name.ninca-common=ninca-common

# Management
management.port=__ALARM_MGMT_PORT__
management.context-path=/actuator
management.security.enabled=false

# Disable non-essential features for test
swagger.enable=false
sendRecord.boolean=false
  • Step 4: 创建 ninca-common 和 component-org 配置模板

源码/scripts/test-env/config/service-templates/ninca-common.properties:

server.port=__NINCA_COMMON_PORT__
spring.application.name=ninca-common
spring.datasource.url=jdbc:mysql://__MYSQL_HOST__:__MYSQL_PORT__/ninca_common?useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.cloud.consul.host=__CONSUL_HOST__
spring.cloud.consul.port=__CONSUL_PORT__
spring.cloud.consul.discovery.register=true
spring.redis.host=__REDIS_HOST__
spring.redis.port=6379
spring.redis.password=__REDIS_PASS__

源码/scripts/test-env/config/service-templates/component-org.properties:

server.port=__COMPONENT_ORG_PORT__
spring.application.name=ninca-common-component-organization
spring.datasource.url=jdbc:mysql://__MYSQL_HOST__:__MYSQL_PORT__/component-organization?useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.cloud.consul.host=__CONSUL_HOST__
spring.cloud.consul.port=__CONSUL_PORT__
spring.cloud.consul.discovery.register=true
spring.redis.host=__REDIS_HOST__
spring.redis.port=6379
spring.redis.password=__REDIS_PASS__
  • Step 5: 编写 prepare-services.sh (解压 + 配置注入)
#!/bin/bash
# prepare-services.sh — 解压 tar.gz + 从模板生成配置
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"

log_info "Phase 4: Service preparation"
mkdir -p "$SERVICE_DIR" "$LOG_DIR"

# 需要解压的 tar.gz 列表
TARBALLS=(
  "$STAR_CENTER/ninca_common_01-ninca_common_backend.tar.gz:ninca-common"
  "$STAR_CENTER/ninca_common_component_organization_01-ninca_common_component_organization.tar.gz:component-org"
  "$STAR_CENTER/ninca_common_snap_app_01-ninca_common_snap_app.tar.gz:snap-app"
  "$STAR_CENTER/ninca_common_vehicle_app_01-ninca_common_vehicle_app.tar.gz:vehicle-app"
  "$STAR_CENTER/ninca_common_monitor_app_01-ninca_common_monitor_app.tar.gz:monitor-app"
  "$STAR_CENTER/ninca-person-file-app-V2.9.2_20210216.tar.gz:person-file"
)

for item in "${TARBALLS[@]}"; do
  tarball="${item%%:*}"
  svc_name="${item##*:}"
  svc_dir="$SERVICE_DIR/$svc_name"

  if [[ -d "$svc_dir" ]]; then
    log_info "Already extracted: $svc_name — skipping"
    continue
  fi

  log_info "Extracting $tarball$svc_dir ..."
  mkdir -p "$svc_dir"
  tar -xzf "$tarball" -C "$svc_dir" --strip-components=1
  log_ok "  $svc_name extracted"
done

# 配置模板变量替换函数
render_template() {
  local template="$1"
  local output="$2"
  sed \
    -e "s|__MYSQL_HOST__|$MYSQL_HOST|g" \
    -e "s|__MYSQL_PORT__|$MYSQL_PORT|g" \
    -e "s|__REDIS_HOST__|$REDIS_HOST|g" \
    -e "s|__REDIS_PASS__|$REDIS_PASS|g" \
    -e "s|__CONSUL_HOST__|$CONSUL_HOST|g" \
    -e "s|__CONSUL_PORT__|$CONSUL_PORT|g" \
    -e "s|__KAFKA_HOST__|$KAFKA_HOST|g" \
    -e "s|__KAFKA_PORT__|$KAFKA_PORT|g" \
    -e "s|__CRK_HOST__|127.0.0.1|g" \
    -e "s|__CRK_PORT__|$PORT_CRK_STD|g" \
    -e "s|__CRK_MGMT_PORT__|$PORT_CRK_MGMT|g" \
    -e "s|__ALARM_PORT__|$PORT_ALARM|g" \
    -e "s|__ALARM_MGMT_PORT__|$PORT_ALARM_MGMT|g" \
    -e "s|__NINCA_COMMON_PORT__|$PORT_NINCA_COMMON|g" \
    -e "s|__COMPONENT_ORG_PORT__|$PORT_COMPONENT_ORG|g" \
    "$template" > "$output"
}

# 为各服务生成配置文件
render_template "$TEST_ENV_DIR/config/service-templates/elevator-v2.properties" \
  "$REPO_ROOT/maven-cw-elevator-application/deploy/v2-maven/application-test.properties"
log_ok "  elevator-v2 config generated"

render_template "$TEST_ENV_DIR/config/service-templates/crk-std.properties" \
  "$STAR_CENTER/ninca_crk_std_01-ninca_crk_std_backend/application-test.properties"
log_ok "  crk-std config generated"

render_template "$TEST_ENV_DIR/config/service-templates/alarm.properties" \
  "$STAR_CENTER/ninca_qk_alarm_app_01-ninca_qk_alarm_app/application-test.properties"
log_ok "  alarm config generated"

# 为解压的 tarball 服务生成配置
if [[ -d "$SERVICE_DIR/ninca-common" ]]; then
  render_template "$TEST_ENV_DIR/config/service-templates/ninca-common.properties" \
    "$SERVICE_DIR/ninca-common/application-test.properties"
  log_ok "  ninca-common config generated"
fi

if [[ -d "$SERVICE_DIR/component-org" ]]; then
  render_template "$TEST_ENV_DIR/config/service-templates/component-org.properties" \
    "$SERVICE_DIR/component-org/application-test.properties"
  log_ok "  component-org config generated"
fi

log_info "Service preparation complete"
  • Step 6: 测试 prepare-services.sh
bash 源码/scripts/test-env/prepare-services.sh
# 预期: 解压 6 个 tar.gz, 生成 5 个配置文件
  • Step 7: Commit
git add 源码/scripts/test-env/config/service-templates/ 源码/scripts/test-env/prepare-services.sh
git commit -m "feat: add service config templates and extraction script"

Task 5: build-elevator-v2.sh — 编译 V2 电梯应用

Files:

  • Create: 源码/scripts/test-env/build-elevator-v2.sh

  • Step 1: 编写 V2 编译脚本

#!/bin/bash
# build-elevator-v2.sh — 编译 cw-elevator-application V2
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"

log_info "Building V2 elevator application..."

cd "$REPO_ROOT/maven-cw-elevator-application"

export JAVA_HOME
export PATH="$JAVA_HOME/bin:$PATH"

log_info "JDK: $($JAVA -version 2>&1 | head -1)"

# 编译 (跳过测试)
$MVN clean install $MVN_OPTS 2>&1 | tail -5

if [[ ${PIPESTATUS[0]} -eq 0 ]]; then
  log_ok "V2 elevator build SUCCESS"
else
  log_error "V2 elevator build FAILED — check logs"
  exit 1
fi

# 同步 JAR 到 deploy/
if [[ -f "cw-elevator-application-starter/target/cw-elevator-application-2.0.7.jar" ]]; then
  cp cw-elevator-application-starter/target/cw-elevator-application-2.0.7.jar \
     deploy/v2-maven/cw-elevator-application-2.0.7.jar
  log_ok "JAR synced to deploy/v2-maven/"
else
  # Fallback: 用脚本同步
  cd deploy && bash sync-jars.sh
  log_ok "JAR synced via sync-jars.sh"
fi
  • Step 2: 测试编译
bash 源码/scripts/test-env/build-elevator-v2.sh
# 预期: BUILD SUCCESS, JAR synced
  • Step 3: Commit
git add 源码/scripts/test-env/build-elevator-v2.sh
git commit -m "feat: add V2 elevator build script"

Task 6: start-all.sh / stop-all.sh — 服务启停

Files:

  • Create: 源码/scripts/test-env/start-all.sh

  • Create: 源码/scripts/test-env/stop-all.sh

  • Step 1: 编写 start-all.sh

#!/bin/bash
# start-all.sh — 按拓扑序启动所有 Java 服务
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"
export JAVA_HOME
export PATH="$JAVA_HOME/bin:$PATH"

log_info "Phase 5: Starting all services..."
mkdir -p "$LOG_DIR"

# 启动函数: start_service <name> <jar_path> <port> <java_opts> <extra_args>
start_service() {
  local name="$1"; local jar="$2"; local port="$3"
  local opts="${4:-$JAVA_OPTS_LIGHT}"; shift 4

  log_info "Starting $name (port $port)..."

  if [[ ! -f "$jar" ]]; then
    log_error "  JAR not found: $jar"
    return 1
  fi

  nohup $JAVA -jar $opts "$jar" "$@" \
    --spring.config.location="$(dirname "$jar")/" \
    > "$LOG_DIR/${name}.log" 2>&1 &

  local pid=$!
  echo $pid > "$LOG_DIR/${name}.pid"

  # 等待服务就绪 (最多 60s)
  for i in $(seq 1 30); do
    sleep 2
    if curl -sf "http://127.0.0.1:$port/actuator/health" &>/dev/null; then
      log_ok "  $name (pid=$pid, port=$port) STARTED"
      return 0
    fi
  done

  log_warn "  $name health check timeout after 60s (pid=$pid)"
  return 1
}

# ============================================
# 启动顺序 (拓扑序)
# ============================================

# A1: ninca-common
COMMON_JAR=$(find "$SERVICE_DIR/ninca-common" -name "*.jar" -not -name "*-sources*" | head -1)
if [[ -n "$COMMON_JAR" ]]; then
  start_service "ninca-common" "$COMMON_JAR" "$PORT_NINCA_COMMON" \
    "$JAVA_OPTS_LIGHT" \
    --spring.config.additional-location="$TEST_ENV_DIR/config/service-templates/ninca-common.properties"
fi

# A2: component-organization
ORG_JAR=$(find "$SERVICE_DIR/component-org" -name "*.jar" -not -name "*-sources*" | head -1)
if [[ -n "$ORG_JAR" ]]; then
  start_service "component-org" "$ORG_JAR" "$PORT_COMPONENT_ORG" \
    "$JAVA_OPTS_LIGHT" \
    --spring.config.additional-location="$TEST_ENV_DIR/config/service-templates/component-org.properties"
fi

# A10: CRK-std
CRK_DIR="$STAR_CENTER/ninca_crk_std_01-ninca_crk_std_backend/ninca-crk-std-backend-V2.9.2_20210730"
CRK_JAR="$STAR_CENTER/ninca_crk_std_01-ninca_crk_std_backend/ninca-crk-std-backend-V2.9.2_20210730.jar"
if [[ -f "$CRK_JAR" ]]; then
  start_service "crk-std" "$CRK_JAR" "$PORT_CRK_STD" \
    "$JAVA_OPTS_HEAVY" \
    --spring.config.location="$CRK_DIR/"
fi

# A11: alarm-app
ALARM_JAR="$STAR_CENTER/ninca_qk_alarm_app_01-ninca_qk_alarm_app/ninca-qk-alarm-app-V2.9.2_20210730.jar"
if [[ -f "$ALARM_JAR" ]]; then
  start_service "alarm-app" "$ALARM_JAR" "$PORT_ALARM" \
    "$JAVA_OPTS_HEAVY" \
    --spring.config.location="$STAR_CENTER/ninca_qk_alarm_app_01-ninca_qk_alarm_app/"
fi

# A12: elevator V2 (最后启动,依赖 CRK+portal+org)
ELEVATOR_V2_JAR="$REPO_ROOT/maven-cw-elevator-application/deploy/v2-maven/cw-elevator-application-2.0.7.jar"
if [[ -f "$ELEVATOR_V2_JAR" ]]; then
  start_service "elevator-v2" "$ELEVATOR_V2_JAR" "$PORT_ELEVATOR_V2" \
    "$JAVA_OPTS_HEAVY" \
    --spring.config.location="$REPO_ROOT/maven-cw-elevator-application/deploy/v2-maven/"
fi

# A13: elevator V1 (对拍对照)
ELEVATOR_V1_JAR="$REPO_ROOT/maven-cw-elevator-application/deploy/v1-legacy/cw-elevator-application-V1.0.0.20211103.jar"
if [[ -f "$ELEVATOR_V1_JAR" ]]; then
  start_service "elevator-v1" "$ELEVATOR_V1_JAR" "$PORT_ELEVATOR_V1" \
    "$JAVA_OPTS_HEAVY" \
    --spring.config.location="$REPO_ROOT/maven-cw-elevator-application/deploy/v1-legacy/"
fi

log_info "All services started"
  • Step 2: 编写 stop-all.sh
#!/bin/bash
# stop-all.sh — 按逆序停止所有服务
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"

log_info "Stopping all services..."

for pid_file in "$LOG_DIR"/*.pid; do
  if [[ -f "$pid_file" ]]; then
    pid=$(cat "$pid_file")
    svc=$(basename "$pid_file" .pid)
    if kill -0 "$pid" 2>/dev/null; then
      log_info "Stopping $svc (pid=$pid)..."
      kill "$pid"
      sleep 2
      kill -9 "$pid" 2>/dev/null || true
      log_ok "  $svc stopped"
    fi
    rm -f "$pid_file"
  fi
done

log_info "All services stopped"
  • Step 3: 测试启动/停止流程
# 先确保 Docker infra 已启动
# 然后:
bash 源码/scripts/test-env/start-all.sh
sleep 30
bash 源码/scripts/test-env/stop-all.sh
  • Step 4: Commit
git add 源码/scripts/test-env/start-all.sh 源码/scripts/test-env/stop-all.sh
git commit -m "feat: add service start/stop orchestration scripts"

Task 7: health-check.sh — 探活检查

Files:

  • Create: 源码/scripts/test-env/health-check.sh

  • Step 1: 编写探活脚本

#!/bin/bash
# health-check.sh — 全组件探活检查
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"

log_info "=== Health Check ==="
FAILED=0
PASSED=0

check_http() {
  local name="$1"; local url="$2"
  if curl -sf --max-time 5 "$url" &>/dev/null; then
    log_ok "  $name ($url)"
    ((PASSED++))
  else
    log_error "  $name ($url) FAILED"
    ((FAILED++))
  fi
}

check_tcp() {
  local name="$1"; local host="$2"; local port="$3"
  if nc -z -w3 "$host" "$port" &>/dev/null; then
    log_ok "  $name ($host:$port)"
    ((PASSED++))
  else
    log_error "  $name ($host:$port) FAILED"
    ((FAILED++))
  fi
}

log_info "--- Infrastructure ---"
check_http "Consul" "http://$CONSUL_HOST:$CONSUL_PORT/v1/status/leader"
check_tcp  "Redis"  "$REDIS_HOST" "$REDIS_PORT"
check_tcp  "Kafka"  "$KAFKA_HOST" "$KAFKA_PORT"
check_http "Nginx"  "http://$CONSUL_HOST:$PORT_NGINX"

log_info "--- Application Services ---"
check_http "elevator-v2" "http://127.0.0.1:$PORT_ELEVATOR_V2/actuator/health"
check_http "elevator-v1" "http://127.0.0.1:$PORT_ELEVATOR_V1/actuator/health"
check_http "crk-std"    "http://127.0.0.1:$PORT_CRK_MGMT/actuator/health"
check_http "alarm-app"  "http://127.0.0.1:$PORT_ALARM_MGMT/actuator/health"

log_info "--- Databases ---"
check_tcp "MySQL" "$MYSQL_HOST" "$MYSQL_PORT"

log_info "--- Consul Registration ---"
CONSUL_SVCS=$(curl -sf "http://$CONSUL_HOST:$CONSUL_PORT/v1/agent/services" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0")
log_info "  Registered services: $CONSUL_SVCS"

echo ""
log_info "=== Result: $PASSED passed, $FAILED failed ==="

[[ $FAILED -eq 0 ]] && exit 0 || exit 1
  • Step 2: 测试探活
bash 源码/scripts/test-env/health-check.sh
  • Step 3: Commit
git add 源码/scripts/test-env/health-check.sh
git commit -m "feat: add health-check script for all components"

Task 8: verify-functional.sh — 功能验证

Files:

  • Create: 源码/scripts/test-env/verify-functional.sh

  • Step 1: 编写功能验证脚本

#!/bin/bash
# verify-functional.sh — V2 全系统功能验证
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"

log_info "=== V2 Functional Verification ==="
FAILED=0

verify() {
  local desc="$1"; shift
  if "$@"; then
    log_ok "  $desc"
  else
    log_error "  $desc FAILED"
    ((FAILED++))
  fi
}

# F1: 电梯 V2 探活
log_info "[F1] Elevator V2 health"
verify "elevator-v2 /actuator/health" \
  curl -sf "http://127.0.0.1:$PORT_ELEVATOR_V2/actuator/health"

# F2: V1 vs V2 API 对拍
log_info "[F2] V1/V2 API parity test"
cd "$REPO_ROOT/maven-cw-elevator-application/tools/elevator_api_parity"
verify "pytest elevator_api_parity" \
  pytest tests/test_smoke_catalog.py -q --tb=short

# F3: V2.0.7 租户策略 — UC-01 基线 (无策略表 → floorList 全集)
log_info "[F3] Tenant visitor policy — UC-01 baseline"
# 注意: 需要先插入策略表测试数据; 基线测试确认无策略时行为不变
RESP=$(curl -sf -X POST "http://127.0.0.1:$PORT_ELEVATOR_V2/elevator/person/add/visitor" \
  -H "Content-Type: application/json" \
  -d '{"personId":"test-person","visitorName":"test-visitor"}' 2>/dev/null || echo '{"code":-1}')
verify "UC-01 baseline (no policy)" echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d.get('code')!=76260532"

# F4: V2.0.7 — UC-02 显式传 floorIds
log_info "[F4] Tenant visitor policy — UC-02 explicit floorIds"
RESP2=$(curl -sf -X POST "http://127.0.0.1:$PORT_ELEVATOR_V2/elevator/person/add/visitor" \
  -H "Content-Type: application/json" \
  -d '{"personId":"test-person","visitorName":"test-visitor2","floorIds":["zone-001"]}' 2>/dev/null || echo '{"code":-1}')
verify "UC-02 (explicit floorIds)" echo "$RESP2" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('code'))"

# F7: CRK 联动
log_info "[F7] CRK integration"
verify "crk-std /actuator/health" \
  curl -sf "http://127.0.0.1:$PORT_CRK_MGMT/actuator/health"

# F8: 报警 Kafka 消费
log_info "[F8] Alarm Kafka — Consul registration check"
verify "alarm registered in Consul" \
  curl -sf "http://$CONSUL_HOST:$CONSUL_PORT/v1/agent/services" | python3 -c "import sys,json; services=json.load(sys.stdin); assert any('alarm' in k.lower() for k in services)"

# F9: Nginx 前端代理
log_info "[F9] Frontend Nginx"
verify "nginx serves cwos-portal" \
  curl -sf -o /dev/null -w "%{http_code}" "http://$CONSUL_HOST:$PORT_NGINX/" | grep -q 200

# I1: MySQL 连通
log_info "[I1] MySQL connectivity"
verify "mysql SELECT 1" \
  mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" -e "SELECT 1" &>/dev/null

echo ""
log_info "=== Verification Complete: $FAILED failures ==="
[[ $FAILED -eq 0 ]] && exit 0 || exit 1
  • Step 2: 测试验证脚本 (需要服务已运行)
bash 源码/scripts/test-env/verify-functional.sh
  • Step 3: Commit
git add 源码/scripts/test-env/verify-functional.sh
git commit -m "feat: add functional verification script (API parity + tenant policy + integration)"

Task 9: setup.sh — 主入口一键搭建

Files:

  • Create: 源码/scripts/test-env/setup.sh

  • Step 1: 编写主入口脚本

#!/bin/bash
# setup.sh — V2 测试环境一键搭建入口
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config/env.sh"

cat << 'BANNER'
╔══════════════════════════════════════════════╗
║   V2 Full-System Test Environment Setup      ║
║   cw-elevator-application v2.0.7             ║
╚══════════════════════════════════════════════╝
BANNER

START_TIME=$(date +%s)

# ============================================
# Phase 1: 环境检查
# ============================================
log_info "Phase 1/6: Environment check"

# JDK 8
if [[ ! -x "$JAVA_HOME/bin/java" ]]; then
  log_error "JDK 8 not found at $JAVA_HOME"
  log_info "Set DEPLOY_JDK8 environment variable to correct path"
  exit 1
fi
JAVA_VER=$("$JAVA_HOME/bin/java" -version 2>&1 | head -1)
log_ok "JDK: $JAVA_VER"

# Maven
if ! command -v mvn &>/dev/null; then
  log_error "Maven not found"
  exit 1
fi
log_ok "Maven: $(mvn --version 2>&1 | head -1)"

# Docker
if ! docker compose version &>/dev/null; then
  log_error "Docker Compose not found"
  exit 1
fi
log_ok "Docker: $(docker compose version)"

# MySQL
if ! mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PASS" -e "SELECT 1" &>/dev/null; then
  log_error "Cannot connect to MySQL at $MYSQL_HOST:$MYSQL_PORT"
  exit 1
fi
log_ok "MySQL: $MYSQL_HOST:$MYSQL_PORT OK"

# 端口冲突
CONFLICT_PORTS=""
for port in 8500 6379 9092 2181 8090 18080 18081 16106 17011 3721; do
  if ss -tlnp 2>/dev/null | grep -q ":$port "; then
    CONFLICT_PORTS="$CONFLICT_PORTS $port"
  fi
done
if [[ -n "$CONFLICT_PORTS" ]]; then
  log_warn "Ports already in use:$CONFLICT_PORTS"
  log_warn "These services may fail to start. Stop existing processes first."
fi

log_ok "Phase 1 complete"

# ============================================
# Phase 2: 数据库准备
# ============================================
log_info "Phase 2/6: Database preparation"
bash "$SCRIPT_DIR/prepare-db.sh"
log_ok "Phase 2 complete"

# ============================================
# Phase 3: Docker 基础组件启动
# ============================================
log_info "Phase 3/6: Docker infrastructure"
cd "$SCRIPT_DIR"
docker compose -f docker-compose.infra.yml up -d

log_info "Waiting for Consul..."
for i in $(seq 1 30); do
  if curl -sf "http://$CONSUL_HOST:$CONSUL_PORT/v1/status/leader" &>/dev/null; then
    break
  fi
  sleep 2
done
log_ok "Consul ready"

log_info "Waiting for Kafka..."
sleep 15  # Kafka 启动较慢
log_ok "Kafka ready"
log_ok "Phase 3 complete"

# ============================================
# Phase 4: 服务准备
# ============================================
log_info "Phase 4/6: Service preparation"
bash "$SCRIPT_DIR/prepare-services.sh"
bash "$SCRIPT_DIR/build-elevator-v2.sh"
log_ok "Phase 4 complete"

# ============================================
# Phase 5: 服务启动
# ============================================
log_info "Phase 5/6: Service startup"
bash "$SCRIPT_DIR/start-all.sh"
log_ok "Phase 5 complete"

# ============================================
# Phase 6: 验证
# ============================================
log_info "Phase 6/6: Verification"
bash "$SCRIPT_DIR/health-check.sh"
bash "$SCRIPT_DIR/verify-functional.sh"
log_ok "Phase 6 complete"

# ============================================
# 汇总
# ============================================
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

cat << SUMMARY

╔══════════════════════════════════════════════╗
║   Setup Complete!                            ║
║                                              ║
║   Duration: ${DURATION}s                               ║
║   Consul:  http://$CONSUL_HOST:$CONSUL_PORT/ui/       ║
║   Nginx:   http://$CONSUL_HOST:$PORT_NGINX/           ║
║   Elevator V2: http://127.0.0.1:$PORT_ELEVATOR_V2     ║
║   Elevator V1: http://127.0.0.1:$PORT_ELEVATOR_V1     ║
║   CRK std:     http://127.0.0.1:$PORT_CRK_STD         ║
║   Alarm:       http://127.0.0.1:$PORT_ALARM           ║
╚══════════════════════════════════════════════╝

To stop: bash scripts/test-env/stop-all.sh && docker compose -f scripts/test-env/docker-compose.infra.yml down

SUMMARY
  • Step 2: 端到端测试
bash 源码/scripts/test-env/setup.sh
# 预期: 所有 6 个 Phase 完成, 显示 Setup Complete!
  • Step 3: Commit
git add 源码/scripts/test-env/setup.sh
git commit -m "feat: add one-click test environment setup script (setup.sh)"

Task 10: 集成测试 — 完整搭建验证

Files:

  • (无新文件,验证所有脚本正确协作)

  • Step 1: 清理环境

# 停止所有之前的进程
bash 源码/scripts/test-env/stop-all.sh 2>/dev/null || true
docker compose -f 源码/scripts/test-env/docker-compose.infra.yml down -v 2>/dev/null || true
  • Step 2: 执行一键搭建
bash 源码/scripts/test-env/setup.sh 2>&1 | tee /tmp/v2test-setup.log
  • Step 3: 验证日志
# 检查关键日志标记
grep -c "ERROR" /tmp/v2test-setup.log  # 预期: 0
grep "Setup Complete" /tmp/v2test-setup.log  # 预期: 找到
grep "Duration:" /tmp/v2test-setup.log  # 预期: 找到耗时
  • Step 4: 验证关键端口
bash 源码/scripts/test-env/health-check.sh
# 预期: 全部 PASSED, FAILED=0
  • Step 5: 验证功能
bash 源码/scripts/test-env/verify-functional.sh
# 预期: 全部通过, 0 failures
  • Step 6: 清理
bash 源码/scripts/test-env/stop-all.sh
docker compose -f 源码/scripts/test-env/docker-compose.infra.yml down
  • Step 7: Commit (如无代码变更则跳过)

实施顺序依赖

Task 1 (env.sh)              ← 所有后续 Task 的基础
    ↓
Task 2 (docker-compose)      ← 可并行
    ↓
Task 3 (prepare-db.sh)       ← 依赖 MySQL 地址 (env.sh)
Task 4 (templates + extract) ← 依赖 env.sh
    ↓
Task 5 (build V2)            ← 依赖 env.sh
    ↓
Task 6 (start-all/stop-all)  ← 依赖 Task 2,3,4,5
Task 7 (health-check)        ← 独立
Task 8 (verify-functional)   ← 独立 (但运行需服务已启动)
    ↓
Task 9 (setup.sh)            ← 依赖 Task 2-8
    ↓
Task 10 (integration test)   ← 依赖 Task 9

可并行执行: Task 3, 4, 5 (均只依赖 Task 1)Task 6, 7, 8 (均只依赖 Task 1-5)