feat: multi-customer architecture + Z-Park support + Gitea migration scripts
Multi-customer config system: - CUSTOMER env var selects customer (tianpu/zpark) - customers/tianpu/config.yaml — Tianpu branding, collectors, features - customers/zpark/config.yaml — Z-Park branding, Sungrow collector - GET /api/v1/branding endpoint for customer-specific branding - main.py loads customer config for app title, CORS, logging - Collector manager filters by customer's enabled collectors Z-Park (中关村医疗器械园) support: - Sungrow iSolarCloud API collector (sungrow_collector.py) - Z-Park device definitions (10 inverters, 8 combiner boxes, 22+ buildings) - Z-Park TOU pricing config (Beijing 2026 rates) - Z-Park seed script (seed_zpark.py) Gitea migration scripts (Mac Studio → labmac3): - 5 migration scripts + README in scripts/gitea-migration/ - Creates 3-repo structure: ems-core, tp-ems, zpark-ems Version: v1.0.0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
scripts/gitea-migration/01_export_mac_studio.sh
Normal file
32
scripts/gitea-migration/01_export_mac_studio.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Run this ON Mac Studio (SSH into 100.108.180.60 first)
|
||||
# Usage: ssh duwenbo@100.108.180.60 'bash -s' < 01_export_mac_studio.sh
|
||||
|
||||
echo "=== Exporting Gitea from Mac Studio ==="
|
||||
|
||||
# Find Gitea container name
|
||||
CONTAINER=$(docker ps --filter "ancestor=gitea/gitea" --format "{{.Names}}" | head -1)
|
||||
if [ -z "$CONTAINER" ]; then
|
||||
CONTAINER=$(docker ps --format "{{.Names}}" | grep -i gitea | head -1)
|
||||
fi
|
||||
echo "Gitea container: $CONTAINER"
|
||||
|
||||
# Create backup directory
|
||||
mkdir -p ~/gitea-backup
|
||||
cd ~/gitea-backup
|
||||
|
||||
# Method 1: Try gitea dump
|
||||
echo "Attempting gitea dump..."
|
||||
docker exec $CONTAINER gitea dump -c /data/gitea/conf/app.ini -f /tmp/gitea-dump.zip 2>/dev/null
|
||||
docker cp $CONTAINER:/tmp/gitea-dump.zip ./gitea-dump.zip 2>/dev/null
|
||||
|
||||
# Method 2: Copy data volume directly
|
||||
echo "Copying Gitea data volume..."
|
||||
docker cp $CONTAINER:/data/gitea ./gitea-data
|
||||
|
||||
echo "=== Export complete ==="
|
||||
echo "Files in ~/gitea-backup/:"
|
||||
ls -la ~/gitea-backup/
|
||||
echo ""
|
||||
echo "Next step: Transfer to labmac3:"
|
||||
echo " scp -r ~/gitea-backup duwenbo@192.168.1.77:/opt/"
|
||||
44
scripts/gitea-migration/02_setup_labmac3.sh
Normal file
44
scripts/gitea-migration/02_setup_labmac3.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Run this ON labmac3 (SSH into 192.168.1.77 first)
|
||||
# Usage: ssh duwenbo@192.168.1.77 'bash -s' < 02_setup_labmac3.sh
|
||||
|
||||
echo "=== Setting up Gitea on labmac3 ==="
|
||||
|
||||
# Create directories
|
||||
sudo mkdir -p /opt/gitea/data
|
||||
sudo chown -R $(whoami):$(id -gn) /opt/gitea
|
||||
|
||||
# Create docker-compose.yml
|
||||
cat > /opt/gitea/docker-compose.yml << 'COMPOSE'
|
||||
version: '3'
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=sqlite3
|
||||
- GITEA__server__ROOT_URL=http://192.168.1.77:3300/
|
||||
- GITEA__server__HTTP_PORT=3000
|
||||
ports:
|
||||
- "3300:3000"
|
||||
- "2222:22"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
restart: unless-stopped
|
||||
COMPOSE
|
||||
|
||||
echo "=== Docker Compose file created ==="
|
||||
cat /opt/gitea/docker-compose.yml
|
||||
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. If restoring from Mac Studio backup:"
|
||||
echo " cp -r /opt/gitea-backup/gitea-data/* /opt/gitea/data/"
|
||||
echo ""
|
||||
echo "2. Start Gitea:"
|
||||
echo " cd /opt/gitea && docker compose up -d"
|
||||
echo ""
|
||||
echo "3. Verify:"
|
||||
echo " curl http://localhost:3300/api/v1/version"
|
||||
60
scripts/gitea-migration/03_restore_data.sh
Normal file
60
scripts/gitea-migration/03_restore_data.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Run this ON labmac3 after transferring backup
|
||||
# Usage: ssh duwenbo@192.168.1.77 'bash -s' < 03_restore_data.sh
|
||||
|
||||
echo "=== Restoring Gitea data on labmac3 ==="
|
||||
|
||||
cd /opt/gitea
|
||||
|
||||
# Stop Gitea if running
|
||||
docker compose down 2>/dev/null
|
||||
|
||||
# Restore data from backup
|
||||
if [ -d "/opt/gitea-backup/gitea-data" ]; then
|
||||
echo "Restoring from data volume backup..."
|
||||
cp -r /opt/gitea-backup/gitea-data/* ./data/ 2>/dev/null
|
||||
# Fix the ROOT_URL in app.ini to point to new IP
|
||||
if [ -f "./data/conf/app.ini" ]; then
|
||||
sed -i 's|ROOT_URL.*=.*|ROOT_URL = http://192.168.1.77:3300/|' ./data/conf/app.ini
|
||||
sed -i 's|SSH_DOMAIN.*=.*|SSH_DOMAIN = 192.168.1.77|' ./data/conf/app.ini
|
||||
echo "Updated ROOT_URL and SSH_DOMAIN in app.ini"
|
||||
fi
|
||||
elif [ -f "/opt/gitea-backup/gitea-dump.zip" ]; then
|
||||
echo "Restoring from gitea dump..."
|
||||
unzip /opt/gitea-backup/gitea-dump.zip -d /tmp/gitea-restore
|
||||
# Copy repos and database
|
||||
cp -r /tmp/gitea-restore/repos/* ./data/gitea/repositories/ 2>/dev/null
|
||||
cp /tmp/gitea-restore/gitea-db.sql ./data/ 2>/dev/null
|
||||
fi
|
||||
|
||||
# Fix permissions
|
||||
sudo chown -R 1000:1000 ./data
|
||||
|
||||
# Start Gitea
|
||||
docker compose up -d
|
||||
|
||||
echo "Waiting for Gitea to start..."
|
||||
sleep 10
|
||||
|
||||
# Verify
|
||||
echo "=== Verification ==="
|
||||
curl -s http://localhost:3300/api/v1/version
|
||||
echo ""
|
||||
curl -s http://localhost:3300/api/v1/repos/search?limit=5 | python3 -c "
|
||||
import sys,json
|
||||
try:
|
||||
data=json.load(sys.stdin)
|
||||
repos = data.get('data', data) if isinstance(data, dict) else data
|
||||
print(f'Repos found: {len(repos)}')
|
||||
for r in repos[:5]:
|
||||
name = r.get('full_name', r.get('name', '?'))
|
||||
print(f' {name}')
|
||||
except: print('Could not parse response')
|
||||
" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "=== Gitea should now be accessible at: ==="
|
||||
echo " http://192.168.1.77:3300/"
|
||||
echo ""
|
||||
echo "If this is a fresh install (no backup), create admin:"
|
||||
echo " docker exec -it gitea gitea admin user create --admin --username tianpu --password 'TianpuGit2026!' --email admin@tianpu.com"
|
||||
27
scripts/gitea-migration/04_update_developer_remotes.sh
Normal file
27
scripts/gitea-migration/04_update_developer_remotes.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Run this on each developer's machine
|
||||
# Usage: cd tianpu-ems && bash path/to/04_update_developer_remotes.sh
|
||||
|
||||
echo "=== Updating Git remotes to labmac3 ==="
|
||||
|
||||
OLD_ORIGIN=$(git remote get-url origin 2>/dev/null)
|
||||
echo "Current origin: $OLD_ORIGIN"
|
||||
|
||||
# Update origin to labmac3
|
||||
git remote set-url origin http://192.168.1.77:3300/tianpu/tianpu-ems.git
|
||||
|
||||
# Keep old Mac Studio as backup remote
|
||||
git remote remove mac-studio 2>/dev/null
|
||||
git remote add mac-studio "$OLD_ORIGIN" 2>/dev/null
|
||||
|
||||
NEW_ORIGIN=$(git remote get-url origin)
|
||||
echo "New origin: $NEW_ORIGIN"
|
||||
|
||||
# Test connectivity
|
||||
echo ""
|
||||
echo "Testing connection..."
|
||||
git ls-remote origin HEAD 2>&1 | head -3
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "You can now: git pull origin main"
|
||||
76
scripts/gitea-migration/05_create_repos.sh
Normal file
76
scripts/gitea-migration/05_create_repos.sh
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# Run this AFTER Gitea is running on labmac3
|
||||
# Creates the 3-repo structure: ems-core, tp-ems, zpark-ems
|
||||
# Usage: bash 05_create_repos.sh
|
||||
|
||||
GITEA_URL="http://192.168.1.77:3300"
|
||||
ADMIN_USER="tianpu"
|
||||
ADMIN_PASS="TianpuGit2026!"
|
||||
|
||||
echo "=== Creating 3-repo structure on Gitea ==="
|
||||
|
||||
# Get admin token
|
||||
TOKEN=$(curl -s -X POST "$GITEA_URL/api/v1/users/$ADMIN_USER/tokens" \
|
||||
-u "$ADMIN_USER:$ADMIN_PASS" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"setup-$(date +%s)\",\"scopes\":[\"all\"]}" | python -c "import sys,json; print(json.load(sys.stdin).get('sha1',''))")
|
||||
|
||||
echo "Token: ${TOKEN:0:8}..."
|
||||
|
||||
# Create ems-core repo
|
||||
echo ""
|
||||
echo "Creating ems-core repo..."
|
||||
curl -s -X POST "$GITEA_URL/api/v1/orgs/tianpu/repos" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "ems-core",
|
||||
"description": "EMS标准产品核心代码 - 共享后端/前端/数据模型",
|
||||
"private": false,
|
||||
"default_branch": "main"
|
||||
}' | python -c "import sys,json; d=json.load(sys.stdin); print(f' Created: {d.get(\"full_name\",\"ERROR\")}') if 'id' in d else print(f' {d.get(\"message\",d)}')"
|
||||
|
||||
# Create tp-ems repo
|
||||
echo ""
|
||||
echo "Creating tp-ems repo..."
|
||||
curl -s -X POST "$GITEA_URL/api/v1/orgs/tianpu/repos" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "tp-ems",
|
||||
"description": "天普大兴园区EMS - 客户定制项目",
|
||||
"private": false,
|
||||
"default_branch": "main"
|
||||
}' | python -c "import sys,json; d=json.load(sys.stdin); print(f' Created: {d.get(\"full_name\",\"ERROR\")}') if 'id' in d else print(f' {d.get(\"message\",d)}')"
|
||||
|
||||
# Create zpark-ems repo
|
||||
echo ""
|
||||
echo "Creating zpark-ems repo..."
|
||||
curl -s -X POST "$GITEA_URL/api/v1/orgs/tianpu/repos" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "zpark-ems",
|
||||
"description": "中关村医疗器械园EMS - 客户定制项目",
|
||||
"private": false,
|
||||
"default_branch": "main"
|
||||
}' | python -c "import sys,json; d=json.load(sys.stdin); print(f' Created: {d.get(\"full_name\",\"ERROR\")}') if 'id' in d else print(f' {d.get(\"message\",d)}')"
|
||||
|
||||
# Add all developers as collaborators to all repos
|
||||
echo ""
|
||||
echo "Adding collaborators..."
|
||||
for REPO in ems-core tp-ems zpark-ems; do
|
||||
for USER in duwenbo hanbing zhangshiyue wangliwei yangruixiao; do
|
||||
curl -s -o /dev/null -X PUT "$GITEA_URL/api/v1/repos/tianpu/$REPO/collaborators/$USER" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"permission": "write"}'
|
||||
done
|
||||
echo " $REPO: all 5 developers added"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== All 3 repos created ==="
|
||||
echo " $GITEA_URL/tianpu/ems-core"
|
||||
echo " $GITEA_URL/tianpu/tp-ems"
|
||||
echo " $GITEA_URL/tianpu/zpark-ems"
|
||||
15
scripts/gitea-migration/README.md
Normal file
15
scripts/gitea-migration/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Gitea 迁移指南:Mac Studio → labmac3
|
||||
|
||||
## 执行顺序
|
||||
|
||||
1. `01_export_mac_studio.sh` — 在Mac Studio上导出Gitea数据
|
||||
2. 手动传输:`scp -r ~/gitea-backup duwenbo@192.168.1.77:/opt/`
|
||||
3. `02_setup_labmac3.sh` — 在labmac3上部署Gitea容器
|
||||
4. `03_restore_data.sh` — 恢复数据并验证
|
||||
5. `04_update_developer_remotes.sh` — 各开发者更新Git远程地址
|
||||
6. `05_create_repos.sh` — 创建3个仓库(ems-core, tp-ems, zpark-ems)
|
||||
|
||||
## 注意事项
|
||||
- 所有SSH操作需要手动输入密码
|
||||
- 迁移前确保labmac3已安装Docker
|
||||
- 迁移完成后保留Mac Studio Gitea 1-2周作为备份
|
||||
189
scripts/seed_zpark.py
Normal file
189
scripts/seed_zpark.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""种子数据 - 中关村医疗器械园光伏设备、告警规则、碳排放因子、电价配置"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.core.database import async_session, engine
|
||||
from app.models.device import Device, DeviceType, DeviceGroup
|
||||
from app.models.alarm import AlarmRule
|
||||
from app.models.carbon import EmissionFactor
|
||||
from app.models.pricing import ElectricityPricing, PricingPeriod
|
||||
|
||||
|
||||
# Path to device definitions
|
||||
DEVICES_JSON = os.path.join(os.path.dirname(__file__), "..", "customers", "zpark", "devices.json")
|
||||
PRICING_JSON = os.path.join(os.path.dirname(__file__), "..", "customers", "zpark", "pricing.json")
|
||||
|
||||
|
||||
async def seed():
|
||||
with open(DEVICES_JSON, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
async with async_session() as session:
|
||||
# =================================================================
|
||||
# 1. 设备类型
|
||||
# =================================================================
|
||||
for dt in config["device_types"]:
|
||||
# Check if type already exists (may overlap with tianpu seed)
|
||||
existing = await session.execute(
|
||||
select(DeviceType).where(DeviceType.code == dt["code"])
|
||||
)
|
||||
if existing.scalar_one_or_none() is None:
|
||||
session.add(DeviceType(
|
||||
code=dt["code"],
|
||||
name=dt["name"],
|
||||
icon=dt.get("icon"),
|
||||
data_fields=dt.get("data_fields"),
|
||||
))
|
||||
await session.flush()
|
||||
|
||||
# =================================================================
|
||||
# 2. 设备分组 (hierarchical)
|
||||
# =================================================================
|
||||
group_name_to_id = {}
|
||||
|
||||
async def create_groups(groups, parent_id=None):
|
||||
for g in groups:
|
||||
grp = DeviceGroup(
|
||||
name=g["name"],
|
||||
parent_id=parent_id,
|
||||
location=g.get("location"),
|
||||
description=g.get("description"),
|
||||
)
|
||||
session.add(grp)
|
||||
await session.flush()
|
||||
group_name_to_id[g["name"]] = grp.id
|
||||
if "children" in g:
|
||||
await create_groups(g["children"], parent_id=grp.id)
|
||||
|
||||
await create_groups(config["device_groups"])
|
||||
|
||||
# =================================================================
|
||||
# 3. 设备
|
||||
# =================================================================
|
||||
devices = []
|
||||
for d in config["devices"]:
|
||||
group_id = group_name_to_id.get(d.get("group"))
|
||||
device = Device(
|
||||
name=d["name"],
|
||||
code=d["code"],
|
||||
device_type=d["device_type"],
|
||||
group_id=group_id,
|
||||
model=d.get("model"),
|
||||
manufacturer=d.get("manufacturer"),
|
||||
rated_power=d.get("rated_power"),
|
||||
location=d.get("location", ""),
|
||||
protocol=d.get("protocol", "http_api"),
|
||||
connection_params=d.get("connection_params"),
|
||||
collect_interval=d.get("collect_interval", 900),
|
||||
status="offline",
|
||||
is_active=True,
|
||||
)
|
||||
devices.append(device)
|
||||
session.add_all(devices)
|
||||
await session.flush()
|
||||
|
||||
# =================================================================
|
||||
# 4. 碳排放因子 (光伏减排)
|
||||
# =================================================================
|
||||
# Check if PV generation factor already exists
|
||||
existing_factor = await session.execute(
|
||||
select(EmissionFactor).where(EmissionFactor.energy_type == "pv_generation")
|
||||
)
|
||||
if existing_factor.scalar_one_or_none() is None:
|
||||
session.add(EmissionFactor(
|
||||
name="华北电网光伏减排因子",
|
||||
energy_type="pv_generation",
|
||||
factor=0.8843,
|
||||
unit="kWh",
|
||||
scope=2,
|
||||
region="north_china",
|
||||
source="等量替代电网电力",
|
||||
year=2023,
|
||||
))
|
||||
await session.flush()
|
||||
|
||||
# =================================================================
|
||||
# 5. 告警规则 (逆变器监控)
|
||||
# =================================================================
|
||||
alarm_rules = [
|
||||
AlarmRule(
|
||||
name="逆变器功率过低告警",
|
||||
device_type="sungrow_inverter",
|
||||
data_type="power",
|
||||
condition="lt",
|
||||
threshold=1.0,
|
||||
duration=1800,
|
||||
severity="warning",
|
||||
notify_channels=["app", "wechat"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="逆变器通信中断告警",
|
||||
device_type="sungrow_inverter",
|
||||
data_type="power",
|
||||
condition="eq",
|
||||
threshold=0.0,
|
||||
duration=3600,
|
||||
severity="critical",
|
||||
notify_channels=["app", "sms", "wechat"],
|
||||
is_active=True,
|
||||
),
|
||||
AlarmRule(
|
||||
name="逆变器过温告警",
|
||||
device_type="sungrow_inverter",
|
||||
data_type="temperature",
|
||||
condition="gt",
|
||||
threshold=70.0,
|
||||
duration=120,
|
||||
severity="major",
|
||||
notify_channels=["app", "sms"],
|
||||
is_active=True,
|
||||
),
|
||||
]
|
||||
session.add_all(alarm_rules)
|
||||
|
||||
# =================================================================
|
||||
# 6. 电价配置
|
||||
# =================================================================
|
||||
if os.path.exists(PRICING_JSON):
|
||||
with open(PRICING_JSON, "r", encoding="utf-8") as f:
|
||||
pricing_config = json.load(f)
|
||||
|
||||
pricing = ElectricityPricing(
|
||||
name=pricing_config["name"],
|
||||
energy_type=pricing_config.get("energy_type", "electricity"),
|
||||
pricing_type=pricing_config.get("pricing_type", "tou"),
|
||||
is_active=True,
|
||||
)
|
||||
session.add(pricing)
|
||||
await session.flush()
|
||||
|
||||
for period in pricing_config.get("periods", []):
|
||||
session.add(PricingPeriod(
|
||||
pricing_id=pricing.id,
|
||||
period_name=period["name"],
|
||||
start_time=period["start"],
|
||||
end_time=period["end"],
|
||||
price_per_unit=period["price"],
|
||||
))
|
||||
|
||||
await session.commit()
|
||||
print("Z-Park seed data created successfully!")
|
||||
|
||||
# Print summary
|
||||
dev_count = len(config["devices"])
|
||||
group_count = len(group_name_to_id)
|
||||
print(f" - Device types: {len(config['device_types'])}")
|
||||
print(f" - Device groups: {group_count}")
|
||||
print(f" - Devices: {dev_count}")
|
||||
print(f" - Alarm rules: {len(alarm_rules)}")
|
||||
print(f" - Pricing periods: {len(pricing_config.get('periods', []))}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in New Issue
Block a user