diff --git a/.gitignore b/.gitignore index 5a3d80c5..45021636 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ **/node_modules/ !/frontend-source/ !/frontend-source/** + +# Generated test env files +scripts/test-env/logs/ +scripts/test-env/services/ +*.jar.bak diff --git a/docs/superpowers/plans/2026-05-02-full-deploy-and-compare.md b/docs/superpowers/plans/2026-05-02-full-deploy-and-compare.md new file mode 100644 index 00000000..9b196d14 --- /dev/null +++ b/docs/superpowers/plans/2026-05-02-full-deploy-and-compare.md @@ -0,0 +1,327 @@ +# 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 完成后立即执行。 diff --git a/maven-cw-elevator-application/deploy/v2-maven/application-test.properties b/maven-cw-elevator-application/deploy/v2-maven/application-test.properties new file mode 100644 index 00000000..38edf95a --- /dev/null +++ b/maven-cw-elevator-application/deploy/v2-maven/application-test.properties @@ -0,0 +1,48 @@ +# V2 elevator test config — auto-generated from template +# Variables: 192.168.3.12 3307 127.0.0.1 1qaz!QAZ +# 127.0.0.1 9517 127.0.0.1 9092 +# 127.0.0.1 16106 +server.port=18081 +spring.application.name=elevator-app +spring.profiles.active=access-control + +# Consul +spring.cloud.consul.host=127.0.0.1 +spring.cloud.consul.port=9517 +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://192.168.3.12:3307/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=127.0.0.1 +spring.redis.port=6379 +spring.redis.password=1qaz!QAZ +spring.redis.database=5 + +# Kafka +cloudwalk.event.bootstrap-servers=127.0.0.1:9092 +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=127.0.0.1:16106 +ninca-crk-std.ip=127.0.0.1:16106 + +# 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