# V2 完整环境部署 + V1/V2 对比测试 — 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax. **Goal:** 搭建 V2 电梯功能测试所需的最小服务集(infra + 3 Java 服务),运行 V1/V2 API 对拍及策略差异验证。 **Architecture:** Docker 提供 Consul/Redis/Nginx,复用 MySQL 192.168.3.12:3307,按 ninca-common → component-org → elevator V2 → elevator V1 顺序启动,最终执行 pytest 对拍 + curl 策略测试。 **Tech Stack:** Bash, Docker Compose v2, JDK 8, Maven 3.9, Python 3.10 + pytest, MySQL 5.7 **关联 Spec:** `docs/superpowers/specs/2026-05-01-v2-test-env-setup-design.md` --- ## 前置状态 ```bash MySQL: 192.168.3.12:3307 root/123456 ✅ Docker: Compose v2.40.3 可用 ✅ JDK 8: /usr/lib/jvm/java-8-openjdk-amd64 ✅ 部署包: 13 个 tar.gz, 7.2 GB ✅ DB 数据: 11 个库已恢复, 策略表已创建 ✅ ``` --- ### Task 1: 启动基础设施 (Docker Compose) **Files:** - Use: `scripts/test-env/docker-compose.infra.yml` - Use: `scripts/test-env/config/env.sh` - [ ] **Step 1: 清理旧容器并启动** ```bash source scripts/test-env/config/env.sh cd scripts/test-env docker rm -f v2test-consul v2test-redis v2test-nginx 2>/dev/null || true docker compose -f docker-compose.infra.yml down --remove-orphans 2>/dev/null || true docker compose -f docker-compose.infra.yml up -d sleep 5 curl -sf http://127.0.0.1:9517/v1/status/leader && echo "Consul UP" || echo "FAIL" redis-cli -p 6380 -a '1qaz!QAZ' PING && echo "Redis UP" || echo "FAIL" ``` - [ ] **Step 2: 验证 MySQL** ```bash mysql -h 192.168.3.12 -P 3307 -u root -p123456 -e "SELECT 1" && echo "MySQL UP" ``` --- ### Task 2: 启动 ninca-common (port 33010) **Files:** - Create: `scripts/test-env/config/ninca-common-override.properties` - Use: `services/ninca-common/ninca_common_01-ninca_common_backend/ninca-common-backend-V2.9.2_20210730.jar` **关键**: ninca-common 用 ShardingSphere,需覆盖 `spring.shardingsphere.datasource.ds0.jdbc-url` - [ ] **Step 1: 创建 ShardingSphere 覆盖配置** ```bash source scripts/test-env/config/env.sh cat > $TEST_ENV_DIR/config/ninca-common-override.properties << EOF spring.shardingsphere.datasource.names=ds0 spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://$MYSQL_HOST:$MYSQL_PORT/ninca_common?useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai spring.shardingsphere.datasource.ds0.username=$MYSQL_USER spring.shardingsphere.datasource.ds0.password=$MYSQL_PASS spring.shardingsphere.sharding.default-data-source-name=ds0 EOF ``` - [ ] **Step 2: 启动** ```bash NC_JAR="$SERVICE_DIR/ninca-common/ninca_common_01-ninca_common_backend/ninca-common-backend-V2.9.2_20210730.jar" nohup /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar -Xmx1024m "$NC_JAR" \ --server.port=33010 \ --spring.config.additional-location="$TEST_ENV_DIR/config/ninca-common-override.properties" \ --spring.cloud.consul.host=$CONSUL_HOST --spring.cloud.consul.port=$CONSUL_PORT \ --spring.redis.host=$REDIS_HOST --spring.redis.port=$REDIS_PORT --spring.redis.password=$REDIS_PASS \ &> /tmp/ninca-common-plan.log & echo "PID=$!" sleep 30 curl -sf http://127.0.0.1:33010/health && echo " ✅" || echo " ❌" ``` - [ ] **Step 3: 如启动失败 → Python stub** ```bash # 若 ninca-common 无法启动,创建 stub 模拟 person 属性查询 # 组件 org 的 person/detail 内部调 ninca-common,可用空响应 stub 绕过 ``` --- ### Task 3: 启动 component-organization (port 33011) **Files:** - Use: `services/component-org/.../ninca-common-component-organization-V2.9.2_20210730.jar` - [ ] **Step 1: 确保 Quartz 表存在** ```bash mysql -h 192.168.3.12 -P 3307 -u root -p123456 -e " CREATE TABLE IF NOT EXISTS \`component-organization\`.QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME, LOCK_NAME) ) ENGINE=InnoDB;" 2>/dev/null ``` - [ ] **Step 2: 启动 (带 ninca-common Ribbon 路由)** ```bash COMP_JAR="$SERVICE_DIR/component-org/ninca_common_component_organization_01-ninca_common_component_organization/ninca-common-component-organization-V2.9.2_20210730.jar" nohup /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dlogging.config=/tmp/logback-comp-org.xml -jar -Xmx1024m "$COMP_JAR" \ --server.port=33011 \ --spring.datasource.url="jdbc:mysql://$MYSQL_HOST:$MYSQL_PORT/component-organization?useSSL=false&characterEncoding=utf-8" \ --spring.datasource.username=$MYSQL_USER --spring.datasource.password=$MYSQL_PASS \ --spring.cloud.consul.host=$CONSUL_HOST --spring.cloud.consul.port=$CONSUL_PORT \ --spring.redis.host=$REDIS_HOST --spring.redis.port=$REDIS_PORT --spring.redis.password=$REDIS_PASS \ --ninca-common.ribbon.listOfServers=127.0.0.1:33010 \ &> /tmp/comp-org-plan.log & echo "PID=$!" sleep 30 curl -sf http://127.0.0.1:17116/actuator/health | python3 -c "import sys,json;print(json.load(sys.stdin)['status'])" && echo " ✅" ``` - [ ] **Step 3: 验证 person/detail** ```bash curl -sf -X POST http://127.0.0.1:33011/component/person/detail \ -H "Content-Type: application/json" \ -d '{"id":"1072908835884208128"}' | python3 -c "import sys,json;d=json.load(sys.stdin);print(f'code={d[\"code\"]}')" # 预期: code=00000000 (非 53014011) ``` - [ ] **Step 4: 如果 still 53014011** 放弃启动 ninca-common,改为 Python stub 方案: - 创建 `scripts/test-env/stub-person-service.py` - 监听 33010,响应 `/health` 和任意 person 查询 - 重启 comp-org(它只需 ninca-common 可达,不关心响应内容) --- ### Task 4: 启动 elevator V2 (port 18081) **Files:** - Use: `maven-cw-elevator-application/deploy/v2-maven/cw-elevator-application-2.0.9.jar` - [ ] **Step 1: 重启 V2 带全量 Ribbon 路由** ```bash pkill -f "elevator.*18081" 2>/dev/null; sleep 2 source scripts/test-env/config/env.sh DEPLOY_DIR="$REPO_ROOT/maven-cw-elevator-application/deploy/v2-maven" V2_JAR=$(ls -t "$DEPLOY_DIR"/cw-elevator-application-*.jar | head -1) nohup /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar -Xmx2048m "$V2_JAR" \ --server.port=18081 --spring.redis.port=6380 --spring.redis.password='1qaz!QAZ' \ --spring.cloud.consul.host=127.0.0.1 --spring.cloud.consul.port=9517 \ --ninca-common-component-organization.ribbon.listOfServers=127.0.0.1:33011 \ --spring.config.location="$DEPLOY_DIR/" \ &> /tmp/v2-plan.log & echo "PID=$!" sleep 35 curl -sf http://127.0.0.1:18081/health && echo " ✅" ``` --- ### Task 5: 启动 elevator V1 (port 18080) **Files:** - Use: `maven-cw-elevator-application/deploy/v1-legacy/cw-elevator-application-V1.0.0.20211103.jar` - [ ] **Step 1: 启动 V1** ```bash pkill -f "elevator.*18080" 2>/dev/null; sleep 2 source scripts/test-env/config/env.sh V1_DIR="$REPO_ROOT/maven-cw-elevator-application/deploy/v1-legacy" V1_JAR=$(ls -t "$V1_DIR"/cw-elevator-application-*.jar | head -1) nohup /usr/lib/jvm/java-8-openjdk-amd64/bin/java -jar -Xmx2048m "$V1_JAR" \ --server.port=18080 --spring.redis.port=6380 --spring.redis.password='1qaz!QAZ' \ --spring.cloud.consul.host=127.0.0.1 --spring.cloud.consul.port=9517 \ --ninca-common-component-organization.ribbon.listOfServers=127.0.0.1:33011 \ --spring.config.location="$V1_DIR/" \ &> /tmp/v1-plan.log & echo "PID=$!" sleep 35 curl -sf http://127.0.0.1:18080/health && echo " ✅" ``` --- ### Task 6: 对拍测试 **Files:** - Use: `maven-cw-elevator-application/tools/elevator_api_parity/` - [ ] **Step 1: 运行全量对拍** ```bash cd maven-cw-elevator-application/tools/elevator_api_parity ELEVATOR_BASE_OLD=http://127.0.0.1:18080 ELEVATOR_BASE_NEW=http://127.0.0.1:18081 \ python3 -m pytest tests/ -v --tb=short -p no:allure_pytest # 预期: 8/8 passed ``` --- ### Task 7: 策略差异验证 **Files:** - DB: `tenant_visitor_floor_policy` (广发基金 org_id=488b8ad049bb43408a6fbcc50bcb89ac) - 人员: `1072908835884208128` (秦夏) - [ ] **Step 1: V1 add/visitor (无策略 → floorList 全集)** ```bash curl -s -X POST http://127.0.0.1:18080/elevator/person/add/visitor \ -H "Content-Type: application/json" \ -d '{"personId":"1072908835884208128","businessId":"2524639890ba4f2cba9ba1a4eeaa4015","visitorName":"test","phone":"13800000000"}' # 预期: code != 76260532, 使用 floorList 全集 ``` - [ ] **Step 2: V2 add/visitor (策略 → allow_zone_ids 替换)** ```bash curl -s -X POST http://127.0.0.1:18081/elevator/person/add/visitor \ -H "Content-Type: application/json" \ -d '{"personId":"1072908835884208128","businessId":"2524639890ba4f2cba9ba1a4eeaa4015","visitorName":"test","phone":"13800000000"}' # 预期: 策略生效 → 返回 allow_zone_ids 交集 或 floorList 全集 (V1 无策略) ``` - [ ] **Step 3: 差异判定** ```bash # V1_CODE != V2_CODE → STRATEGY DIVERGENCE CONFIRMED # V1_CODE == V2_CODE → policy not triggered (check comp-org person/detail) ``` --- ### Task 8: 失败回退 — Python stub 方案 **如果 Task 2-3 的 Java 服务无法启动:** **Files:** - Create: `scripts/test-env/stub-person-service.py` - [ ] **Step 1: 创建 stub (模拟 ninca-common + comp-org)** ```python #!/usr/bin/env python3 """Stub: 模拟 component-organization person/detail,返回广发基金秦夏数据""" from http.server import HTTPServer, BaseHTTPRequestHandler import json class PersonStub(BaseHTTPRequestHandler): def do_POST(self): body_len = int(self.headers.get('Content-Length', 0)) body = json.loads(self.rfile.read(body_len)) if body_len > 0 else {} person_id = body.get('id', body.get('personId', 'unknown')) if self.path in ('/health', '/actuator/health'): self._json(200, {"status": "UP"}) elif '/person/detail' in self.path or '/component/person/detail' in self.path: self._json(200, { "code": "00000000", "success": True, "message": "success", "data": { "id": person_id, "name": "秦夏", "businessId": "2524639890ba4f2cba9ba1a4eeaa4015", "phone": "13666667067", "organizationIds": ["488b8ad049bb43408a6fbcc50bcb89ac"], "floorList": ["605560541473144832", "605560541657694208"], "labelIds": [], "labelNames": [] } }) elif '/component/person/page' in self.path: self._json(200, {"code": "00000000", "success": True, "data": {"datas": [], "total": 0}}) else: self._json(200, {"code": "00000000", "success": True, "data": None}) def do_GET(self): self._json(200, {"status": "UP"}) def _json(self, status, data): self.send_response(status) self.send_header('Content-Type', 'application/json;charset=utf-8') self.end_headers() self.wfile.write(json.dumps(data, ensure_ascii=False).encode()) HTTPServer(('127.0.0.1', 33010), PersonStub).serve_forever() ``` - [ ] **Step 2: 启动 stub 并重新配置电梯** ```bash python3 scripts/test-env/stub-person-service.py & echo "Stub PID=$!" # 重新启动 elevator V2,Feign 路由到 stub:33010 ``` --- ## 实施依赖 ``` Task 1 (infra) ──→ Task 2 (ninca-common) ↓ ↘ (fail → Task 8 stub) Task 3 (comp-org) ↓ Task 4 (elevator V2) + Task 5 (elevator V1) ↓ Task 6 (对拍) + Task 7 (策略差异) ``` Task 4/5 可并行;Task 6/7 可在 4/5 完成后立即执行。