feat: add service config templates and extraction script

Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
This commit is contained in:
反编译工作区
2026-05-01 19:38:01 +08:00
parent 3175b7074b
commit 8b15445328
2433 changed files with 8322164 additions and 1604 deletions
@@ -0,0 +1,120 @@
# org_id 策略修复 — 生产环境部署验证
## 发布包内容
```
org-policy-fix-test-20260501/
├── cw-elevator-application-V1.0.0.20211103.jar # V2 JAR(含 @RibbonClients 修复,命名同 V1
├── scripts/
│ ├── verify_org_policy_fix.py # 7 用例验证脚本
│ └── stub_org_service.py # 组织服务 HTTP 桩(本地测试用)
├── config/
│ └── v2-local-config.properties # 本地测试配置参考
├── sql/
│ ├── tenant_visitor_floor_policy_v2.sql # DDL(加 org_id 列)
│ └── tenant_visitor_floor_policy_migrate_org_id.sql # 数据迁移(business_id → org_id
└── docs/
└── 2026-05-01-org-policy-verify-manual.md # 详细操作手册
```
---
## 部署验证流程
### 第一步:DDL 上线(DBA 执行,仅一次)
在生产库 `cw-elevator-application` 执行:
```bash
mysql -h <生产MySQL主机> -P <端口> -u <用户> -p cw-elevator-application < sql/tenant_visitor_floor_policy_v2.sql
```
验证:
```sql
-- 确认 org_id 列已添加,uk_org_building 约束已创建
SELECT COLUMN_NAME, COLUMN_KEY, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'cw-elevator-application'
AND TABLE_NAME = 'tenant_visitor_floor_policy'
ORDER BY ORDINAL_POSITION;
```
### 第二步:数据迁移(DBA 执行,按公司逐行)
```sql
-- 1. 查看可用公司节点
SELECT o.ID, o.NAME
FROM component-organization.cw_is_organization o
WHERE o.BUSINESS_ID = '2524639890ba4f2cba9ba1a4eeaa4015'
AND o.IS_DEL = 0
ORDER BY o.NAME;
-- 2. 为广发基金迁移(示例)
UPDATE cw-elevator-application.tenant_visitor_floor_policy
SET org_id = '<广发基金 org_id>'
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
-- 3. 为其他公司新增策略(模板)
INSERT INTO cw-elevator-application.tenant_visitor_floor_policy
(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, remark, created_at, updated_at)
VALUES
(REPLACE(UUID(),'-',''), '<公司 org_id>', NULL, 'INTERSECT_ALLOWLIST',
'["<zone_id>"]', NULL, 1, 1, '', UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
```
### 第三步:部署 V2 JAR(运维执行)
```bash
# 1. 停旧 V2
/path/to/stop.sh
# 2. 替换 JAR
cp cw-elevator-application-V1.0.0.20211103.jar /path/to/deploy/
# 3. 确认配置文件不变(application.properties / bootstrap.properties 沿用现有)
# 4. 启动
/path/to/start.sh
```
### 第四步:运行验证脚本
```bash
# 安装依赖
pip3 install requests pymysql
# 运行(根据生产环境修改 DB 地址和电梯 URL)
cd scripts/
python3 verify_org_policy_fix.py \
--elevator-base-url http://<生产V2地址>:<端口>
```
**注意:** 生产环境不需要桩服务——`ninca-common``ninca-common-component-organization` 服务在生产 Consul 或 Dubbo 中可用。脚本直接调用生产 V2V2 通过现有 Feign/Ribbon 链路访问真实组织服务。
---
## 验证用例清单
| ID | 场景 | 期望 |
|----|------|------|
| T1 | 公司有策略 allow=[28F] | passRule 返回仅 [28F] |
| T2 | 公司无策略 | passRule 返回 floorList 全集 |
| T3 | allow 含无效 zoneId | fail 76260533 |
| T4 | 被访人多组织,其一有策略 | 命中该策略 |
| T5 | enabled=0 | 等同无策略 |
| T6 | 调用方传 floorIds,但策略优先 | 策略 allow 覆盖调用方值 |
| T7 | 广发基金迁移后生效 | 仅 [28F] |
---
## 回滚方案
```sql
-- 回滚 DDL
ALTER TABLE tenant_visitor_floor_policy DROP INDEX uk_org_building;
ALTER TABLE tenant_visitor_floor_policy DROP COLUMN org_id;
ALTER TABLE tenant_visitor_floor_policy ADD UNIQUE KEY uk_biz_building (business_id, building_id);
```
部署旧 JAR 即可恢复原有行为。
@@ -0,0 +1,70 @@
server.port=18081
logging.path=/tmp/elevator-logs
logging.file=cw-elevator
spring.cloud.consul.host=192.168.3.12
spring.cloud.consul.port=8500
spring.cloud.consul.enabled=true
spring.redis.host=127.0.0.1
spring.redis.port=6380
spring.redis.database=5
spring.redis.timeout=3000
spring.redis.pool.max-active=10
spring.redis.pool.max-idle=1
spring.redis.pool.max-wait=10
spring.redis.pool.min-idle=0
spring.shardingsphere.datasource.names=ds0
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://192.168.3.12:3307/cw-elevator-application?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
spring.shardingsphere.datasource.ds0.connection-timeout=60000
spring.shardingsphere.datasource.ds0.maximum-pool-size=20
spring.shardingsphere.datasource.ds0.minimum-idle=5
spring.shardingsphere.datasource.ds0.max-lifetime=1765000
spring.shardingsphere.datasource.ds0.auto-commit=true
spring.shardingsphere.datasource.ds0.pool-name=ds0-pool
spring.shardingsphere.props.sql.show=false
spring.shardingsphere.sharding.default-data-source-name=ds0
cloudwalk.event.bootstrap-servers=127.0.0.1:9092
cloudwalk.event.group-id=cw-elevator-local
feign.device.name=cwos-portal
feign.resource.name=cwos-portal
feign.cwos-portal.name=cwos-portal
feign.davinci-portal.name=cwos-portal
feign.ninca-crk-std.name=ninca-crk-std
feign.component-organization.name=ninca-common-component-organization
feign.ninca-common.name=ninca-common
feign.mqtt.name=cloudwalk-device-thirdparty
feign.hystrix.enable=true
feign.okhttp.enable=true
ribbon.okhttp.enabled=true
ribbon.ReadTimeout=10000
ribbon.ConnectTimeout=10000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
spring.mvc.throw-exception-if-no-handler-found=true
spring.messages.basename=access-control
mybatis.mapper-locations=classpath*:cn/cloudwalk/elevator/**/*.xml
mybatis.config-location=classpath:mapper/mybatis-config.xml
cloudwalk.serial.enable=true
cloudwalk.serial.serial-type=redis
cloudwalk.serial.serial-redis-key=CLOUDWALK-ACS-SERIAL-KEY
management.health.redis.enabled=false
management.health.db.enabled=false
cloudwalk.datafield.enable=true
cloudwalk.datafield.securityKey=d4b2aabc97394a12a27fc3cca6cd9ba1
intelligent.lock.enable=true
intelligent.lock.config.default-wait-time=10000
lockWatchdogTimeout=21000
person.name.space=recordEvent
elevator.application.key=xinghewan
elevator.application.time=600
elevator.application.keyA=5B7DEF88FF04
ninca-crk-std.ip=127.0.0.1:16106
sendRecord.boolean=false
floor.building.id=605560539791228928
spring.redis.password=1qaz!QAZ
ninca-common-component-organization.ribbon.listOfServers=127.0.0.1:18082
ninca-common-component-organization.ribbon.ServerListRefreshInterval=5000
ninca-common.ribbon.listOfServers=127.0.0.1:18082
ninca-common.ribbon.ServerListRefreshInterval=5000
@@ -0,0 +1,113 @@
# org_id 策略修复 — 人工验证操作手册
## 前置条件
- V2 JAR 已构建:`cw-elevator-application-2.0.9.jar`
- 配置文件:`/tmp/v2-redis-fix.properties`
- Redis Docker`v2-test-redis`(端口 6380,密码 `1qaz!QAZ`
- 桩服务脚本:`stub_org_service.py`
---
## 步骤 1:启动 V2 电梯应用
打开**终端 1**,执行:
```bash
/usr/lib/jvm/java-8-openjdk-amd64/bin/java \
-jar /media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/源码/maven-cw-elevator-application/cw-elevator-application-starter/target/cw-elevator-application-2.0.9.jar \
--spring.config.location=file:/tmp/v2-redis-fix.properties
```
等待约 **35 秒**,看到 `Started ElevatorApplication` 后验证:
```bash
curl http://127.0.0.1:18081/health
```
期望输出:`{"status":"UP"}`
---
## 步骤 2:启动组织服务桩
打开**终端 2**,执行:
```bash
python3 /media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/源码/maven-cw-elevator-application/tools/stub_org_service.py
```
验证:
```bash
curl http://127.0.0.1:18082/health
```
期望输出:`{"status":"UP"}`
---
## 步骤 3:运行验证脚本
打开**终端 3**,执行:
```bash
cd /media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/星河湾星中星/源码/maven-cw-elevator-application/tools/visitor_floor_verification
python3 scripts/verify_org_policy_fix.py --elevator-base-url http://127.0.0.1:18081
```
期望输出:
```
=== Phase 2: run 7 cases ===
[T1] 有策略→allow替换floorList → ✅
[T2] 无策略→floorList → ✅
[T3] allow含无效zone→拒绝 (76260533) → ✅
[T4] 多组织命中第一个策略 → ✅
[T5] enabled=0等同无策略 → ✅
[T6] UC-02策略优先 → ✅
[T7] 广发基金迁移验证 → ✅
Passed: 7/7
```
报告文件:`report/org-policy-fix-verify-YYYYMMDD-HHMMSS.json`
---
## 步骤 4:停止服务
```bash
# 终端1 按 Ctrl+C
# 终端2 按 Ctrl+C
```
---
## 故障排查
| 症状 | 原因 | 解决 |
|------|------|------|
| V2 启动报 `RedisConnectionException` | Redis 密码未配置或端口错误 | 确认 `v2-test-redis` 运行中:`docker ps \| grep v2-test-redis` |
| V2 报 `UnknownHostException: mysql_01` | ShardingSphere 数据源未覆盖 | 确认使用了 `--spring.config.location=file:/tmp/v2-redis-fix.properties` |
| 桩服务端口被占用 | 上次未正常退出 | `pkill -f stub_org_service` |
| 验证脚本 `Connection refused` | V2 或桩未启动 | 检查终端 1/2 的服务日志 |
| DB 连接失败 | MySQL 密码错误 | 确认 `192.168.3.12:3307 root/123456` 可达 |
---
## 本地 Docker 基础设施
| 服务 | 容器名 | 端口 | 说明 |
|------|--------|------|------|
| Redis | `v2-test-redis` | 6380 | 密码 `1qaz!QAZ` |
| Kafka | `ybs-kafka` | 9092 | 无认证 |
| Consul | — | 192.168.3.12:8500 | 远程 |
| MySQL | — | 192.168.3.12:3307 | root/123456 |
如需重建 Redis
```bash
docker rm -f v2-test-redis
docker run -d --name v2-test-redis -p 6380:6379 redis:7-alpine --requirepass "1qaz!QAZ"
```
@@ -0,0 +1,60 @@
#!/bin/bash
# 组织服务楼层数据诊断
# 用法: bash diag_person_floors.sh
HOST="127.0.0.1"
PORT="18081"
BUSINESS_ID="2524639890ba4f2cba9ba1a4eeaa4015"
PERSONS=(
"1060601019894960128|陈国辉|1403艾斯"
"1090779433129840640|王姣|1405一博"
"1072908835884208128|秦夏|广发基金"
)
echo "=== 电梯健康检查 ==="
curl -s "http://${HOST}:${PORT}/health" 2>/dev/null || echo "FAIL"
for p in "${PERSONS[@]}"; do
IFS='|' read -r PID PNAME PORG <<< "$p"
echo ""
echo "=== $PNAME ($PID) [$PORG] ==="
# 调电梯 addVisitor 接口(会内部调组织服务获取人员详情)
RESP=$(curl -s -X POST "http://${HOST}:${PORT}/elevator/person/add/visitor" \
-H "Content-Type: application/json" \
-H "businessid: ${BUSINESS_ID}" \
-d "{
\"personId\": \"${PID}\",
\"visitorId\": \"diag_$(date +%s)\",
\"floorIds\": [],
\"begVisitorTime\": $(date +%s)000,
\"endVisitorTime\": $(($(date +%s) + 86400))000
}")
CODE=$(echo "$RESP" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('code',''))" 2>/dev/null)
MSG=$(echo "$RESP" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('message',''))" 2>/dev/null)
echo " code=$CODE"
echo " message=$MSG"
echo " raw=$RESP"
done
echo ""
echo "=== 试探:用特定 floorId 绕过策略 ==="
# 看看哪些 zoneId 能被组织服务接受
for zone in "605560541473144832" "605560545117995008" "605560542752407552" "605560545449345024"; do
RESP=$(curl -s -X POST "http://${HOST}:${PORT}/elevator/person/add/visitor" \
-H "Content-Type: application/json" \
-H "businessid: ${BUSINESS_ID}" \
-d "{
\"personId\": \"1060601019894960128\",
\"visitorId\": \"zone_test_${zone}\",
\"floorIds\": [\"${zone}\"],
\"begVisitorTime\": $(date +%s)000,
\"endVisitorTime\": $(($(date +%s) + 86400))000
}")
CODE=$(echo "$RESP" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('code',''))" 2>/dev/null)
MSG=$(echo "$RESP" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('message',''))" 2>/dev/null)
echo " zone=$zone → code=$CODE msg=$MSG"
done
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""组织服务桩 — 模拟 ninca-common-component-organization 的 /component/person/detail 端点"""
from flask import Flask, request, jsonify
app = Flask(__name__)
# 被访人数据映射:personId → { floorList, organizationIds }
HOST_DATA = {
"1060601250460012544": { # 丘文明 — 1403艾斯(122devices)
"name": "丘文明",
"floorList": ["605560542353948672", "605560541473144832", "605560545117995008"], # 12F, 6F, 28F
"organizationIds": [
"72fb65ec5de94201b909a98b8bae1892", # 1403艾斯
],
},
"1090914042800263168": { # 陈美全 — 1405一博环保(116devices)
"name": "陈美全",
"floorList": ["605560542752407552", "605560543834537984"], # 15F, 20F
"organizationIds": [
"2095de3d541f44eba686c78fda68336f", # 1405一博环保
],
},
"964454497399468032": { # 蒙海文 — 广发基金(279devices)
"name": "蒙海文",
"floorList": [
"605560545117995008", # 28F
"605560545449345024", # 30F
"605560545596145664", # 31F
"605560545738752000", # 32F
"605560545893941248", # 33F
],
"organizationIds": [
"488b8ad049bb43408a6fbcc50bcb89ac", # 广发基金
],
},
}
@app.route("/component/person/detail", methods=["POST"])
def person_detail():
data = request.get_json(force=True, silent=True) or {}
person_id = data.get("id", "")
host = HOST_DATA.get(person_id)
if not host:
return jsonify({
"success": False,
"code": "76260531",
"message": f"person not found: {person_id}",
"data": None,
})
return jsonify({
"success": True,
"code": "0",
"message": "ok",
"data": {
"id": person_id,
"businessId": "2524639890ba4f2cba9ba1a4eeaa4015",
"name": host["name"],
"floorList": host["floorList"],
"organizationIds": host["organizationIds"],
},
})
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "UP"})
@app.route("/sysetting/zone/page", methods=["POST"])
def zone_page():
return jsonify({
"success": True,
"code": "0",
"data": {
"totalRows": 1,
"currentPage": 1,
"pageSize": 10,
"datas": [{
"id": "605560545117995008",
"zoneId": "605560545117995008",
"parentId": "605560539791228928",
}],
},
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=18082, debug=False)
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""org_id 策略修复 — 无鉴权验证脚本"""
import argparse
import json
import os
import sys
import time
from datetime import datetime
from typing import Any, Dict, List, Optional
import requests
# ===== 配置常量 =====
DB_CONFIG = {
"host": "192.168.3.12",
"port": 3307,
"user": "root",
"password": "123456",
"db_org": "component-organization",
"db_elevator": "cw-elevator-application",
}
BUSINESS_ID = "2524639890ba4f2cba9ba1a4eeaa4015"
ORG_1403 = "72fb65ec5de94201b909a98b8bae1892"
ORG_1405 = "2095de3d541f44eba686c78fda68336f"
ORG_GUANGFA = "488b8ad049bb43408a6fbcc50bcb89ac"
HOST_1403 = "1060601250460012544" # 丘文明 (1403艾斯, 122 devices)
HOST_1405 = "1090914042800263168" # 陈美全 (1405一博环保, 116 devices)
HOST_GUANGFA = "964454497399468032" # 蒙海文 (广发基金, 279 devices)
VISITOR_IDS = [
HOST_1403 + "_t1", HOST_1405 + "_t2", HOST_1403 + "_t3",
HOST_1403 + "_t4", HOST_1403 + "_t5", HOST_1403 + "_t6",
HOST_GUANGFA + "_t7",
]
ZONE_12F = "605560542353948672" # 12F (0x0C from code_elevator_area)
ZONE_4F = "605560541473144832" # 6F (0x06, alternate floor for T6)
ZONE_28F = "605560545117995008" # 28F (0x1C, 广发基金)
ZONE_INVALID = "605560540000000000" # non-existent zone
OK_CODES = {"0", "200", "53013538"} # 53013538=策略通过但图库绑定失败(访客非真实人员)
TEST_CASES = [
{"id":"T1","name":"有策略→allow替换floorList","host_id":HOST_1403,"visitor_id":VISITOR_IDS[0],"policy_id":"policy_t1_1403","expected_pass":True,"expected_floors":[ZONE_12F]},
{"id":"T2","name":"无策略→floorList","host_id":HOST_1405,"visitor_id":VISITOR_IDS[1],"policy_id":None,"expected_pass":True,"expected_floors":None},
{"id":"T3","name":"allow含无效zone→拒绝","host_id":HOST_1403,"visitor_id":VISITOR_IDS[2],"policy_id":"policy_t3_invalid","expected_pass":False,"expected_code":"76260533"},
{"id":"T4","name":"多组织命中第一个策略","host_id":HOST_1403,"visitor_id":VISITOR_IDS[3],"policy_id":"policy_t1_1403","expected_pass":True,"expected_floors":[ZONE_12F]},
{"id":"T5","name":"enabled=0等同无策略","host_id":HOST_1403,"visitor_id":VISITOR_IDS[4],"policy_id":"policy_t5_disabled","expected_pass":True,"expected_floors":None},
{"id":"T6","name":"UC-02策略优先","host_id":HOST_1403,"visitor_id":VISITOR_IDS[5],"policy_id":"policy_t1_1403","expected_pass":True,"expected_floors":[ZONE_12F],"floor_ids_override":[ZONE_4F]},
{"id":"T7","name":"广发基金迁移验证","host_id":HOST_GUANGFA,"visitor_id":VISITOR_IDS[6],"policy_id":"policy_t7_guangfa","expected_pass":True,"expected_floors":[ZONE_28F]},
]
def parse_args():
p = argparse.ArgumentParser(description="org_id 策略修复验证")
p.add_argument("--elevator-base-url", default="http://127.0.0.1:18081")
p.add_argument("--skip-db", action="store_true")
return p.parse_args()
def health_check(base_url):
try:
r = requests.get(f"{base_url}/health", timeout=10)
ok = r.status_code == 200
print(f"[HEALTH] {base_url} -> {r.status_code} {'OK' if ok else 'FAIL'}")
return ok
except Exception as e:
print(f"[HEALTH] {base_url} -> ERROR: {e}")
return False
def get_db_conn():
import pymysql # 延迟导入,--skip-db 模式无需安装
return pymysql.connect(
host=DB_CONFIG["host"], port=DB_CONFIG["port"],
user=DB_CONFIG["user"], password=DB_CONFIG["password"],
database=DB_CONFIG["db_elevator"],
charset="utf8mb4", autocommit=True,
)
def execute_sql(sql, params=None):
conn = get_db_conn()
try:
with conn.cursor() as cur:
cur.execute(sql, params)
finally:
conn.close()
def prepare_test_data():
policies = [
("policy_t1_1403", ORG_1403, f'["{ZONE_12F}"]', 1),
("policy_t3_invalid", ORG_1403, f'["{ZONE_12F}","{ZONE_INVALID}"]', 1),
("policy_t5_disabled", ORG_1403, f'["{ZONE_12F}"]', 0),
]
for pid, oid, zones_json, enabled in policies:
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id=%s", (pid,))
execute_sql(
"INSERT INTO tenant_visitor_floor_policy "
"(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at) "
"VALUES (%s, %s, NULL, 'INTERSECT_ALLOWLIST', %s, NULL, %s, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000)",
(pid, oid, zones_json, enabled),
)
print(f" INSERT policy {pid} org={oid} enabled={enabled}")
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id='policy_t7_guangfa'")
execute_sql(
"INSERT INTO tenant_visitor_floor_policy "
"(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at) "
"VALUES (%s, %s, NULL, 'INTERSECT_ALLOWLIST', %s, NULL, 1, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000)",
('policy_t7_guangfa', ORG_GUANGFA, '["605560545117995008","605560545449345024","605560545596145664","605560545738752000","605560545893941248","605560546036547584","605560546242068480","605560546401452032","605560546552446976","605560546711830528"]'),
)
print(f" INSERT policy_t7_guangfa org={ORG_GUANGFA} (28F-38F)")
def cleanup_test_data():
for pid in ["policy_t1_1403", "policy_t3_invalid", "policy_t5_disabled", "policy_t7_guangfa"]:
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id=%s", (pid,))
print(f" DELETE {pid}")
def build_noauth_headers():
return {"Content-Type": "application/json", "businessid": BUSINESS_ID}
def now_ms():
return int(time.time() * 1000)
def tomorrow_ms():
return int((time.time() + 86400) * 1000)
def call_add_visitor(base_url, person_id, visitor_id, floor_ids=None):
body = {
"personId": person_id, "visitorId": visitor_id,
"floorIds": floor_ids if floor_ids is not None else [],
"begVisitorTime": now_ms(), "endVisitorTime": tomorrow_ms(),
}
try:
r = requests.post(f"{base_url}/elevator/person/add/visitor", json=body, headers=build_noauth_headers(), timeout=30)
return {"http_status": r.status_code, "body": r.json() if r.headers.get("content-type","").startswith("application/json") else r.text}
except Exception as e:
return {"http_status": 0, "error": str(e)}
def call_passrule_image(base_url, visitor_id):
body = {"personId": visitor_id}
try:
r = requests.post(f"{base_url}/elevator/passRule/image", json=body, headers=build_noauth_headers(), timeout=30)
return {"http_status": r.status_code, "body": r.json() if r.headers.get("content-type","").startswith("application/json") else r.text}
except Exception as e:
return {"http_status": 0, "error": str(e)}
def extract_zone_ids(passrule_response):
try:
datas = passrule_response["body"]["data"]["datas"]
return [d["zoneId"] for d in datas if "zoneId" in d]
except (KeyError, TypeError):
return []
def run_case(base_url, case, skip_db=False):
cid = case["id"]
print(f"\n[{cid}] {case['name']}")
floor_ids = case.get("floor_ids_override")
pid = case.get("policy_id")
if pid and cid == "T3":
if skip_db:
print(" [SKIP-DB] 请 DBA 手动执行: DELETE FROM tenant_visitor_floor_policy WHERE id='policy_t1_1403'")
else:
execute_sql("DELETE FROM tenant_visitor_floor_policy WHERE id='policy_t1_1403'")
print(" [DB] 临时删除 policy_t1_1403")
result = {"id": cid, "name": case["name"]}
r = call_add_visitor(base_url, case["host_id"], case["visitor_id"], floor_ids)
body = r.get("body") if isinstance(r.get("body"), dict) else {}
result["add_visitor"] = {
"http_status": r.get("http_status"),
"success": body.get("success"),
"code": body.get("code"),
"message": body.get("message"),
"error": r.get("error"),
}
av = result["add_visitor"]
business_ok = av["http_status"] == 200 and str(av.get("code", "")) in OK_CODES
if case["expected_pass"]:
if business_ok:
pr = call_passrule_image(base_url, case["visitor_id"])
actual_zones = extract_zone_ids(pr)
result["passrule_image"] = {"zones": actual_zones}
expected = case.get("expected_floors")
if expected is not None:
match = set(actual_zones) == set(expected)
result["floor_match"] = match
result["passed"] = match
print(f" add/visitor OK, floors actual={actual_zones} expected={expected} match={match}")
else:
result["passed"] = True
print(f" add/visitor OK, floors={actual_zones}")
else:
result["passed"] = False
print(f" expected success but got code={av.get('code')} msg={av.get('message')}")
else:
expected_code = case.get("expected_code")
actual_code = str(av.get("code", ""))
result["passed"] = (not business_ok) and (actual_code == expected_code)
print(f" expected fail code={expected_code} actual={actual_code} passed={result['passed']}")
if cid == "T3":
if skip_db:
print(" [SKIP-DB] 请 DBA 手动恢复 policy_t1_1403 (见 test_data_prepare.sql)")
else:
execute_sql(
"INSERT INTO tenant_visitor_floor_policy "
"(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at) "
"VALUES ('policy_t1_1403', %s, NULL, 'INTERSECT_ALLOWLIST', %s, NULL, 1, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000)",
(ORG_1403, f'["{ZONE_12F}"]'),
)
print(" [DB] 恢复 policy_t1_1403")
return result
def generate_report(results, base_url):
passed = sum(1 for r in results if r.get("passed"))
return {
"test": "org_id policy fix verification",
"timestamp": datetime.now().isoformat(),
"elevator_url": base_url,
"mode": "noauth-probe",
"business_id": BUSINESS_ID,
"summary": {"total": len(results), "passed": passed, "failed": len(results) - passed},
"results": results,
}
def main():
args = parse_args()
base = args.elevator_base_url.rstrip("/")
if not health_check(base):
print("FATAL: elevator not reachable")
sys.exit(1)
if not args.skip_db:
print("\n=== Phase 1: prepare ===")
prepare_test_data()
print(f"\n=== Phase 2: run {len(TEST_CASES)} cases ===")
results = [run_case(base, c, args.skip_db) for c in TEST_CASES]
if not args.skip_db:
print("\n=== Phase 3: cleanup ===")
cleanup_test_data()
report = generate_report(results, base)
report_path = f"report/org-policy-fix-verify-{datetime.now().strftime('%Y%m%d-%H%M%S')}.json"
os.makedirs("report", exist_ok=True)
with open(report_path, "w", encoding="utf-8") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"\n=== Report: {report_path} ===")
print(f"Passed: {report['summary']['passed']}/{report['summary']['total']}")
for r in results:
print(f" {'OK' if r.get('passed') else 'FAIL'} [{r['id']}] {r['name']}")
sys.exit(0 if report["summary"]["failed"] == 0 else 1)
if __name__ == "__main__":
main()
@@ -0,0 +1,31 @@
-- 租户访客楼层策略:business_id → org_id 数据迁移
-- 前提:DDLtenant_visitor_floor_policy_v2.sql)已执行
-- 执行方式:人工确认 org_id 对应关系后逐行执行
-- 1. 列出所有公司级组织节点(在 component-organization 库执行,供确认)
-- SELECT o.ID, o.NAME, o.PARENT_ID
-- FROM `component-organization`.cw_is_organization o
-- WHERE o.BUSINESS_ID = '2524639890ba4f2cba9ba1a4eeaa4015'
-- AND o.IS_DEL = 0
-- ORDER BY o.NAME;
USE cw-elevator-application;
-- 2. 为现有策略行填入 org_id(示例:广发基金)
-- 请先确认 NAME 匹配正确,再执行
-- UPDATE tenant_visitor_floor_policy
-- SET org_id = '<广发基金的 org_id>',
-- business_id = NULL
-- WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
-- 3. 为其他公司新增策略行(模板)
-- INSERT INTO tenant_visitor_floor_policy
-- (id, org_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, remark, created_at, updated_at)
-- VALUES
-- (REPLACE(UUID(),'-',''), '<公司 org_id>', 'INTERSECT_ALLOWLIST',
-- '["<zone_id>"]', NULL, 1, 1, '', UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
-- 4. 验证迁移结果
SELECT id, org_id, business_id, policy_type, allow_zone_ids, enabled
FROM tenant_visitor_floor_policy
ORDER BY org_id;
@@ -0,0 +1,26 @@
-- 租户访客楼层策略:org_id 粒度修复
-- 执行顺序:先 DDL → 数据迁移 → 发应用包
-- 回滚:DROP INDEX uk_org_building, DROP COLUMN org_id, ADD UNIQUE KEY uk_biz_building (business_id, building_id)
USE `cw-elevator-application`;
-- 1. 新增 org_id 列
ALTER TABLE tenant_visitor_floor_policy
ADD COLUMN org_id VARCHAR(32) NULL COMMENT '组织节点ID(cw_is_organization.ID)'
AFTER business_id;
-- 2. 替换唯一约束(business_id → org_id
ALTER TABLE tenant_visitor_floor_policy
DROP INDEX uk_biz_building,
ADD UNIQUE KEY uk_org_building (org_id, building_id);
-- 3. 标记 business_id 为废弃
ALTER TABLE tenant_visitor_floor_policy
MODIFY COLUMN business_id VARCHAR(64) NULL COMMENT 'DEPRECATED: 已废弃,以 org_id 为准';
-- 验证
SELECT COLUMN_NAME, COLUMN_KEY, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'cw-elevator-application'
AND TABLE_NAME = 'tenant_visitor_floor_policy'
ORDER BY ORDINAL_POSITION;
@@ -0,0 +1,13 @@
-- ============================================================
-- org_id 策略修复 — 测试数据清理(验证完成后执行)
-- 目标库:192.168.3.12:3307 / cw-elevator-application
-- 执行:mysql -h 192.168.3.12 -P 3307 -u root -p123456 cw-elevator-application < test_data_cleanup.sql
-- ============================================================
DELETE FROM tenant_visitor_floor_policy WHERE id IN ('policy_t1_1403', 'policy_t3_invalid', 'policy_t5_disabled');
UPDATE tenant_visitor_floor_policy SET org_id = NULL WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
-- 验证清理结果(应返回空集或 org_id=NULL
SELECT id, org_id, enabled FROM tenant_visitor_floor_policy
WHERE id IN ('policy_t1_1403', 'policy_t3_invalid', 'policy_t5_disabled', 'gf_vstr_policy_guangfa_fund_001x');
@@ -0,0 +1,26 @@
DELETE FROM tenant_visitor_floor_policy WHERE id IN ('policy_t1_1403', 'policy_t3_invalid', 'policy_t5_disabled');
INSERT INTO tenant_visitor_floor_policy
(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at)
VALUES
('policy_t1_1403', '72fb65ec5de94201b909a98b8bae1892', NULL, 'INTERSECT_ALLOWLIST',
'["db18ffc1346a44fcba96fad0fd2d7d3a"]', NULL, 1, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
INSERT INTO tenant_visitor_floor_policy
(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at)
VALUES
('policy_t3_invalid', '72fb65ec5de94201b909a98b8bae1892', NULL, 'INTERSECT_ALLOWLIST',
'["db18ffc1346a44fcba96fad0fd2d7d3a","605560540000000000"]', NULL, 1, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
INSERT INTO tenant_visitor_floor_policy
(id, org_id, business_id, policy_type, allow_zone_ids, building_id, enabled, policy_version, created_at, updated_at)
VALUES
('policy_t5_disabled', '72fb65ec5de94201b909a98b8bae1892', NULL, 'INTERSECT_ALLOWLIST',
'["db18ffc1346a44fcba96fad0fd2d7d3a"]', NULL, 0, 1, UNIX_TIMESTAMP(NOW())*1000, UNIX_TIMESTAMP(NOW())*1000);
UPDATE tenant_visitor_floor_policy SET org_id = '488b8ad049bb43408a6fbcc50bcb89ac'
WHERE id = 'gf_vstr_policy_guangfa_fund_001x';
SELECT id, org_id, enabled, allow_zone_ids
FROM tenant_visitor_floor_policy
WHERE id IN ('policy_t1_1403', 'policy_t3_invalid', 'policy_t5_disabled', 'gf_vstr_policy_guangfa_fund_001x');