Initial commit: reorganized source tree

- backend/: 13 Maven modules (cw-elevator-application, cloudwalk-cloud, intelligent-cwoscomponent, ninca-crk, etc.)
- frontend/: 4 Vue projects (elevator-front, cwos-portal, alarm-front, front_acs) + decompiled + scripts
- scripts/: build, test-env, tools (Docker Compose, service templates, API parity)
- docs/: AGENTS.md, superpowers specs, architecture docs
- .gitignore: standard Java/Maven exclusions

Moved from legacy maven-*/ root layout to backend/ organized structure.
This commit is contained in:
hpd840321
2026-05-09 09:00:12 +08:00
commit 7b2bd307f1
7260 changed files with 612980 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
#!/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)"
# Build (skip tests)
$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
# Sync JAR to deploy/ — use wildcard to match any version
LATEST_JAR=$(ls -t cw-elevator-application-starter/target/cw-elevator-application-*.jar 2>/dev/null | grep -v sources | head -1)
if [[ -n "$LATEST_JAR" ]]; then
cp "$LATEST_JAR" deploy/v2-maven/
log_ok "JAR synced: $(basename "$LATEST_JAR") → deploy/v2-maven/"
else
log_error "No JAR found in target/"
exit 1
fi
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
# V2 测试环境 — 统一环境变量
# 所有脚本 source 此文件获取统一配置
set -euo pipefail
# ============================================
# 路径
# ============================================
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
STAR_CENTER="$REPO_ROOT/../runtime"
DATA_BACKUP="$REPO_ROOT/../data-backups"
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"
JAVA_OPTS_LIGHT="-Xmx2048m -Xms512m"
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=6380
REDIS_PASS="1qaz!QAZ"
CONSUL_HOST=127.0.0.1
CONSUL_PORT=9517
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/backend/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} $*"; }
@@ -0,0 +1,41 @@
# 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
@@ -0,0 +1,11 @@
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__
@@ -0,0 +1,54 @@
# 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
@@ -0,0 +1,48 @@
# 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
@@ -0,0 +1,11 @@
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__
+36
View File
@@ -0,0 +1,36 @@
services:
consul:
image: hashicorp/consul:1.22
container_name: v2test-consul
restart: unless-stopped
ports:
- "9517: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:
- "6380:6379"
command: redis-server --requirepass "1qaz!QAZ"
nginx:
image: nginx:alpine
container_name: v2test-nginx
restart: unless-stopped
ports:
- "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:
+53
View File
@@ -0,0 +1,53 @@
#!/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/health"
check_http "elevator-v1" "http://127.0.0.1:$PORT_ELEVATOR_V1/health"
check_http "crk-std" "http://127.0.0.1:$PORT_CRK_MGMT/health"
check_http "alarm-app" "http://127.0.0.1:$PORT_ALARM_MGMT/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" 2>/dev/null | 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
+91
View File
@@ -0,0 +1,91 @@
#!/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}"
# Check MySQL connectivity
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"
# DB name → backup file mapping
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"
)
# Check if DB exists and has tables — skip restore if already populated
db_has_tables() {
local db="$1"
local count
count=$($MYSQL_CMD -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$db'" 2>/dev/null || echo "0")
[[ "$count" -gt 0 ]]
}
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
# Idempotency: skip if DB already has data
if db_has_tables "$db_name"; then
log_ok " $db_name already exists ($(du -h "$backup_path" | cut -f1) backup skipped)"
continue
fi
log_info "Restoring $db_name from $backup_file ($(du -h "$backup_path" | cut -f1))..."
# Create DB if not exists
$MYSQL_CMD -e "CREATE DATABASE IF NOT EXISTS \`$db_name\` DEFAULT CHARACTER SET utf8mb4;"
# Import (strip multi-line GTID_PURGED to avoid MySQL error 1840)
zcat "$backup_path" | sed '/SET @@GLOBAL.GTID_PURGED=/,/;/d' | $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
# Elevator app also needs second backup (34_*.sql.gz) — skip if DB already populated
if [[ -f "$DATA_BACKUP/34_2026_04_23_17_28_33.sql.gz" ]]; then
if db_has_tables "$DB_ELEVATOR"; then
log_ok " $DB_ELEVATOR partition 34 skipped (DB already populated)"
else
log_info "Restoring elevator DB partition 34..."
zcat "$DATA_BACKUP/34_2026_04_23_17_28_33.sql.gz" | sed '/SET @@GLOBAL.GTID_PURGED=/,/;/d' | $MYSQL_CMD "$DB_ELEVATOR"
log_ok " $DB_ELEVATOR partition 34 restored"
fi
fi
# Apply V2.0.7 DDL (tenant_visitor_floor_policy) — skip if table exists
log_info "Checking V2.0.7 DDL (tenant_visitor_floor_policy)..."
TABLE_EXISTS=$($MYSQL_CMD -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_ELEVATOR' AND table_name='tenant_visitor_floor_policy'" 2>/dev/null || echo "0")
if [[ "$TABLE_EXISTS" -gt 0 ]]; then
log_ok " tenant_visitor_floor_policy already exists — DDL skipped"
else
log_info "Applying V2.0.7 DDL..."
$MYSQL_CMD "$DB_ELEVATOR" < "$REPO_ROOT/docs/sql/tenant_visitor_floor_policy.sql"
log_ok " V2 DDL applied"
fi
log_info "Database preparation complete"
+82
View File
@@ -0,0 +1,82 @@
#!/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"
# Tarballs to extract
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"
}
# Generate configs for each service
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"
# Generate configs for extracted tarball services
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"
+145
View File
@@ -0,0 +1,145 @@
#!/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 9517 6380 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"
# Stop and remove any existing v2test containers and conflicting containers
log_info "Cleaning up existing containers..."
docker rm -f v2test-consul v2test-redis v2test-nginx cw-frontend-local-nginx v2-test-redis 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
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
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
+92
View File
@@ -0,0 +1,92 @@
#!/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
if pgrep -f "$(basename "$jar")" >/dev/null 2>&1; then
log_warn " $name already running ($(pgrep -f "$(basename "$jar")" | tr '\n' ' ')) — skipping"
return 0
fi
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/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*" 2>/dev/null | head -1)
if [[ -n "$COMMON_JAR" ]]; then
start_service "ninca-common" "$COMMON_JAR" "$PORT_NINCA_COMMON" "$JAVA_OPTS_LIGHT"
fi
# A2: component-organization
ORG_JAR=$(find "$SERVICE_DIR/component-org" -name "*.jar" -not -name "*-sources*" 2>/dev/null | head -1)
if [[ -n "$ORG_JAR" ]]; then
start_service "component-org" "$ORG_JAR" "$PORT_COMPONENT_ORG" "$JAVA_OPTS_LIGHT"
fi
# A10: CRK-std
CRK_DIR="$STAR_CENTER/ninca-crk-std/ninca-crk-std-backend-V2.9.2_20210730"
CRK_JAR="$STAR_CENTER/ninca-crk-std/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/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/"
fi
# A12: elevator V2 (最后启动) — use wildcard to match any version
ELEVATOR_V2_JAR=$(ls -t "$REPO_ROOT/backend/cw-elevator-application/deploy/v2-maven/cw-elevator-application-"*.jar 2>/dev/null | head -1)
if [[ -n "$ELEVATOR_V2_JAR" ]]; then
start_service "elevator-v2" "$ELEVATOR_V2_JAR" "$PORT_ELEVATOR_V2" "$JAVA_OPTS_HEAVY" --spring.config.location="$REPO_ROOT/backend/cw-elevator-application/deploy/v2-maven/"
else
log_warn "No elevator V2 JAR found in deploy/v2-maven/"
fi
# A13: elevator V1 (对拍对照)
ELEVATOR_V1_JAR=$(ls -t "$REPO_ROOT/backend/cw-elevator-application/deploy/v1-legacy/cw-elevator-application-"*.jar 2>/dev/null | head -1)
if [[ -n "$ELEVATOR_V1_JAR" ]]; then
start_service "elevator-v1" "$ELEVATOR_V1_JAR" "$PORT_ELEVATOR_V1" "$JAVA_OPTS_HEAVY" --spring.config.location="$REPO_ROOT/backend/cw-elevator-application/deploy/v1-legacy/"
else
log_warn "No elevator V1 JAR found in deploy/v1-legacy/"
fi
log_info "All services started"
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# stop-all.sh — 停止所有 V2 测试环境服务 (PID 文件 + 进程名匹配)
source "$(dirname "${BASH_SOURCE[0]}")/config/env.sh"
log_info "Stopping all services..."
# 1. Stop by PID files
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" 2>/dev/null || true
sleep 2
kill -9 "$pid" 2>/dev/null || true
log_ok " $svc stopped"
else
log_info " $svc already dead (pid=$pid)"
fi
rm -f "$pid_file"
fi
done
# 2. Kill any remaining Java processes by name pattern
KILL_PATTERNS=(
"cw-elevator-application"
"ninca-crk-std-backend"
"ninca-qk-alarm-app"
"ninca-common"
"component-organization"
"cwos-portal"
"cwos-manager"
"cwos-system-api"
)
for pattern in "${KILL_PATTERNS[@]}"; do
pids=$(pgrep -f "$pattern" 2>/dev/null || true)
if [[ -n "$pids" ]]; then
for pid in $pids; do
log_info "Killing $pattern (pid=$pid)..."
kill "$pid" 2>/dev/null || true
sleep 1
kill -9 "$pid" 2>/dev/null || true
done
log_ok " $pattern cleaned up"
fi
done
# 3. Stop Docker containers
log_info "Stopping Docker containers..."
cd "$(dirname "${BASH_SOURCE[0]}")"
docker compose -f docker-compose.infra.yml down 2>/dev/null || true
log_ok " Docker containers stopped"
log_info "All services stopped"
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Stub: PersonResult format — floorList is a top-level field in data"""
from http.server import HTTPServer, BaseHTTPRequestHandler
import json, sys
PERSON_DATA = {
"id": "1072908835884208128", "businessId": "2524639890ba4f2cba9ba1a4eeaa4015",
"personCode": "PC001", "name": "秦夏", "userName": "qinxia", "phone": "13666667067",
"organizationIds": ["488b8ad049bb43408a6fbcc50bcb89ac"],
"organizationNames": ["广发基金"],
"labelIds": [], "labelNames": [],
"floorList": ["605560541473144832", "605560541657694208", "605560542911791104"],
"defaultFloor": "605560541473144832",
"status": 0, "isDel": 0
}
class PersonStub(BaseHTTPRequestHandler):
def log_message(self, f, *a): print(f"[stub] {a}", file=sys.stderr)
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 {}
if '/detail' in self.path:
resp = {"code": "00000000", "success": True, "message": "success", "data": PERSON_DATA}
else:
resp = {"code": "00000000", "success": True, "data": {"total": 0, "datas": []}}
self._json(resp)
def do_GET(self):
self._json({"status": "UP"})
def _json(self, data):
resp = json.dumps(data, ensure_ascii=False).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json;charset=utf-8')
self.send_header('Content-Length', str(len(resp)))
self.end_headers()
self.wfile.write(resp)
port = int(sys.argv[1]) if len(sys.argv) > 1 else 33011
HTTPServer(('127.0.0.1', port), PersonStub).serve_forever()
+65
View File
@@ -0,0 +1,65 @@
#!/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 基线
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 — Consul 注册检查
log_info "[F8] Alarm Consul registration"
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
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Heuristic static checks aligned with common 阿里巴巴手册 themes (not full P3C).
Outputs markdown to stdout or --out file.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
TAB = re.compile(r"^\t+")
TRAIL = re.compile(r"[ \t]+$")
LONG_LINE = re.compile(r".{121,}")
SYSTEM_OUT = re.compile(r"\bSystem\.(out|err)\.(print|println)\s*\(")
EMPTY_CATCH = re.compile(r"catch\s*\([^)]+\)\s*\{\s*\}", re.MULTILINE)
CATCH_EXCEPTION_PASS = re.compile(
r"catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}", re.MULTILINE
)
TODO_FXXX = re.compile(r"\b(FIXME|XXX)\b", re.IGNORECASE)
def scan_file(path: Path, rel: str) -> list[tuple[str, int, str]]:
issues: list[tuple[str, int, str]] = []
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError as e:
return [("ERROR", 0, f"{rel}: {e}")]
lines = text.splitlines()
for i, line in enumerate(lines, 1):
if TAB.search(line):
issues.append(("FORMAT", i, "行首含 TAB,建议仅用空格缩进(手册排版/可读)"))
if TRAIL.search(line):
issues.append(("FORMAT", i, "行尾多余空白"))
if LONG_LINE.search(line):
issues.append(("FORMAT", i, "行宽超过约 120 字符,建议换行(团队规约常见上限)"))
if SYSTEM_OUT.search(line) and "/test/" not in str(path).replace("\\", "/"):
issues.append(("STYLE", i, "使用 System.out/err,生产代码建议改用日志框架"))
if TODO_FXXX.search(line):
issues.append(("STYLE", i, "含 FIXME/XXX 标记,发版前应清理或跟踪"))
body = "\n".join(lines)
if EMPTY_CATCH.search(body):
issues.append(("RISK", 0, "存在空 catch 块 {},手册建议至少记录或处理异常"))
if CATCH_EXCEPTION_PASS.search(body):
issues.append(
("RISK", 0, "存在 catch (Exception) { } 空实现,建议细化异常类型或记录日志")
)
return issues
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument(
"--repo",
type=Path,
default=Path(__file__).resolve().parent.parent,
help="Repo root containing maven-*",
)
ap.add_argument("--out", type=Path, default=None, help="Write markdown report")
args = ap.parse_args()
repo: Path = args.repo
roots = sorted(p for p in repo.glob("maven-*") if p.is_dir())
lines_out: list[str] = []
lines_out.append("# 阿里巴巴手册相关:启发式静态扫描\n")
lines_out.append(
"> 非 P3C 全量规则;仅覆盖 **TAB/行尾空白/超长行/System.out/空 catch/FIXME** 等易自动化项。\n"
)
grand = 0
for root in roots:
name = root.name
lines_out.append(f"\n## `{name}`\n")
mod_issues = 0
for java in sorted(root.rglob("*.java")):
sp = str(java).replace("\\", "/")
if "/target/" in sp:
continue
rel = java.relative_to(repo)
found = scan_file(java, str(rel))
if not found:
continue
mod_issues += len(found)
lines_out.append(f"\n### `{rel}`\n")
lines_out.append("| 级别 | 行 | 说明 |\n|------|----|------|\n")
for sev, ln, msg in found[:30]:
lines_out.append(f"| {sev} | {ln or '-'} | {msg} |\n")
if len(found) > 30:
lines_out.append(f"\n… 另有 {len(found) - 30} 条(同文件省略)\n")
if mod_issues == 0:
lines_out.append("\n(本模块未发现上述启发式命中项)\n")
grand += mod_issues
lines_out.append(f"\n---\n\n**启发式命中条数(含重复行规则)**:{grand}\n")
text = "".join(lines_out)
if args.out:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(text, encoding="utf-8")
print(args.out, file=sys.stderr)
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
sys.exit(main())
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# V1 基准与当前构建 fat-jar 的嵌套 lib 坐标 multiset 门禁。见 scripts/generate_v1_v2_elevator_dependency_diff.py --help
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec python3 "$ROOT/scripts/generate_v1_v2_elevator_dependency_diff.py" --gate "$@"
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -euo pipefail
# 生产只读证据采集(Maven 发布包内与本脚本同置于部署根目录,与 start.sh / properties 同层):
# - 进程参数/环境/工作目录
# - 本地配置文件
# - jcmd system properties
# - 应用日志关键片段
# - Consul 健康与 KV 快照
# 最终输出 tar.gz,便于离线定位“配置来源 -> Ribbon 实例列表”问题。
APP_DIR="${1:-/data/cwos/cw-elevator-application-V1.0.0.20211103}"
CONSUL_ADDR="${2:-10.0.22.102:8500}"
OUT_ROOT="${3:-${APP_DIR}/evidence}"
APP_NAME="${4:-elevator-app}"
# 现场 JDK(生产 cwos-node 固定路径;不依赖 PATH
CWOS_JAVA_BIN="/data/cwos/java/bin"
JAVA_BIN="${CWOS_JAVA_BIN}/java"
JAR_BIN="${CWOS_JAVA_BIN}/jar"
JCMD_BIN="${CWOS_JAVA_BIN}/jcmd"
DATE_BIN="/bin/date"
timestamp="$(${DATE_BIN} +%Y%m%d-%H%M%S)"
OUT_DIR="${OUT_ROOT}/elevator-evidence-${timestamp}"
mkdir -p "${OUT_DIR}"
log() { echo "[collect] $*"; }
log "APP_DIR=${APP_DIR}"
log "CONSUL_ADDR=${CONSUL_ADDR}"
log "OUT_DIR=${OUT_DIR}"
log "JAVA_BIN=${JAVA_BIN} JAR_BIN=${JAR_BIN} JCMD_BIN=${JCMD_BIN}"
PID="$(ps -ef | awk '/java/ && /cw-elevator-application/ && !/awk/ {print $2; exit}')"
if [[ -z "${PID}" ]]; then
echo "ERROR: 未找到 cw-elevator-application Java 进程" >&2
exit 1
fi
log "PID=${PID}"
echo "${PID}" > "${OUT_DIR}/pid.txt"
# 1) 进程与系统基础信息
ps -ef > "${OUT_DIR}/ps-ef.txt"
uname -a > "${OUT_DIR}/uname.txt"
${DATE_BIN} +%Y-%m-%dT%H:%M:%S%z > "${OUT_DIR}/collected-at.txt"
tr '\0' ' ' < "/proc/${PID}/cmdline" > "${OUT_DIR}/proc-cmdline.txt" || true
tr '\0' '\n' < "/proc/${PID}/environ" > "${OUT_DIR}/proc-environ.txt" || true
ls -l "/proc/${PID}/cwd" > "${OUT_DIR}/proc-cwd.txt" || true
# 2) 本地配置快照(若存在)
for f in bootstrap.properties application.properties application-access-control.properties start.sh stop.sh cw-elevator-application.service; do
if [[ -f "${APP_DIR}/${f}" ]]; then
cp -a "${APP_DIR}/${f}" "${OUT_DIR}/${f}"
fi
done
# 3) JAR 与结构快照
JAR_PATH="$(awk '{print $1}' "${OUT_DIR}/proc-cmdline.txt" | sed 's/[[:space:]]*$//')"
if [[ -f "${APP_DIR}/cw-elevator-application-V1.0.0.20211103.jar" ]]; then
JAR_PATH="${APP_DIR}/cw-elevator-application-V1.0.0.20211103.jar"
fi
echo "${JAR_PATH}" > "${OUT_DIR}/jar-path.txt"
if [[ -f "${JAR_PATH}" ]]; then
sha256sum "${JAR_PATH}" > "${OUT_DIR}/jar.sha256.txt" || true
if [[ -x "${JAR_BIN}" ]]; then
"${JAR_BIN}" tf "${JAR_PATH}" > "${OUT_DIR}/jar-tf.txt" || true
else
echo "jar not found or not executable: ${JAR_BIN}" > "${OUT_DIR}/jar-tf.txt"
fi
unzip -p "${JAR_PATH}" application.properties > "${OUT_DIR}/jar-application.properties.txt" 2>/dev/null || true
unzip -p "${JAR_PATH}" bootstrap.properties > "${OUT_DIR}/jar-bootstrap.properties.txt" 2>/dev/null || true
fi
# 4) jcmd system properties + attach 诊断(不修改应用配置;便于修复 AttachNotSupportedException
{
echo "=== current shell user ==="
id 2>/dev/null || true
echo "=== target java process ==="
ps -o user=,group=,pid=,args= -p "${PID}" 2>/dev/null || true
PROC_USER="$(stat -c '%U' "/proc/${PID}" 2>/dev/null || echo "")"
PROC_UID="$(stat -c '%u' "/proc/${PID}" 2>/dev/null || echo "")"
echo "proc_owner=${PROC_USER} uid=${PROC_UID}"
echo "=== /tmp hsperfdata (HotSpot perf counter; attach 相关) ==="
if [[ -n "${PROC_USER}" && "${PROC_USER}" != "unknown" ]]; then
HS="/tmp/hsperfdata_${PROC_USER}"
if [[ -d "${HS}" ]]; then
ls -la "${HS}" 2>/dev/null | head -30 || true
ls -la "${HS}/${PID}" 2>/dev/null || echo "missing ${HS}/${PID}"
else
echo "no directory ${HS}"
fi
fi
echo "=== cmdline tokens (attach / jdwp) ==="
tr '\0' '\n' < "/proc/${PID}/cmdline" 2>/dev/null | grep -E 'DisableAttach|Attach|jdwp|agentpath' || echo "(none matched)"
} > "${OUT_DIR}/jcmd-attach-diagnose.txt" 2>&1
if [[ -x "${JCMD_BIN}" ]]; then
"${JCMD_BIN}" "${PID}" VM.system_properties > "${OUT_DIR}/jcmd-system-properties.txt" 2>&1 || true
if grep -q 'AttachNotSupportedException\|Unable to open socket file' "${OUT_DIR}/jcmd-system-properties.txt" 2>/dev/null; then
{
echo ""
echo "HINT: jcmd attach 失败常见原因:"
echo " 1) 与 Java 进程不同用户执行 jcmd(请用与进程相同用户,例如: sudo -u <java_user> ${JCMD_BIN} ${PID} VM.system_properties"
echo " 2) /tmp/hsperfdata_<user>/<pid> 缺失或权限异常"
echo " 3) JVM 启动参数含 -XX:+DisableAttachMechanism(见 jcmd-attach-diagnose.txt 中 cmdline"
echo " 4) 进程非 HotSpot 或尚未完全初始化(极少见于长期运行的 Spring Boot"
} >> "${OUT_DIR}/jcmd-system-properties.txt"
fi
else
echo "jcmd not found or not executable: ${JCMD_BIN}" > "${OUT_DIR}/jcmd-system-properties.txt"
fi
# 4b) java 版本(与现场 JDK 一致性的旁证)
if [[ -x "${JAVA_BIN}" ]]; then
"${JAVA_BIN}" -version > "${OUT_DIR}/java-version.txt" 2>&1 || true
else
echo "java not found or not executable: ${JAVA_BIN}" > "${OUT_DIR}/java-version.txt"
fi
# 5) 应用日志关键行
LOG_FILE="${APP_DIR}/logs/elevator-app.log"
if [[ -f "${LOG_FILE}" ]]; then
cp -a "${LOG_FILE}" "${OUT_DIR}/elevator-app.log.full"
awk '
/CONFIG SOURCE PROBE START|CONFIG SOURCE PROBE END|probe key=|ConfigurationBasedServerList|Load balancer does not have available server|DynamicServerListLoadBalancer|ConsulServiceRegistry|Registering service with consul/ { print }
' "${LOG_FILE}" > "${OUT_DIR}/elevator-app.log.keylines.txt"
fi
# 6) Consul 快照
CURL="curl -sS --max-time 8"
${CURL} "http://${CONSUL_ADDR}/v1/health/service/${APP_NAME}?passing=true" > "${OUT_DIR}/consul-health-${APP_NAME}.json" || true
for svc in cwos-portal ninca-common ninca-common-component-organization ninca-crk-std cloudwalk-device-thirdparty; do
${CURL} "http://${CONSUL_ADDR}/v1/health/service/${svc}?passing=true" > "${OUT_DIR}/consul-health-${svc}.json" || true
done
${CURL} "http://${CONSUL_ADDR}/v1/kv/config/${APP_NAME}/data?raw" > "${OUT_DIR}/consul-kv-${APP_NAME}.properties" || true
${CURL} "http://${CONSUL_ADDR}/v1/kv/config/${APP_NAME},access-control/data?raw" > "${OUT_DIR}/consul-kv-${APP_NAME},access-control.properties" || true
# 7) 现场可达性快照(已知主机名)
for host in 0837a70b5fab47569391828f5feb2561 371bfca4972c43d2aefcf302d0a4a277 44700995ee904679a7ad5afddcf93bb5; do
getent hosts "${host}" > "${OUT_DIR}/getent-${host}.txt" 2>&1 || true
curl -I --max-time 5 "http://${host}:8089/" > "${OUT_DIR}/curl-head-${host}-8089.txt" 2>&1 || true
done
ARCHIVE="${OUT_DIR}.tar.gz"
tar -czf "${ARCHIVE}" -C "$(dirname "${OUT_DIR}")" "$(basename "${OUT_DIR}")"
log "DONE archive=${ARCHIVE}"
echo "${ARCHIVE}"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Compare a JAR's .class entries to decompiled .java tree (Maven src/main/java)."""
from __future__ import annotations
import argparse
import sys
import zipfile
from pathlib import Path
def jar_class_entries(jar_path: Path) -> set[str]:
"""FQN-like names for each .class (uses /), inner as Outer$Inner."""
out: set[str] = set()
with zipfile.ZipFile(jar_path, "r") as zf:
for name in zf.namelist():
if not name.endswith(".class") or "META-INF" in name:
continue
rel = name[:-6] # drop .class
out.add(rel.replace("/", "."))
return out
def java_fqns(src_root: Path) -> set[str]:
"""Best-effort FQN from path .../src/main/java/a/b/C.java -> a.b.C."""
src_root = src_root.resolve()
java_root = src_root / "src/main/java"
if not java_root.is_dir():
if src_root.name == "java" and (src_root.parent / "main").exists():
java_root = src_root
else:
return set()
fqns: set[str] = set()
for f in java_root.rglob("*.java"):
rel = f.relative_to(java_root).with_suffix("")
fqns.add(".".join(rel.parts))
return fqns
def outer_java_for_class(fqn: str, java_fqns: set[str]) -> str | None:
"""Map class FQN to expected .java outer name (strip $ inner)."""
if "$" in fqn:
outer = fqn.split("$", 1)[0]
else:
outer = fqn
if outer in java_fqns:
return outer
return None
def analyze(jar: Path, src_module_roots: list[Path]) -> dict:
classes = jar_class_entries(jar)
all_java: set[str] = set()
for root in src_module_roots:
if root.is_dir():
all_java |= java_fqns(root)
missing_outer: list[str] = []
for c in sorted(classes):
if c.startswith("module-info"):
continue
if outer_java_for_class(c, all_java) is None:
missing_outer.append(c)
# "extra" java: outer types not present as any class in jar (rough)
jar_outers = {x.split("$", 1)[0] for x in classes if not x.startswith("module-info")}
extra_java = sorted(x for x in all_java if x not in jar_outers and "$" not in x)
return {
"jar": str(jar),
"class_count": len(classes),
"java_outer_count": len(all_java),
"missing_classes": missing_outer,
"possibly_extra_sources": extra_java[:200], # cap
"possibly_extra_sources_total": len(extra_java),
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("jar", type=Path)
ap.add_argument("src_roots", nargs="+", type=Path, help="Module dirs containing src/main/java")
args = ap.parse_args()
r = analyze(args.jar, args.src_roots)
print(f"JAR: {r['jar']}")
print(f"Classes in JAR: {r['class_count']}; outer .java types under src/main/java: {r['java_outer_count']}")
miss = r["missing_classes"]
print(f"Missing (no matching outer .java): {len(miss)}")
for line in miss[:80]:
print(f" - {line}")
if len(miss) > 80:
print(f" ... and {len(miss) - 80} more")
ex = r["possibly_extra_sources"]
tot = r["possibly_extra_sources_total"]
print(f"Possibly extra .java (not as outer in JAR): {tot} (showing up to {len(ex)})")
for line in ex[:40]:
print(f" + {line}")
if tot > 40:
print(f" ...")
return 1 if miss else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从 V1 运行包目录读取 lib/ 与 cw_lib/ 的 JAR 清单,并与 V2 Maven 反应堆
`cw-elevator-application-reactor/pom.xml` 中显式声明的版本属性做对照。
用法(在仓库根目录):
python3 scripts/compare_v1_v2_elevator_dependencies.py
可选环境变量:
V1_ROOT 默认: cw-elevator-application-V1.0.0.20211103
REPO_ROOT 默认: 脚本所在目录的上一级(仓库根)
"""
from __future__ import annotations
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parents[1]
def list_jars(d: Path) -> list[str]:
if not d.is_dir():
return []
return sorted(p.name for p in d.glob("*.jar"))
def jar_stem_versions(jars: list[str]) -> dict[str, list[str]]:
"""artifactId -> [version-ish suffixes from filename]."""
by_artifact: dict[str, list[str]] = defaultdict(list)
for name in jars:
if not name.endswith(".jar"):
continue
base = name[:-4]
m = re.match(r"^(.+)-(\d[\d.\w-]*)$", base)
if m:
by_artifact[m.group(1)].append(m.group(2))
else:
by_artifact[base].append("")
return dict(by_artifact)
def parse_pom_properties(pom: Path) -> dict[str, str]:
text = pom.read_text(encoding="utf-8", errors="replace")
m = re.search(r"<properties>(.*?)</properties>", text, re.DOTALL)
if not m:
return {}
block = m.group(1)
props = {}
for mm in re.finditer(r"<([a-zA-Z0-9_.-]+)>\s*([^<]+?)\s*</\1>", block):
k, v = mm.group(1), mm.group(2).strip()
if k and v and "${" not in v:
props[k] = v
return props
def main() -> int:
root = Path(os.environ.get("REPO_ROOT", repo_root()))
v1 = root / os.environ.get(
"V1_ROOT", "cw-elevator-application-V1.0.0.20211103"
)
lib = v1 / "lib"
cw = v1 / "cw_lib"
pom = root / "maven-cw-elevator-application" / "pom.xml"
if not pom.is_file():
print("missing", pom, file=sys.stderr)
return 2
jars_lib = list_jars(lib)
jars_cw = list_jars(cw)
props = parse_pom_properties(pom)
print("## V1 目录\n")
print(f"- lib: {lib} {len(jars_lib)} 个 JAR")
print(f"- cw_lib: {cw} {len(jars_cw)} 个 JAR\n")
print("## V2 反应堆版本属性(节选,与 cw_lib 对齐项)\n")
keys = [
"cloudwalk.internal.version",
"cloudwalk.legacy.public.version",
"intelligent.cwoscomponent.version",
"davinci.manager.storage.version",
"intelligent.lock.version",
"cwos.sdk.resource.version",
"cwos.sdk.event.version",
"spring-cloud.version",
]
for k in keys:
if k in props:
print(f"- `{k}` → **{props[k]}**")
dup = {a: vs for a, vs in jar_stem_versions(jars_lib).items() if len(vs) > 1}
print("\n## V1 lib 中「同 artifact 前缀、多版本」示例( Classpath 冲突风险)\n")
shown = 0
for art in sorted(dup.keys()):
vers = sorted(set(dup[art]))
if len(vers) <= 1:
continue
print(f"- `{art}`: {', '.join(vers)}")
shown += 1
if shown >= 25:
print("- …(其余略,可对本脚本输出重定向后全文检索)")
break
print("\n## 与 cw_lib 文件名逐字对照(V2 POM 显式坐标)\n")
mapping = [
("cloudwalk-common-event", "cloudwalk.internal.version"),
("cloudwalk-common-result", "cloudwalk.legacy.public.version"),
("cloudwalk-common-serial", "cloudwalk.legacy.public.version"),
("cloudwalk-common-service", "cloudwalk.internal.version"),
("cloudwalk-common-web", "cloudwalk.legacy.public.version"),
("cloudwalk-intelligent-component-lock", "intelligent.lock.version"),
("davinci-manager-storage", "davinci.manager.storage.version"),
("intelligent-cwoscomponent-rest", "intelligent.cwoscomponent.version"),
("intelligent-cwoscomponent-interface", "intelligent.cwoscomponent.version"),
("cwos-java-sdk-resource", "cwos.sdk.resource.version"),
("cwos-sdk-event", "cwos.sdk.event.version"),
]
cw_set = set(jars_cw)
for prefix, prop in mapping:
ver = props.get(prop, "?")
expect = f"{prefix}-{ver}.jar"
hit = expect if expect in cw_set else None
if not hit:
# SNAPSHOT / classifier 简判:取 cw_lib 中以 prefix- 开头的文件
cand = [j for j in jars_cw if j.startswith(prefix + "-")]
cand_s = cand[0] if len(cand) == 1 else str(cand)
print(f"- **{prefix}**: V2 属性 `{prop}`=`{ver}` → 期望 `{expect}`V1 cw_lib 实际: `{cand_s}`")
else:
print(f"- **{prefix}**: V1=`{hit}`V2 属性 `{prop}`=`{ver}` ✓")
print("\n## 电梯自研模块 JAR 版本\n")
print(
"- V1 cw_lib: `cw-elevator-application-*-**1.0-SNAPSHOT**.jar`(四模块)\n"
"- V2 Maven: `cn.cloudwalk.elevator:*:**2.0-SNAPSHOT**`(反应堆版本,与 1.0 并存为正常升级)"
)
print("\n完整清单文件: `docs/architecture/data/v1-elevator-lib-jars.txt` 与 `v1-elevator-cw-lib-jars.txt`。")
print(
"\n生成 **V2 全量传递依赖树** 请在可解析私服的环境执行:\n"
" cd maven-cw-elevator-application && "
"mvn -pl cw-elevator-application-starter -am dependency:tree "
"-DoutputFile=target/v2-starter-dependency-tree.txt"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""
Extract listed ninca-crk-* lib jars from fat jar, CFR-decompile each into maven-ninca-crk-from-lib/<artifactId>/,
emit parent + child POMs (embedded pom from jar when present, sanitized parent).
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
FAT_JAR_DEFAULT = Path(
"/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/"
"星河湾星中星/星中心/ninca_crk_std_01-ninca_crk_std_backend/ninca-crk-std-backend-V2.9.2_20210730.jar"
)
REPO_ROOT = Path(__file__).resolve().parents[1]
OUT_ROOT = REPO_ROOT / "maven-ninca-crk-from-lib"
CFR_JAR = Path("/tmp/cfr-0.152.jar")
TARGET_NAMES = [
"ninca-crk-access-control-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-web-2.9.1_210630-SNAPSHOT.jar",
]
GROUP_ID = "cn.cloudwalk.ninca"
REACTOR_VERSION = "2.9.1_210630-SNAPSHOT"
JAVA_VERSION = "1.8"
def artifact_dir(jar_name: str) -> str:
return jar_name.replace(".jar", "").replace("-2.9.1_210630-SNAPSHOT", "")
def ensure_cfr() -> Path:
if CFR_JAR.is_file():
return CFR_JAR
raise SystemExit(
"Missing CFR at {} — download:\n"
" curl -fsSL -o {} "
"https://github.com/leibnitz27/cfr/releases/download/0.152/cfr-0.152.jar".format(CFR_JAR, CFR_JAR)
)
def extract_embedded_pom(zf: zipfile.ZipFile, inner_path: str) -> str | None:
try:
return zf.read(inner_path).decode("utf-8", errors="replace")
except KeyError:
return None
def find_embedded_pom_xml(zf: zipfile.ZipFile) -> tuple[str | None, str | None]:
"""Return (pom_xml_content, relative_path_in_zip)."""
for n in zf.namelist():
if n.endswith("/pom.xml") and "/META-INF/maven/" in n and "/ninca-crk-" in n:
return extract_embedded_pom(zf, n), n
return None, None
def sanitize_pom_xml(raw: str, artifact_id: str) -> str:
"""Remove embedded parent; attach reactor parent after modelVersion."""
raw = raw.lstrip("\ufeff").strip()
raw = re.sub(r"<parent\b[^>]*>[\s\S]*?</parent>\s*", "", raw, count=1, flags=re.I)
parent_block = (
" <parent>\n"
" <groupId>%s</groupId>\n"
" <artifactId>ninca-crk-from-lib-reactor</artifactId>\n"
" <version>%s</version>\n"
" <relativePath>../pom.xml</relativePath>\n"
" </parent>\n" % (GROUP_ID, REACTOR_VERSION)
)
m = re.search(r"</modelVersion>\s*", raw, flags=re.I)
if not m:
raise ValueError("embedded pom missing modelVersion")
pos = m.end()
return raw[:pos] + parent_block + raw[pos:]
def minimal_pom(artifact_id: str, description: str) -> str:
return """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>{gid}</groupId>
<artifactId>ninca-crk-from-lib-reactor</artifactId>
<version>{ver}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>{aid}</artifactId>
<name>{aid}</name>
<description>{desc}</description>
<properties>
<maven.compiler.source>{jv}</maven.compiler.source>
<maven.compiler.target>{jv}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
""".format(
gid=GROUP_ID,
ver=REACTOR_VERSION,
aid=artifact_id,
desc=description,
jv=JAVA_VERSION,
)
def reactor_pom(module_dirs: list[str]) -> str:
mods = "".join(" <module>{}</module>\n".format(m) for m in module_dirs)
return """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{gid}</groupId>
<artifactId>ninca-crk-from-lib-reactor</artifactId>
<version>{ver}</version>
<packaging>pom</packaging>
<name>ninca-crk-from-lib-reactor</name>
<description>
CFR 反编译自交付 fat jar lib/ 内 ninca-crk 四条业务线(access-control / conference-attendance /
smart-attendance / visitor-management)共 28 个构件;用于源码走查与对照,不代表官方原始工程结构。
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>{jv}</java.version>
<maven.compiler.source>${{java.version}}</maven.compiler.source>
<maven.compiler.target>${{java.version}}</maven.compiler.target>
</properties>
<modules>
{mods} </modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${{java.version}}</source>
<target>${{java.version}}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
""".format(
gid=GROUP_ID,
ver=REACTOR_VERSION,
mods=mods,
jv=JAVA_VERSION,
)
def run_cfr(jar_path: Path, java_out: Path) -> None:
java_out.mkdir(parents=True, exist_ok=True)
cmd = [
"java",
"-jar",
str(ensure_cfr()),
str(jar_path),
"--outputdir",
str(java_out),
"--silent",
"true",
]
subprocess.run(cmd, check=True)
def main() -> int:
fat_jar = Path(sys.argv[1]) if len(sys.argv) > 1 else FAT_JAR_DEFAULT
if not fat_jar.is_file():
print("Fat jar not found:", fat_jar, file=sys.stderr)
return 1
if OUT_ROOT.exists():
shutil.rmtree(OUT_ROOT)
OUT_ROOT.mkdir(parents=True)
module_dirs: list[str] = []
tmpdir = Path(tempfile.mkdtemp(prefix="crk_lib_extract_"))
try:
with zipfile.ZipFile(fat_jar, "r") as zf:
for name in TARGET_NAMES:
inner = "lib/" + name
if inner not in zf.namelist():
print("MISSING in fat jar:", inner, file=sys.stderr)
return 2
extract_path = tmpdir / name
with zf.open(inner) as src, open(extract_path, "wb") as dst:
shutil.copyfileobj(src, dst)
aid = artifact_dir(name)
mod_path = OUT_ROOT / aid
mod_path.mkdir(parents=True)
java_root = mod_path / "src" / "main" / "java"
java_root.mkdir(parents=True)
run_cfr(extract_path, java_root)
with zipfile.ZipFile(extract_path, "r") as inner_zf:
embedded, emb_path = find_embedded_pom_xml(inner_zf)
desc = "CFR from {} (embedded pom: {})".format(name, emb_path or "none")
if embedded:
try:
pom_body = sanitize_pom_xml(embedded, aid)
(mod_path / "pom.xml").write_text(pom_body, encoding="utf-8")
except Exception as exc:
print("WARN sanitize pom:", aid, exc, file=sys.stderr)
(mod_path / "pom.xml").write_text(minimal_pom(aid, desc), encoding="utf-8")
else:
(mod_path / "pom.xml").write_text(minimal_pom(aid, desc), encoding="utf-8")
module_dirs.append(aid)
(OUT_ROOT / "pom.xml").write_text(reactor_pom(module_dirs), encoding="utf-8")
readme = OUT_ROOT / "README.txt"
readme.write_text(
"Generated by scripts/decompile_ninca_crk_lib_modules.py\n"
"Source fat jar: {}\n".format(fat_jar)
+ "CFR: {}\n".format(CFR_JAR)
+ "\n反编译产物仅供走查;首次编译需在私服可用的环境下补齐依赖。\n",
encoding="utf-8",
)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
print("OK ->", OUT_ROOT)
return 0
if __name__ == "__main__":
sys.exit(main())
+555
View File
@@ -0,0 +1,555 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从 V1/V2 fat-jar 二进制解析嵌套 lib,读取 META-INF/maven/**/pom.properties,生成与 Maven 坐标可比对的数据。
子命令/模式:
- 默认:写 docs/testing/cw-elevator-v1-v2-dependency-diff.md
- --gate:以 V1 为基准,对 candidate fat-jar 做嵌套 JAR 坐标 multiset 比对,不一致时非零退出(可配允许列表)。
"""
from __future__ import annotations
import argparse
import io
import sys
import zipfile
from collections import Counter, defaultdict
from pathlib import Path
def _parse_props(raw: str) -> dict[str, str]:
props: dict[str, str] = {}
for line in raw.splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
props[k.strip()] = v.strip()
return props
def coords_from_nested_jar(data: bytes, jar_basename: str) -> tuple[str, str, str]:
"""Returns (groupId, artifactId, version) using META-INF/maven/**/pom.properties."""
try:
z = zipfile.ZipFile(io.BytesIO(data))
except zipfile.BadZipFile:
return ("?", "?", "?")
candidates: list[tuple[str, str, str]] = []
for name in z.namelist():
if not name.endswith("/pom.properties"):
continue
if "META-INF/maven/" not in name:
continue
try:
raw = z.read(name).decode("utf-8", errors="replace")
except KeyError:
continue
props = _parse_props(raw)
gid = props.get("groupId")
aid = props.get("artifactId")
ver = props.get("version")
if gid and aid and ver:
candidates.append((gid, aid, ver))
if not candidates:
return ("?", "?", "?")
stem = jar_basename[: -len(".jar")] if jar_basename.endswith(".jar") else jar_basename
for g, a, v in candidates:
if stem.startswith(a) or a in stem or stem.startswith(a.split(".")[-1]):
return (g, a, v)
return candidates[0]
def list_outer_nested(
outer: Path, inner_dir_prefix: str
) -> dict[str, tuple[str, str, str]]:
"""path_in_zip -> (g,a,v)"""
result: dict[str, tuple[str, str, str]] = {}
z = zipfile.ZipFile(outer)
for name in z.namelist():
if not name.startswith(inner_dir_prefix):
continue
if not name.endswith(".jar"):
continue
if name.endswith(".jar.original"):
continue
try:
data = z.read(name)
except KeyError:
continue
base = Path(name).name
g, a, v = coords_from_nested_jar(data, base)
result[name] = (g, a, v)
return result
def detect_lib_prefix(outer: Path) -> str:
"""可执行包为 V1 风格 lib/ 或 Spring Boot repackage 的 BOOT-INF/lib/。"""
z = zipfile.ZipFile(outer)
has_boot = any(n.startswith("BOOT-INF/lib/") and n.endswith(".jar") for n in z.namelist())
has_lib = any(n.startswith("lib/") and n.endswith(".jar") for n in z.namelist())
if has_boot and not has_lib:
return "BOOT-INF/lib/"
if has_lib:
return "lib/"
return "lib/"
def key_ga(path: str, t: tuple[str, str, str]) -> str:
g, a, v = t
if g == "?" or a == "?" or v == "?":
return f"unresolved:{Path(path).name}"
return f"{g}:{a}:{v}"
def multiset_from_jar(outer: Path) -> tuple[Counter[str], str, dict[str, tuple[str, str, str]]]:
prefix = detect_lib_prefix(outer)
jmap = list_outer_nested(outer, prefix)
ctr: Counter[str] = Counter()
for path, t in jmap.items():
ctr[key_ga(path, t)] += 1
return ctr, prefix, jmap
def _read_allow_counter(path: Path | None) -> Counter[str]:
if path is None or not path.is_file():
return Counter()
c: Counter[str] = Counter()
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) >= 2 and parts[-1].isdigit():
key, cnt = " ".join(parts[:-1]), int(parts[-1])
else:
key, cnt = line, 1
c[key] += cnt
return c
def _subtract_allow(diff: Counter[str], allow: Counter[str]) -> Counter[str]:
"""从 multiset 差值中扣减允许的盈余额度(每行 `coord` 或 `coord N`)。"""
out = Counter(diff)
for k, n in allow.items():
if k not in out:
continue
out[k] -= min(out[k], n)
if out[k] <= 0:
del out[k]
return out
def run_lib_gate(
baseline_jar: Path,
candidate_jar: Path,
allow_baseline_only: Path | None,
allow_candidate_only: Path | None,
) -> int:
"""Multiset 门禁:在应用允许偏差后 baseline 与 candidate 须一致。"""
if not baseline_jar.is_file():
print("Missing baseline jar:", baseline_jar, file=sys.stderr)
return 2
if not candidate_jar.is_file():
print("Missing candidate jar:", candidate_jar, file=sys.stderr)
return 2
b_ctr, b_prefix, _ = multiset_from_jar(baseline_jar)
c_ctr, c_prefix, _ = multiset_from_jar(candidate_jar)
allow_b = _read_allow_counter(allow_baseline_only)
allow_c = _read_allow_counter(allow_candidate_only)
only_b = _subtract_allow(b_ctr - c_ctr, allow_b)
only_c = _subtract_allow(c_ctr - b_ctr, allow_c)
if not only_b and not only_c:
print(
"lib parity OK:",
f"baseline={baseline_jar.name} ({b_prefix} n={sum(b_ctr.values())})",
f"candidate={candidate_jar.name} ({c_prefix} n={sum(c_ctr.values())})",
)
return 0
print("lib parity FAILED (multiset diff after allowlists)", file=sys.stderr)
print(f" baseline: {baseline_jar} prefix={b_prefix} keys={len(b_ctr)}", file=sys.stderr)
print(f" candidate: {candidate_jar} prefix={c_prefix} keys={len(c_ctr)}", file=sys.stderr)
if only_b:
print(" only in baseline (or excess count):", file=sys.stderr)
for k in sorted(only_b.keys()):
print(f" {k} x{only_b[k]}", file=sys.stderr)
if only_c:
print(" only in candidate (or excess count):", file=sys.stderr)
for k in sorted(only_c.keys()):
print(f" {k} x{only_c[k]}", file=sys.stderr)
return 1
def parse_maven_dependency_list(path: Path) -> list[tuple[str, str, str, str]]:
"""Parse mvn dependency:list output lines like 'groupId:artifactId:jar:version:compile'."""
rows: list[tuple[str, str, str, str]] = []
text = path.read_text(encoding="utf-8", errors="replace")
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("The following"):
continue
if line == "none":
continue
parts = line.split(":")
if len(parts) >= 5 and parts[2] == "jar":
gid, aid, _, ver, scope = parts[0], parts[1], parts[2], parts[3], parts[4]
rows.append((gid, aid, ver, scope))
return sorted(rows, key=lambda x: (x[0], x[1], x[2]))
def collect_ga_versions_from_jar_map(
jmap: dict[str, tuple[str, str, str]],
) -> dict[tuple[str, str], set[str]]:
"""嵌套 jar -> 每个 (groupId, artifactId) 在 fat-jar 中出现的 version 集合(仅 pom.properties 可解析条目)。"""
ga_ver: dict[tuple[str, str], set[str]] = defaultdict(set)
for _path, (g, a, v) in jmap.items():
if g != "?" and a != "?" and v != "?":
ga_ver[(g, a)].add(v)
return ga_ver
def ga_version_skew_rows(
v1_ga_v: dict[tuple[str, str], set[str]],
v2_ga_v: dict[tuple[str, str], set[str]],
) -> list[tuple[tuple[str, str], list[str], list[str]]]:
"""两侧均能解析到 version、且 version 集合不一致的 GA。"""
out: list[tuple[tuple[str, str], list[str], list[str]]] = []
all_ga = set(v1_ga_v.keys()) | set(v2_ga_v.keys())
for ga in sorted(all_ga, key=lambda x: (x[0], x[1])):
s1 = v1_ga_v.get(ga, set())
s2 = v2_ga_v.get(ga, set())
if s1 and s2 and s1 != s2:
out.append((ga, sorted(s1), sorted(s2)))
return out
def main() -> int:
root = Path(__file__).resolve().parents[1]
ap = argparse.ArgumentParser(description="V1/V2 fat-jar 依赖对比或 lib multiset 门禁")
ap.add_argument(
"--gate",
action="store_true",
help="运行 multiset 门禁(默认基准 V1 jar,候选为 releases 或 starter/target",
)
ap.add_argument("--baseline-jar", type=Path, default=None, help="门禁基准 fat-jar")
ap.add_argument("--candidate-jar", type=Path, default=None, help="门禁待测 fat-jar")
ap.add_argument(
"--allow-baseline-only",
type=Path,
default=None,
help="允许仅出现在 baseline 的坐标(每行 `g:a:v` 或 `unresolved:name.jar`,可选末尾计数)",
)
ap.add_argument(
"--allow-candidate-only",
type=Path,
default=None,
help="允许仅出现在 candidate 的坐标(格式同上)",
)
args = ap.parse_args()
if args.gate:
b = (
args.baseline_jar
if args.baseline_jar
else root
/ "cw-elevator-application-V1.0.0.20211103"
/ "cw-elevator-application-V1.0.0.20211103.jar"
)
cand = args.candidate_jar
if cand is None:
# 优先本地 package 产物(与当前父 POM / 插件一致);其次历史 releases 归档
cand = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.7.jar"
)
if not cand.is_file():
cand = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.0.jar"
)
if not cand.is_file():
cand = (
root
/ "maven-cw-elevator-application"
/ "releases"
/ "cw-elevator-application-V2.0.6.20260430"
/ "cw-elevator-application-2.0.6.jar"
)
allow_b = args.allow_baseline_only
if allow_b is None:
p = root / "docs" / "testing" / "cw-elevator-fatjar-lib-parity-allow-baseline-only.txt"
allow_b = p if p.is_file() else None
allow_c = args.allow_candidate_only
if allow_c is None:
p = root / "docs" / "testing" / "cw-elevator-fatjar-lib-parity-allow-candidate-only.txt"
allow_c = p if p.is_file() else None
return run_lib_gate(b, cand, allow_b, allow_c)
v1_jar = root / "cw-elevator-application-V1.0.0.20211103" / "cw-elevator-application-V1.0.0.20211103.jar"
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.7.jar"
)
if not v2_jar.is_file():
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.0.jar"
)
if not v2_jar.is_file():
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "releases"
/ "cw-elevator-application-V2.0.6.20260430"
/ "cw-elevator-application-2.0.6.jar"
)
# reactor + dependency:list 时各模块写各自 target/starter 模块输出才是入口 fat-jar 的 runtime 列表
mvn_list = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "v2-maven-deps.txt"
)
if not mvn_list.is_file():
mvn_list = root / "maven-cw-elevator-application" / "target" / "v2-maven-deps.txt"
if not mvn_list.is_file():
mvn_list = Path("/tmp/v2-maven-deps.txt")
out_md = (
root
/ "docs"
/ "testing"
/ "cw-elevator-v1-v2-dependency-diff.md"
)
if not v1_jar.is_file():
print("Missing V1 jar:", v1_jar, file=sys.stderr)
return 1
if not v2_jar.is_file():
print("Missing V2 jar:", v2_jar, file=sys.stderr)
return 1
v1_map = list_outer_nested(v1_jar, "lib/")
v2_prefix = detect_lib_prefix(v2_jar)
v2_map = list_outer_nested(v2_jar, v2_prefix)
v1_by_ga: dict[str, list[str]] = defaultdict(list)
for path, t in v1_map.items():
v1_by_ga[key_ga(path, t)].append(path)
v2_by_ga: dict[str, list[str]] = defaultdict(list)
for path, t in v2_map.items():
v2_by_ga[key_ga(path, t)].append(path)
keys1 = set(v1_by_ga.keys())
keys2 = set(v2_by_ga.keys())
only_v1 = sorted(keys1 - keys2)
only_v2 = sorted(keys2 - keys1)
both = sorted(keys1 & keys2)
mvn_rows: list[tuple[str, str, str, str]] = []
if mvn_list.is_file():
mvn_rows = parse_maven_dependency_list(mvn_list)
mvn_ga = {f"{g}:{a}:{v}" for g, a, v, _ in mvn_rows}
lines: list[str] = []
lines.append("# cw-elevator-application V1 fat-jar 与 V2 fat-jar 依赖差异核对")
lines.append("")
lines.append("**生成方式**:脚本 `scripts/generate_v1_v2_elevator_dependency_diff.py`(可重复执行覆盖本文件)。")
lines.append("")
lines.append("## 样本路径")
lines.append("")
lines.append(f"- **V1**`{v1_jar.relative_to(root)}`")
lines.append(f"- **V2**`{v2_jar.relative_to(root)}`")
lines.append("")
lines.append("| 指标 | V1 | V2 |")
lines.append("|------|----|----|")
lines.append(f"| 嵌套 jar 条目数(lib / BOOT-INF/lib | {len(v1_map)} | {len(v2_map)} |")
lines.append(f"| 解析出唯一坐标 `groupId:artifactId:version` 数 | {len(keys1)} | {len(keys2)} |")
v1_ga_v = collect_ga_versions_from_jar_map(v1_map)
v2_ga_v = collect_ga_versions_from_jar_map(v2_map)
skew_ga = ga_version_skew_rows(v1_ga_v, v2_ga_v)
lines.append(
f"| 同名 GA、两侧均有解析且 version 集合不一致(§2.2.1)条数 | — | **{len(skew_ga)}** |"
)
lines.append(
f"| 与 Maven `dependency:list`runtime)条目数 | — | {len(mvn_rows)} |"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 1. Maven 方式(仅 V2 reactor")
lines.append("")
lines.append(
"在 `maven-cw-elevator-application` 下执行:`mvn -pl cw-elevator-application-starter -am "
"dependency:list -DincludeScope=runtime -Dsort=true "
"-DoutputFile=target/v2-maven-deps.txt`。`-am` 时每个子模块写各自的 `target/`;"
"**§1.1 使用 starter 模块文件**`cw-elevator-application-starter/target/v2-maven-deps.txt`。"
)
lines.append("")
lines.append(
"**说明**:历史 **V1 运行包** 当前仓库无对应 **1.0** 聚合工程可一键 `dependency:list`"
"V1 的 Maven 坐标视图见 **§2 二进制嵌套 JAR 的 pom.properties**。"
)
lines.append("")
lines.append("### 1.1 V2 `dependency:list` 全量(runtime")
lines.append("")
lines.append("| # | groupId | artifactId | version | scope |")
lines.append("|---|---------|--------------|---------|-------|")
for i, (g, a, v, s) in enumerate(mvn_rows, 1):
lines.append(f"| {i} | `{g}` | `{a}` | `{v}` | `{s}` |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 2. 二进制方式(嵌套 JAR + pom.properties")
lines.append("")
lines.append(
f"- **V1**`lib/*.jar`。\n"
f"- **V2**:自动检测为 `{v2_prefix}*.jar`(与 spring-boot-maven-plugin 1.3.x + Boot 1.5 一致时为 `lib/`)。"
)
lines.append("")
lines.append(
"对每个嵌套 jar 读取 `META-INF/maven/**/pom.properties` 得到 `groupId:artifactId:version`"
"无法读取时记为 `?:?:?`(多为无 Maven 元数据的第三方包)。"
)
lines.append("")
lines.append("### 2.1 仅在 V1 出现的坐标(相对 V2 二进制集合)")
lines.append("")
lines.append(f"**共 {len(only_v1)} 项**。")
lines.append("")
lines.append("| groupId:artifactId:version | V1 嵌套路径 |")
lines.append("|------------------------------|-------------|")
for k in only_v1:
paths = ", ".join(f"`{p}`" for p in sorted(v1_by_ga[k]))
lines.append(f"| `{k}` | {paths} |")
lines.append("")
lines.append("### 2.2 仅在 V2 出现的坐标(相对 V1 二进制集合)")
lines.append("")
lines.append(f"**共 {len(only_v2)} 项**。")
lines.append("")
lines.append("| groupId:artifactId:version | V2 嵌套路径 |")
lines.append("|------------------------------|-------------|")
for k in only_v2:
paths = ", ".join(f"`{p}`" for p in sorted(v2_by_ga[k]))
lines.append(f"| `{k}` | {paths} |")
lines.append("")
lines.append("### 2.2.1 同名构件(groupId:artifactId)在 V1 与 V2 中的版本集合差异")
lines.append("")
lines.append(
"由嵌套 jar 的 `pom.properties` 聚合:若同一 **GA** 在 V1、V2 中均能解析出版本,且 **version 集合不同**"
"则单独列出(与 §2.1 / §2.2 中分列的 `g:a:v` 键互为补充)。**不含**仅一侧出现的 GA。"
)
lines.append("")
lines.append(f"**共 {len(skew_ga)} 项**。")
lines.append("")
lines.append("| groupId:artifactId | V1 version(s) | V2 version(s) |")
lines.append("|--------------------|---------------|---------------|")
for (g, a), v1s, v2s in skew_ga:
ga_s = f"`{g}:{a}`"
lines.append(
f"| {ga_s} | `{', '.join(v1s)}` | `{', '.join(v2s)}` |"
)
lines.append("")
lines.append("### 2.3 两边均存在且坐标一致的依赖")
lines.append("")
lines.append(f"**共 {len(both)} 项**(名称版本完全一致)。")
lines.append("")
lines.append("<details>")
lines.append("<summary>展开长表</summary>")
lines.append("")
lines.append("| groupId:artifactId:version |")
lines.append("|------------------------------|")
for k in both:
lines.append(f"| `{k}` |")
lines.append("")
lines.append("</details>")
lines.append("")
lines.append("### 2.4 V2 二进制坐标 vs Maven dependency:list")
lines.append("")
lines.append(
"- **版本字符串不一致**:例如 reactor 在 `dependency:list` 中为 **`2.0-SNAPSHOT`**"
"而 fat-jar 内嵌模块 **`cw-elevator-application-*-2.0.6.jar`** 的 `pom.properties` 为 **`2.0.6`**"
"字符串比对会视为「仅一侧存在」,属**同名构件不同表述**,非缺失依赖。"
)
lines.append(
"- **在 dependency:list 中但不在嵌套 jar 元数据中的**:多为 **仅存在于解析树、与本模块 jar 文件命名不一致**,需对照 §1 表格。"
)
lines.append(
"- **未解析 `unresolved:*`**:见 §3,此类条目不参与坐标相等判断。"
)
lines.append("")
only_mvn = sorted(mvn_ga - keys2)
only_bin = sorted(keys2 - mvn_ga)
lines.append(f"- **仅在 Maven listruntime**{len(only_mvn)}")
lines.append("")
if only_mvn:
lines.append("|坐标|")
lines.append("|----|")
for k in only_mvn[:80]:
lines.append(f"| `{k}` |")
if len(only_mvn) > 80:
lines.append(f"| … 其余 {len(only_mvn) - 80} 项省略 |")
lines.append("")
lines.append(f"- **仅在二进制坐标集合**{len(only_bin)}")
lines.append("")
if only_bin:
lines.append("|坐标|")
lines.append("|----|")
for k in only_bin[:80]:
lines.append(f"| `{k}` |")
if len(only_bin) > 80:
lines.append(f"| … 其余 {len(only_bin) - 80} 项省略 |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 3. 无法解析 pom.properties 的嵌套 JAR(仅列文件名)")
lines.append("")
bad_v1 = [p for p, t in v1_map.items() if t[0] == "?" or t[2] == "?"]
bad_v2 = [p for p, t in v2_map.items() if t[0] == "?" or t[2] == "?"]
lines.append(f"- **V1** 未解析条目:**{len(bad_v1)}**")
for p in sorted(bad_v1)[:50]:
lines.append(f" - `{Path(p).name}`")
if len(bad_v1) > 50:
lines.append(f" - … 省略 {len(bad_v1) - 50}")
lines.append(f"- **V2** 未解析条目:**{len(bad_v2)}**")
for p in sorted(bad_v2)[:50]:
lines.append(f" - `{Path(p).name}`")
if len(bad_v2) > 50:
lines.append(f" - … 省略 {len(bad_v2) - 50}")
lines.append("")
out_md.parent.mkdir(parents=True, exist_ok=True)
out_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("Wrote", out_md)
return 0
if __name__ == "__main__":
sys.exit(main())
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 转发至 maven-cw-elevator-application 内的一键 API 套件脚本。
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec bash "${ROOT}/maven-cw-elevator-application/scripts/run_full_elevator_api_suite.sh" "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 转发至 maven-cw-elevator-application:一键构建 V2、启动 V1/V2、跑完整 API 套件。
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec bash "${ROOT}/maven-cw-elevator-application/scripts/run_v1v2_parity_automated.sh" "$@"
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Scan *.java for block comments in the last N lines (tail noise / orphan */). Read-only."""
from __future__ import annotations
import sys
from pathlib import Path
TAIL_LINES = 50
REPO = Path(__file__).resolve().parent.parent
def main() -> int:
suspicious: list[tuple[Path, str]] = []
with_slash: list[Path] = []
for p in REPO.rglob("*.java"):
sp = str(p)
if "/target/" in sp:
continue
try:
t = p.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
lines = t.splitlines()
if not lines:
continue
tail = "\n".join(lines[-TAIL_LINES:])
if "/*" in tail or "*/" in tail:
with_slash.append(p)
low = tail.lower()
if "location" in low and "/*" in tail:
suspicious.append((p, "Location in tail block"))
if "jd-core" in low or "jd core" in low:
suspicious.append((p, "jd-core in tail"))
if "decompil" in low and "/*" in tail:
suspicious.append((p, "decompil+block in tail"))
# Trailing */ only: last non-empty line is */
nonws = [ln for ln in lines[-20:] if ln.strip()]
if len(nonws) >= 1 and nonws[-1].strip() == "*/" and "/**" not in "\n".join(lines[-5:]):
# might be orphan closer (rare)
if "*/" in tail and tail.count("/*") < tail.count("*/"):
suspicious.append((p, "possible orphan */ at EOF"))
print(f"Scanned under {REPO}")
print(f"Files with /* or */ in last {TAIL_LINES} lines: {len(with_slash)}")
print(f"Flagged suspicious: {len(suspicious)}")
for p, reason in suspicious[:80]:
print(f" {reason}: {p.relative_to(REPO)}")
if len(suspicious) > 80:
print(f" ... and {len(suspicious) - 80} more")
return 0
if __name__ == "__main__":
sys.exit(main())
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Remove JD-Core decompiler noise from Java sources under maven-*:
- Line prefix comments: /* */ and /* N */ / /* N */ (line numbers; spaces around N vary)
- Trailing metadata block: /* Location: ... JD-Core Version: ... */
Does not strip normal Javadoc /** ... */ or arbitrary block comments.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
RE_LINE_EMPTY_PREFIX = re.compile(r"^/\* +\*/\s*")
# JD-Core often emits "/* 90 */" (extra spaces after /* and/or before */)
RE_LINE_NUM_PREFIX = re.compile(r"^/\* *\d+ *\*/\s*")
# Ends with line like " */" (space + */) after "* JD-Core Version: ..."
RE_TAIL_META = re.compile(
r"(?:^|\n)/\* Location:.*?\n\s*\*/\s*",
re.DOTALL,
)
def strip_content(text: str) -> str:
text = text.replace("\r\n", "\n").replace("\r", "\n")
lines = text.split("\n")
stripped = []
for line in lines:
line = RE_LINE_EMPTY_PREFIX.sub("", line)
line = RE_LINE_NUM_PREFIX.sub("", line)
stripped.append(line)
joined = "\n".join(stripped)
joined = RE_TAIL_META.sub("\n", joined)
if joined and not joined.endswith("\n"):
joined += "\n"
return joined
def process_file(path: Path, dry_run: bool) -> bool:
raw = path.read_text(encoding="utf-8", errors="replace")
new = strip_content(raw)
raw_norm = raw.replace("\r\n", "\n").replace("\r", "\n")
if raw_norm and not raw_norm.endswith("\n"):
raw_norm += "\n"
if new == raw_norm:
return False
if not dry_run:
path.write_text(new, encoding="utf-8", newline="\n")
return True
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"roots",
nargs="*",
type=Path,
help="Optional roots (default: all maven-* under repo root next to scripts/)",
)
ap.add_argument("--dry-run", action="store_true", help="Report only, do not write")
args = ap.parse_args()
script_dir = Path(__file__).resolve().parent
repo = script_dir.parent
if args.roots:
roots = [Path(p).resolve() for p in args.roots]
else:
roots = sorted(repo.glob("maven-*"))
roots = [p for p in roots if p.is_dir()]
changed = 0
scanned = 0
for root in roots:
if not root.exists():
print(f"skip missing: {root}", file=sys.stderr)
continue
for path in root.rglob("*.java"):
if "/target/" in str(path).replace("\\", "/"):
continue
scanned += 1
if process_file(path, args.dry_run):
changed += 1
if args.dry_run:
print(f"would update: {path}")
print(f"Scanned: {scanned} Java files under {len(roots)} root(s)")
print(f"{'Would change' if args.dry_run else 'Changed'}: {changed} file(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
前端运行时自动验收(Playwright):
1) 打开页面并等待首屏稳定
2) 采集 console/pageerror/requestfailed
3) 统计重复资源请求(按 URL 去 query 后聚合)
4) 输出 markdown 报告 + 截图
"""
from __future__ import annotations
import argparse
import asyncio
import datetime as dt
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Dict, List, Tuple
from urllib.parse import urlsplit, urlunsplit
from playwright.async_api import Error as PlaywrightError
from playwright.async_api import async_playwright
def _strip_query(url: str) -> str:
p = urlsplit(url)
return urlunsplit((p.scheme, p.netloc, p.path, "", ""))
def _now_tag() -> str:
return dt.datetime.now().strftime("%Y%m%d-%H%M%S")
def _classify_resource(url: str) -> str:
path = urlsplit(url).path.lower()
if path.endswith(".js"):
return "js"
if path.endswith(".css"):
return "css"
if re.search(r"\.(png|jpg|jpeg|gif|svg|ico|webp)$", path):
return "image"
if "/api/" in path or "/elevator/" in path:
return "api"
return "other"
async def run_check(url: str, wait_ms: int, screenshot_path: Path) -> Dict[str, object]:
console_errors: List[str] = []
console_warnings: List[str] = []
page_errors: List[str] = []
failed_requests: List[str] = []
requests: List[Tuple[str, str]] = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(ignore_https_errors=True)
page = await context.new_page()
page.set_default_timeout(5_000)
page.on(
"console",
lambda msg: (
console_errors.append(msg.text)
if msg.type == "error"
else console_warnings.append(msg.text)
if msg.type in {"warning", "warn"}
else None
),
)
page.on("pageerror", lambda err: page_errors.append(str(err)))
def _on_request_failed(req) -> None:
failure = req.failure
if isinstance(failure, dict):
err_text = failure.get("errorText", "unknown")
elif isinstance(failure, str):
err_text = failure
elif failure is None:
err_text = "unknown"
else:
err_text = getattr(failure, "error_text", str(failure))
failed_requests.append(f"{req.url} | {err_text}")
page.on("requestfailed", _on_request_failed)
page.on("requestfinished", lambda req: requests.append((req.method, req.url)))
navigation_mode = "domcontentloaded"
try:
try:
await page.goto(url, wait_until="domcontentloaded", timeout=12_000)
except PlaywrightError:
# 某些历史页面会因脚本阻塞导致 domcontentloaded 不触发,降级到 commit 保证可诊断
navigation_mode = "commit(fallback)"
await page.goto(url, wait_until="commit", timeout=20_000)
await page.wait_for_timeout(wait_ms)
try:
# 避免 full_page 在超长文档上触发超时,先抓取可视区域。
await page.screenshot(path=str(screenshot_path), full_page=False, timeout=15_000)
except Exception:
screenshot_path.write_bytes(b"")
try:
title = await page.title()
except Exception:
title = ""
try:
body_non_empty = bool(await page.evaluate("Boolean(document.body && document.body.innerText && document.body.innerText.trim())"))
except Exception:
body_non_empty = False
try:
app_count = await page.locator("#app *").count()
except Exception:
app_count = -1
except PlaywrightError as e:
await browser.close()
return {
"fatal_error": str(e),
"console_errors": console_errors,
"console_warnings": console_warnings,
"page_errors": page_errors,
"failed_requests": failed_requests,
"requests": requests,
}
await browser.close()
normalized_urls = [_strip_query(u) for _, u in requests]
url_counter = Counter(normalized_urls)
duplicate_urls = [(u, c) for u, c in url_counter.items() if c > 1]
duplicate_urls.sort(key=lambda x: x[1], reverse=True)
resource_counter = Counter(_classify_resource(u) for u in normalized_urls)
return {
"fatal_error": "",
"navigation_mode": navigation_mode,
"title": title,
"body_non_empty": body_non_empty,
"app_count": app_count,
"console_errors": console_errors,
"console_warnings": console_warnings,
"page_errors": page_errors,
"failed_requests": failed_requests,
"requests_total": len(requests),
"requests_unique": len(url_counter),
"resource_counter": dict(resource_counter),
"duplicate_urls": duplicate_urls[:30],
}
def _render_report(url: str, result: Dict[str, object], screenshot_path: Path) -> str:
lines: List[str] = []
lines.append("# 前端运行时自动验收报告")
lines.append("")
lines.append(f"- URL: `{url}`")
lines.append(f"- 时间: `{dt.datetime.now().isoformat(timespec='seconds')}`")
lines.append(f"- 截图: `{screenshot_path}`")
lines.append("")
fatal = str(result.get("fatal_error", "")).strip()
if fatal:
lines.append("## 结论")
lines.append("- **失败**:页面打开阶段异常")
lines.append(f"- 异常: `{fatal}`")
return "\n".join(lines) + "\n"
lines.append("## 页面状态")
lines.append(f"- 导航模式: `{result.get('navigation_mode', '')}`")
lines.append(f"- title: `{result.get('title', '')}`")
lines.append(f"- body 是否非空: `{result.get('body_non_empty', False)}`")
lines.append(f"- `#app` 子节点数: `{result.get('app_count', 0)}`")
lines.append("")
lines.append("## 请求统计")
lines.append(f"- 总请求数: `{result.get('requests_total', 0)}`")
lines.append(f"- 去重后 URL 数: `{result.get('requests_unique', 0)}`")
lines.append(f"- 类型分布: `{json.dumps(result.get('resource_counter', {}), ensure_ascii=False)}`")
lines.append("")
dup = result.get("duplicate_urls", [])
lines.append("## 重复请求(Top")
if dup:
for u, c in dup:
lines.append(f"- `{c}x` {u}")
else:
lines.append("- 无重复 URL 请求")
lines.append("")
def _section(title: str, rows: List[str]) -> None:
lines.append(f"## {title}")
if rows:
for row in rows[:50]:
lines.append(f"- {row}")
else:
lines.append("- 无")
lines.append("")
_section("Console Error", result.get("console_errors", []))
_section("Page Error", result.get("page_errors", []))
_section("Request Failed", result.get("failed_requests", []))
_section("Console Warning", result.get("console_warnings", []))
return "\n".join(lines)
async def _amain() -> int:
parser = argparse.ArgumentParser(description="自动检测前端运行时错误/重复加载")
parser.add_argument("--url", default="http://127.0.0.1:8090/login", help="待检测 URL")
parser.add_argument(
"--wait-ms", type=int, default=5000, help="页面打开后额外等待毫秒数(默认 5000)"
)
parser.add_argument(
"--out-dir",
default="artifacts/frontend-check",
help="报告与截图输出目录(默认 artifacts/frontend-check",
)
args = parser.parse_args()
out_dir = Path(args.out_dir).resolve()
out_dir.mkdir(parents=True, exist_ok=True)
tag = _now_tag()
screenshot = out_dir / f"frontend-{tag}.png"
report = out_dir / f"frontend-{tag}.md"
result = await run_check(args.url, args.wait_ms, screenshot)
report.write_text(_render_report(args.url, result, screenshot), encoding="utf-8")
print(f"[frontend-check] report={report}")
print(f"[frontend-check] screenshot={screenshot}")
if result.get("fatal_error"):
return 2
if result.get("console_errors") or result.get("page_errors") or result.get("failed_requests"):
return 1
return 0
def main() -> int:
try:
return asyncio.run(_amain())
except KeyboardInterrupt:
return 130
except Exception as e:
print(f"[frontend-check] unexpected error: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
V1_DIR="${ROOT_DIR}/cw-elevator-application-V1.0.0.20211103"
V2_DIR="${ROOT_DIR}/maven-cw-elevator-application/deploy/v2-maven"
V2_RELEASES_DIR="${ROOT_DIR}/maven-cw-elevator-application/releases"
V1_JAR="${V1_DIR}/cw-elevator-application-V1.0.0.20211103.jar"
V2_JAR=""
for candidate in $(ls -1t "${V2_RELEASES_DIR}"/v*/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ -f "${candidate}" ]]; then
V2_JAR="${candidate}"
break
fi
done
if [[ -z "${V2_JAR}" ]]; then
for candidate in $(ls -1t "${V2_DIR}"/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ -f "${candidate}" ]]; then
V2_JAR="${candidate}"
break
fi
done
fi
if [[ ! -f "${V1_JAR}" ]]; then
echo "ERROR: V1 jar not found: ${V1_JAR}" >&2
exit 1
fi
if [[ -z "${V2_JAR}" || ! -f "${V2_JAR}" ]]; then
echo "ERROR: V2 jar not found under ${V2_DIR}" >&2
exit 1
fi
python3 - "$V1_DIR" "$V2_DIR" "$V1_JAR" "$V2_JAR" <<'PY'
import subprocess
import sys
from pathlib import Path
v1_dir = Path(sys.argv[1])
v2_dir = Path(sys.argv[2])
v1_jar = Path(sys.argv[3])
v2_jar = Path(sys.argv[4])
files = [
"bootstrap.properties",
"application.properties",
"application-access-control.properties",
]
keys = [
"spring.cloud.consul.host",
"spring.cloud.consul.port",
"spring.cloud.consul.discovery.enabled",
"spring.cloud.consul.config.enabled",
"feign.cwos-portal.name",
"feign.ninca-common.name",
"feign.component-organization.name",
]
def parse_properties(text):
result = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or line.startswith("!"):
continue
if "=" in line:
k, v = line.split("=", 1)
elif ":" in line:
k, v = line.split(":", 1)
else:
continue
result[k.strip()] = v.strip()
return result
def read_external(dir_path, name):
p = dir_path / name
if not p.exists():
return None
return parse_properties(p.read_text(encoding="utf-8", errors="ignore"))
def read_jar(jar_path, name):
try:
out = subprocess.check_output(
["jar", "tf", str(jar_path)],
text=True,
errors="ignore",
).splitlines()
except subprocess.CalledProcessError:
return None
candidates = []
suffix = "/" + name
for item in out:
if item == name or item.endswith(suffix):
candidates.append(item)
if not candidates:
return None
preferred = None
for c in candidates:
if c.startswith("BOOT-INF/classes/"):
preferred = c
break
if preferred is None:
preferred = candidates[0]
try:
content = subprocess.check_output(
["unzip", "-p", str(jar_path), preferred],
text=True,
errors="ignore",
)
except subprocess.CalledProcessError:
return None
return parse_properties(content)
def describe_source(external_map, internal_map, key):
ext_val = None if external_map is None else external_map.get(key)
int_val = None if internal_map is None else internal_map.get(key)
if ext_val is not None:
return "external", ext_val
if int_val is not None:
return "jar-internal", int_val
return "unset", ""
def report_version(tag, base_dir, jar_path):
print(f"\n==== {tag} ====")
print(f"base_dir: {base_dir}")
print(f"jar: {jar_path}")
ext_maps = {name: read_external(base_dir, name) for name in files}
int_maps = {name: read_jar(jar_path, name) for name in files}
print("\n[1] 配置文件存在性")
for name in files:
ext_exists = ext_maps[name] is not None
int_exists = int_maps[name] is not None
print(f"- {name}: external={'Y' if ext_exists else 'N'}, jar={'Y' if int_exists else 'N'}")
print("\n[2] 关键键值最终命中来源(按常见优先级:external > jar")
merged_ext = {}
merged_int = {}
for name in files:
if ext_maps[name]:
merged_ext.update(ext_maps[name])
if int_maps[name]:
merged_int.update(int_maps[name])
for key in keys:
src, val = describe_source(merged_ext, merged_int, key)
print(f"- {key}: source={src}, value={val}")
print("\n[3] 加载顺序结论")
has_any_internal = any(int_maps[name] is not None for name in files)
if has_any_internal:
print("- 该版本具备 jar 内置配置兜底。")
else:
print("- 该版本无 jar 内置配置兜底,依赖 external/远端配置。")
print("- 在未显式传 --spring.config.location 时,外部同名配置通常优先于 jar 内置。")
report_version("V1", v1_dir, v1_jar)
report_version("V2", v2_dir, v2_jar)
print("\n==== 总结对比 ====")
print("- V1: external + jar 内置(有兜底)")
v2_internal = any(read_jar(v2_jar, name) is not None for name in files)
if v2_internal:
print("- V2: external + jar 内置(有兜底)")
else:
print("- V2: external 优先,当前样本无 jar 内置同名配置(无兜底)")
print("- 若 external 与远端配置中心并存,最终以运行时属性源优先级为准。")
PY