mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-09 08:20:31 +08:00
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:
Executable
+32
@@ -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
|
||||
@@ -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__
|
||||
@@ -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:
|
||||
Executable
+53
@@ -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
|
||||
Executable
+91
@@ -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"
|
||||
Executable
+82
@@ -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"
|
||||
Executable
+145
@@ -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
|
||||
Executable
+92
@@ -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"
|
||||
Executable
+56
@@ -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"
|
||||
@@ -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()
|
||||
Executable
+65
@@ -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
|
||||
@@ -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
@@ -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
@@ -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}"
|
||||
@@ -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
@@ -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 list(runtime)**:{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())
|
||||
Executable
+4
@@ -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" "$@"
|
||||
Executable
+4
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user