mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-12 01:40:30 +08:00
feat: add service config templates and extraction script
Former-commit-id: 1de24b7eb79676d1aba9d799a58c5a753290cf52
This commit is contained in:
@@ -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 中可用。脚本直接调用生产 V2,V2 通过现有 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 即可恢复原有行为。
|
||||
+70
@@ -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
|
||||
+113
@@ -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"
|
||||
```
|
||||
+60
@@ -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
|
||||
+92
@@ -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)
|
||||
+273
@@ -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()
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
-- 租户访客楼层策略:business_id → org_id 数据迁移
|
||||
-- 前提:DDL(tenant_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;
|
||||
+26
@@ -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;
|
||||
+13
@@ -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');
|
||||
+26
@@ -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');
|
||||
Reference in New Issue
Block a user