ocdp v1
This commit is contained in:
273
backend/scripts/docker-quick-start.sh
Executable file
273
backend/scripts/docker-quick-start.sh
Executable file
@ -0,0 +1,273 @@
|
||||
#!/bin/bash
|
||||
# ==================================================
|
||||
# OCDP Backend - Docker Compose 快速启动脚本
|
||||
# ==================================================
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 打印带颜色的消息
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 检查 Docker
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker 未安装,请先安装 Docker"
|
||||
echo "访问: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker compose version &> /dev/null; then
|
||||
print_error "Docker Compose 未安装,请先安装 Docker Compose V2"
|
||||
echo "访问: https://docs.docker.com/compose/install/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Docker 环境检查通过"
|
||||
}
|
||||
|
||||
# 检查环境变量文件
|
||||
check_env_file() {
|
||||
if [ ! -f .env ]; then
|
||||
print_warning ".env 文件不存在,正在从 env.example 创建..."
|
||||
cp env.example .env
|
||||
print_success ".env 文件已创建"
|
||||
print_warning "请编辑 .env 文件,配置必要的环境变量(特别是生产环境的密钥)"
|
||||
echo ""
|
||||
read -p "按回车键继续..."
|
||||
else
|
||||
print_success ".env 文件已存在"
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示菜单
|
||||
show_menu() {
|
||||
print_header "OCDP Backend - Docker Compose 快速启动"
|
||||
|
||||
echo "请选择运行模式:"
|
||||
echo ""
|
||||
echo " 1) Mock 模式 (无需数据库,快速测试)"
|
||||
echo " 2) 生产模式 (完整功能,需要数据库)"
|
||||
echo " 3) 开发模式 (热重载,需要数据库)"
|
||||
echo " 4) 查看服务状态"
|
||||
echo " 5) 查看日志"
|
||||
echo " 6) 停止所有服务"
|
||||
echo " 7) 启动 pgAdmin (数据库管理)"
|
||||
echo " 0) 退出"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 启动 Mock 模式
|
||||
start_mock() {
|
||||
print_header "启动 Mock 模式"
|
||||
print_info "正在启动服务..."
|
||||
|
||||
docker compose --profile mock up -d
|
||||
|
||||
echo ""
|
||||
print_success "Mock 模式启动成功!"
|
||||
echo ""
|
||||
print_info "服务访问地址:"
|
||||
echo " 📍 API: http://localhost:8080/api/v1"
|
||||
echo " 📍 Health: http://localhost:8080/health"
|
||||
echo ""
|
||||
print_info "查看日志: docker compose logs -f backend-mock"
|
||||
print_info "停止服务: docker compose down"
|
||||
}
|
||||
|
||||
# 启动生产模式
|
||||
start_production() {
|
||||
print_header "启动生产模式"
|
||||
print_info "正在启动数据库和后端服务..."
|
||||
|
||||
docker compose --profile production up -d
|
||||
|
||||
echo ""
|
||||
print_info "等待服务就绪..."
|
||||
sleep 5
|
||||
|
||||
# 检查服务健康
|
||||
if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
|
||||
print_success "生产模式启动成功!"
|
||||
else
|
||||
print_warning "服务正在启动中,请稍候..."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_info "服务访问地址:"
|
||||
echo " 📍 API: http://localhost:8080/api/v1"
|
||||
echo " 📍 Health: http://localhost:8080/health"
|
||||
echo ""
|
||||
print_info "数据库信息:"
|
||||
echo " 📍 Host: localhost"
|
||||
echo " 📍 Port: 5432"
|
||||
echo " 📍 Database: ocdp"
|
||||
echo ""
|
||||
print_info "查看日志: docker compose logs -f backend"
|
||||
print_info "停止服务: docker compose down"
|
||||
}
|
||||
|
||||
# 启动开发模式
|
||||
start_development() {
|
||||
print_header "启动开发模式"
|
||||
print_info "正在启动开发环境(支持热重载)..."
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||
|
||||
echo ""
|
||||
print_info "等待服务就绪..."
|
||||
sleep 5
|
||||
|
||||
print_success "开发模式启动成功!"
|
||||
echo ""
|
||||
print_info "服务访问地址:"
|
||||
echo " 📍 API: http://localhost:8080/api/v1"
|
||||
echo " 📍 Health: http://localhost:8080/health"
|
||||
echo ""
|
||||
print_info "开发模式特性:"
|
||||
echo " 🔥 支持代码热重载(修改代码自动重启)"
|
||||
echo " 📂 源代码已挂载到容器"
|
||||
echo ""
|
||||
print_info "查看日志: docker compose logs -f backend-dev"
|
||||
print_info "停止服务: docker compose down"
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
show_status() {
|
||||
print_header "服务状态"
|
||||
docker compose ps
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
show_logs() {
|
||||
print_header "查看日志"
|
||||
echo "实时查看日志(按 Ctrl+C 退出)..."
|
||||
echo ""
|
||||
sleep 2
|
||||
docker compose logs -f
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
stop_services() {
|
||||
print_header "停止服务"
|
||||
print_info "正在停止所有服务..."
|
||||
|
||||
docker compose down
|
||||
|
||||
print_success "所有服务已停止"
|
||||
}
|
||||
|
||||
# 启动 pgAdmin
|
||||
start_pgadmin() {
|
||||
print_header "启动 pgAdmin"
|
||||
print_info "正在启动 pgAdmin..."
|
||||
|
||||
docker compose --profile tools up -d pgadmin
|
||||
|
||||
echo ""
|
||||
print_success "pgAdmin 启动成功!"
|
||||
echo ""
|
||||
print_info "访问地址: http://localhost:5050"
|
||||
print_info "登录信息:"
|
||||
echo " 📧 邮箱: admin@ocdp.local"
|
||||
echo " 🔑 密码: admin"
|
||||
echo ""
|
||||
print_info "连接数据库配置:"
|
||||
echo " 📍 Host: postgres"
|
||||
echo " 📍 Port: 5432"
|
||||
echo " 📍 Database: ocdp"
|
||||
echo " 📍 Username: postgres"
|
||||
echo " 📍 Password: postgres"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
# 获取脚本目录
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
# 检查环境
|
||||
check_docker
|
||||
check_env_file
|
||||
|
||||
while true; do
|
||||
show_menu
|
||||
read -p "请选择 [0-7]: " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
start_mock
|
||||
echo ""
|
||||
read -p "按回车键返回菜单..."
|
||||
;;
|
||||
2)
|
||||
start_production
|
||||
echo ""
|
||||
read -p "按回车键返回菜单..."
|
||||
;;
|
||||
3)
|
||||
start_development
|
||||
echo ""
|
||||
read -p "按回车键返回菜单..."
|
||||
;;
|
||||
4)
|
||||
show_status
|
||||
echo ""
|
||||
read -p "按回车键返回菜单..."
|
||||
;;
|
||||
5)
|
||||
show_logs
|
||||
;;
|
||||
6)
|
||||
stop_services
|
||||
echo ""
|
||||
read -p "按回车键返回菜单..."
|
||||
;;
|
||||
7)
|
||||
start_pgadmin
|
||||
echo ""
|
||||
read -p "按回车键返回菜单..."
|
||||
;;
|
||||
0)
|
||||
print_info "再见!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "无效的选择,请重试"
|
||||
sleep 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main
|
||||
|
||||
235
backend/scripts/generate-bootstrap-config.sh
Executable file
235
backend/scripts/generate-bootstrap-config.sh
Executable file
@ -0,0 +1,235 @@
|
||||
#!/bin/bash
|
||||
|
||||
# generate-bootstrap-config.sh
|
||||
# 从 kubeconfig 生成 bootstrap 配置文件
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 OCDP Bootstrap Configuration Generator"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 检查依赖
|
||||
command -v kubectl >/dev/null 2>&1 || { echo "❌ kubectl is required but not installed. Aborting." >&2; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo "❌ jq is required but not installed. Aborting." >&2; exit 1; }
|
||||
|
||||
# 默认输出文件
|
||||
OUTPUT_FILE="${1:-config/bootstrap.json}"
|
||||
|
||||
# 临时文件
|
||||
TMP_FILE=$(mktemp)
|
||||
|
||||
# 创建基础配置结构
|
||||
cat > "$TMP_FILE" <<'EOF'
|
||||
{
|
||||
"enabled": true,
|
||||
"users": [
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123",
|
||||
"email": "admin@example.com"
|
||||
}
|
||||
],
|
||||
"registries": [],
|
||||
"clusters": []
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "📋 请按提示输入信息..."
|
||||
echo ""
|
||||
|
||||
# ===== Registries 配置 =====
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📦 Registry 配置"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
read -p "是否添加 Registry? (y/n) [y]: " ADD_REGISTRY
|
||||
ADD_REGISTRY=${ADD_REGISTRY:-y}
|
||||
|
||||
if [[ "$ADD_REGISTRY" == "y" ]]; then
|
||||
read -p "Registry 名称 [harbor-bwgdi]: " REGISTRY_NAME
|
||||
REGISTRY_NAME=${REGISTRY_NAME:-harbor-bwgdi}
|
||||
|
||||
read -p "Registry URL [https://harbor.bwgdi.com]: " REGISTRY_URL
|
||||
REGISTRY_URL=${REGISTRY_URL:-https://harbor.bwgdi.com}
|
||||
|
||||
read -p "Registry 描述 [BWGDI Harbor Registry]: " REGISTRY_DESC
|
||||
REGISTRY_DESC=${REGISTRY_DESC:-"BWGDI Harbor Registry"}
|
||||
|
||||
read -p "Registry 用户名 [admin]: " REGISTRY_USER
|
||||
REGISTRY_USER=${REGISTRY_USER:-admin}
|
||||
|
||||
read -sp "Registry 密码: " REGISTRY_PASS
|
||||
echo ""
|
||||
|
||||
read -p "是否跳过 SSL 验证? (y/n) [n]: " REGISTRY_INSECURE
|
||||
REGISTRY_INSECURE=${REGISTRY_INSECURE:-n}
|
||||
|
||||
if [[ "$REGISTRY_INSECURE" == "y" ]]; then
|
||||
INSECURE_VALUE="true"
|
||||
else
|
||||
INSECURE_VALUE="false"
|
||||
fi
|
||||
|
||||
# 添加 Registry 到配置
|
||||
TMP_REGISTRY=$(cat <<JSON
|
||||
{
|
||||
"name": "$REGISTRY_NAME",
|
||||
"url": "$REGISTRY_URL",
|
||||
"description": "$REGISTRY_DESC",
|
||||
"username": "$REGISTRY_USER",
|
||||
"password": "$REGISTRY_PASS",
|
||||
"insecure": $INSECURE_VALUE
|
||||
}
|
||||
JSON
|
||||
)
|
||||
|
||||
jq ".registries += [$TMP_REGISTRY]" "$TMP_FILE" > "${TMP_FILE}.tmp" && mv "${TMP_FILE}.tmp" "$TMP_FILE"
|
||||
echo "✅ Registry '$REGISTRY_NAME' 已添加"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ===== Clusters 配置 =====
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "☸️ Kubernetes Cluster 配置"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
read -p "是否从 kubeconfig 导入 Cluster? (y/n) [y]: " ADD_CLUSTER
|
||||
ADD_CLUSTER=${ADD_CLUSTER:-y}
|
||||
|
||||
CLUSTER_INDEX=0
|
||||
|
||||
while [[ "$ADD_CLUSTER" == "y" ]]; do
|
||||
echo ""
|
||||
echo "--- Cluster $(($CLUSTER_INDEX + 1)) ---"
|
||||
|
||||
read -p "Cluster 名称 [cluster$(($CLUSTER_INDEX + 1))]: " CLUSTER_NAME
|
||||
CLUSTER_NAME=${CLUSTER_NAME:-cluster$(($CLUSTER_INDEX + 1))}
|
||||
|
||||
read -p "Cluster 描述: " CLUSTER_DESC
|
||||
|
||||
echo ""
|
||||
echo "📂 请选择数据源:"
|
||||
echo " 1) 从 kubeconfig 文件提取"
|
||||
echo " 2) 手动输入"
|
||||
read -p "选择 [1]: " DATA_SOURCE
|
||||
DATA_SOURCE=${DATA_SOURCE:-1}
|
||||
|
||||
if [[ "$DATA_SOURCE" == "1" ]]; then
|
||||
# 从 kubeconfig 提取
|
||||
read -p "kubeconfig 文件路径 [~/.kube/config]: " KUBECONFIG_PATH
|
||||
KUBECONFIG_PATH=${KUBECONFIG_PATH:-~/.kube/config}
|
||||
KUBECONFIG_PATH="${KUBECONFIG_PATH/#\~/$HOME}"
|
||||
|
||||
if [[ ! -f "$KUBECONFIG_PATH" ]]; then
|
||||
echo "❌ kubeconfig 文件不存在: $KUBECONFIG_PATH"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 列出可用的 contexts
|
||||
echo ""
|
||||
echo "📋 可用的 contexts:"
|
||||
kubectl --kubeconfig="$KUBECONFIG_PATH" config get-contexts
|
||||
echo ""
|
||||
|
||||
read -p "选择 context (留空使用当前 context): " CONTEXT_NAME
|
||||
|
||||
if [[ -n "$CONTEXT_NAME" ]]; then
|
||||
KUBECTL_OPTS="--kubeconfig=$KUBECONFIG_PATH --context=$CONTEXT_NAME"
|
||||
else
|
||||
KUBECTL_OPTS="--kubeconfig=$KUBECONFIG_PATH"
|
||||
fi
|
||||
|
||||
# 提取数据
|
||||
echo "🔍 正在提取 Cluster 配置..."
|
||||
|
||||
CLUSTER_HOST=$(kubectl $KUBECTL_OPTS config view --raw -o jsonpath='{.clusters[0].cluster.server}')
|
||||
CLUSTER_CA=$(kubectl $KUBECTL_OPTS config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')
|
||||
CLUSTER_CERT=$(kubectl $KUBECTL_OPTS config view --raw -o jsonpath='{.users[0].user.client-certificate-data}')
|
||||
CLUSTER_KEY=$(kubectl $KUBECTL_OPTS config view --raw -o jsonpath='{.users[0].user.client-key-data}')
|
||||
|
||||
echo " ✓ Server: $CLUSTER_HOST"
|
||||
echo " ✓ CA Data: ${CLUSTER_CA:0:50}..."
|
||||
echo " ✓ Cert Data: ${CLUSTER_CERT:0:50}..."
|
||||
echo " ✓ Key Data: ${CLUSTER_KEY:0:50}..."
|
||||
|
||||
else
|
||||
# 手动输入
|
||||
read -p "API Server 地址: " CLUSTER_HOST
|
||||
|
||||
echo "请输入 CA 证书数据 (Base64 编码,多行输入以空行结束):"
|
||||
CLUSTER_CA=""
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && break
|
||||
CLUSTER_CA="${CLUSTER_CA}${line}"
|
||||
done
|
||||
|
||||
echo "请输入客户端证书数据 (Base64 编码,多行输入以空行结束):"
|
||||
CLUSTER_CERT=""
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && break
|
||||
CLUSTER_CERT="${CLUSTER_CERT}${line}"
|
||||
done
|
||||
|
||||
echo "请输入客户端密钥数据 (Base64 编码,多行输入以空行结束):"
|
||||
CLUSTER_KEY=""
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && break
|
||||
CLUSTER_KEY="${CLUSTER_KEY}${line}"
|
||||
done
|
||||
fi
|
||||
|
||||
# 添加 Cluster 到配置
|
||||
TMP_CLUSTER=$(cat <<JSON
|
||||
{
|
||||
"name": "$CLUSTER_NAME",
|
||||
"host": "$CLUSTER_HOST",
|
||||
"description": "$CLUSTER_DESC",
|
||||
"caData": "$CLUSTER_CA",
|
||||
"certData": "$CLUSTER_CERT",
|
||||
"keyData": "$CLUSTER_KEY"
|
||||
}
|
||||
JSON
|
||||
)
|
||||
|
||||
jq ".clusters += [$TMP_CLUSTER]" "$TMP_FILE" > "${TMP_FILE}.tmp" && mv "${TMP_FILE}.tmp" "$TMP_FILE"
|
||||
echo "✅ Cluster '$CLUSTER_NAME' 已添加"
|
||||
|
||||
CLUSTER_INDEX=$(($CLUSTER_INDEX + 1))
|
||||
|
||||
read -p "是否继续添加 Cluster? (y/n) [n]: " ADD_CLUSTER
|
||||
ADD_CLUSTER=${ADD_CLUSTER:-n}
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
# ===== 保存配置 =====
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "💾 保存配置"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# 格式化 JSON
|
||||
jq '.' "$TMP_FILE" > "$OUTPUT_FILE"
|
||||
rm "$TMP_FILE"
|
||||
|
||||
echo "✅ Bootstrap 配置已保存到: $OUTPUT_FILE"
|
||||
echo ""
|
||||
|
||||
# 显示配置摘要
|
||||
echo "📊 配置摘要:"
|
||||
echo " - 用户数: $(jq '.users | length' "$OUTPUT_FILE")"
|
||||
echo " - Registry 数: $(jq '.registries | length' "$OUTPUT_FILE")"
|
||||
echo " - Cluster 数: $(jq '.clusters | length' "$OUTPUT_FILE")"
|
||||
echo ""
|
||||
|
||||
echo "🚀 接下来的步骤:"
|
||||
echo " 1. 检查配置文件: cat $OUTPUT_FILE"
|
||||
echo " 2. 启动应用: make run-mock"
|
||||
echo " 3. 验证数据:"
|
||||
echo " curl http://localhost:8080/api/v1/registries"
|
||||
echo " curl http://localhost:8080/api/v1/clusters"
|
||||
echo ""
|
||||
|
||||
echo "✨ 完成!"
|
||||
|
||||
129
backend/scripts/init-db.sql
Normal file
129
backend/scripts/init-db.sql
Normal file
@ -0,0 +1,129 @@
|
||||
-- OCDP Backend PostgreSQL 数据库初始化脚本
|
||||
-- 创建数据库和必要的表结构
|
||||
|
||||
-- ===== Users 表 =====
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
revoked_after TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_revoked_after ON users(revoked_after);
|
||||
|
||||
COMMENT ON TABLE users IS '用户表';
|
||||
COMMENT ON COLUMN users.id IS '用户 ID (UUID)';
|
||||
COMMENT ON COLUMN users.username IS '用户名(唯一)';
|
||||
COMMENT ON COLUMN users.password_hash IS '密码哈希';
|
||||
COMMENT ON COLUMN users.email IS '邮箱';
|
||||
|
||||
-- ===== Clusters 表 =====
|
||||
CREATE TABLE IF NOT EXISTS clusters (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
host TEXT NOT NULL,
|
||||
ca_data TEXT,
|
||||
cert_data TEXT,
|
||||
key_data TEXT,
|
||||
token TEXT,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_clusters_name ON clusters(name);
|
||||
|
||||
COMMENT ON TABLE clusters IS 'Kubernetes 集群表';
|
||||
COMMENT ON COLUMN clusters.id IS '集群 ID (UUID)';
|
||||
COMMENT ON COLUMN clusters.name IS '集群名称(唯一)';
|
||||
COMMENT ON COLUMN clusters.host IS 'Kubernetes API Server URL';
|
||||
COMMENT ON COLUMN clusters.ca_data IS 'CA 证书(加密存储)';
|
||||
COMMENT ON COLUMN clusters.cert_data IS '客户端证书(加密存储)';
|
||||
COMMENT ON COLUMN clusters.key_data IS '客户端密钥(加密存储)';
|
||||
COMMENT ON COLUMN clusters.token IS 'Bearer Token(加密存储)';
|
||||
|
||||
-- ===== Registries 表 =====
|
||||
CREATE TABLE IF NOT EXISTS registries (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
url TEXT NOT NULL,
|
||||
description TEXT,
|
||||
username VARCHAR(255),
|
||||
password TEXT,
|
||||
insecure BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_registries_name ON registries(name);
|
||||
|
||||
COMMENT ON TABLE registries IS 'OCI Registry 表';
|
||||
COMMENT ON COLUMN registries.id IS 'Registry ID (UUID)';
|
||||
COMMENT ON COLUMN registries.name IS 'Registry 名称(唯一)';
|
||||
COMMENT ON COLUMN registries.url IS 'Registry URL';
|
||||
COMMENT ON COLUMN registries.username IS '认证用户名';
|
||||
COMMENT ON COLUMN registries.password IS '认证密码(加密存储)';
|
||||
COMMENT ON COLUMN registries.insecure IS '是否跳过 TLS 验证';
|
||||
|
||||
-- ===== Instances 表 =====
|
||||
CREATE TABLE IF NOT EXISTS instances (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
cluster_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
namespace VARCHAR(255) NOT NULL,
|
||||
registry_id VARCHAR(36) NOT NULL,
|
||||
repository TEXT NOT NULL,
|
||||
chart VARCHAR(255) NOT NULL,
|
||||
version VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
values JSONB,
|
||||
values_yaml TEXT,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
status_reason TEXT,
|
||||
last_operation VARCHAR(50),
|
||||
last_error TEXT,
|
||||
revision INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_cluster FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_registry FOREIGN KEY (registry_id) REFERENCES registries(id) ON DELETE CASCADE,
|
||||
CONSTRAINT unique_cluster_name UNIQUE (cluster_id, name, namespace)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_cluster ON instances(cluster_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_registry ON instances(registry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_name ON instances(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_status ON instances(status);
|
||||
|
||||
COMMENT ON TABLE instances IS 'Helm 应用实例表';
|
||||
COMMENT ON COLUMN instances.id IS '实例 ID (UUID)';
|
||||
COMMENT ON COLUMN instances.cluster_id IS '所属集群 ID';
|
||||
COMMENT ON COLUMN instances.name IS 'Helm Release 名称';
|
||||
COMMENT ON COLUMN instances.namespace IS 'Kubernetes 命名空间';
|
||||
COMMENT ON COLUMN instances.registry_id IS '所属 Registry ID';
|
||||
COMMENT ON COLUMN instances.repository IS 'OCI Repository';
|
||||
COMMENT ON COLUMN instances.chart IS 'Chart 名称';
|
||||
COMMENT ON COLUMN instances.version IS 'Chart 版本';
|
||||
COMMENT ON COLUMN instances.values IS 'Helm Values (JSON 格式)';
|
||||
COMMENT ON COLUMN instances.values_yaml IS 'Helm Values (YAML 格式)';
|
||||
COMMENT ON COLUMN instances.status IS '实例状态';
|
||||
COMMENT ON COLUMN instances.status_reason IS '状态说明';
|
||||
COMMENT ON COLUMN instances.last_operation IS '最后一次操作类型';
|
||||
COMMENT ON COLUMN instances.last_error IS '最近一次错误信息';
|
||||
COMMENT ON COLUMN instances.revision IS 'Helm Release Revision';
|
||||
|
||||
-- ===== 数据库版本表 =====
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version VARCHAR(50) PRIMARY KEY,
|
||||
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE schema_migrations IS '数据库迁移版本记录';
|
||||
|
||||
-- 插入初始版本
|
||||
INSERT INTO schema_migrations (version) VALUES ('v1.0.0')
|
||||
ON CONFLICT (version) DO NOTHING;
|
||||
@ -0,0 +1,12 @@
|
||||
-- Migration: add status_reason / last_operation / last_error columns to instances
|
||||
-- Applies to PostgreSQL
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE instances
|
||||
ADD COLUMN IF NOT EXISTS status_reason TEXT,
|
||||
ADD COLUMN IF NOT EXISTS last_operation VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS last_error TEXT;
|
||||
|
||||
COMMIT;
|
||||
|
||||
85
backend/scripts/quick-start-production.sh
Executable file
85
backend/scripts/quick-start-production.sh
Executable file
@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# OCDP Backend - Production 模式快速启动脚本
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 OCDP Backend - Production 模式快速启动"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# 检查 Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查 docker compose
|
||||
if ! docker compose version &> /dev/null; then
|
||||
echo "❌ docker compose 未安装,请先安装 Docker Compose V2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Docker 环境检查通过"
|
||||
echo ""
|
||||
|
||||
# 获取项目根目录
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )"
|
||||
|
||||
# 启动 PostgreSQL(从根目录)
|
||||
echo "📦 启动 PostgreSQL..."
|
||||
cd "$PROJECT_ROOT"
|
||||
docker compose up -d postgres
|
||||
|
||||
echo "⏳ 等待 PostgreSQL 就绪..."
|
||||
sleep 5
|
||||
|
||||
# 检查 PostgreSQL 是否就绪
|
||||
docker compose exec -T postgres pg_isready -U postgres || {
|
||||
echo "❌ PostgreSQL 启动失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 返回 backend 目录
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
|
||||
echo "✅ PostgreSQL 已就绪"
|
||||
echo ""
|
||||
|
||||
# 设置环境变量
|
||||
export ADAPTER_MODE=production
|
||||
export DATABASE_URL="postgres://postgres:postgres@localhost:5432/ocdp?sslmode=disable"
|
||||
export ENCRYPTION_KEY="default-encryption-key-change-me-32"
|
||||
export JWT_SECRET="your-jwt-secret-key-change-in-production"
|
||||
export PORT=8080
|
||||
|
||||
echo "🔧 环境配置:"
|
||||
echo " - ADAPTER_MODE: $ADAPTER_MODE"
|
||||
echo " - DATABASE_URL: $DATABASE_URL"
|
||||
echo " - PORT: $PORT"
|
||||
echo ""
|
||||
|
||||
# 编译
|
||||
echo "🔨 编译应用..."
|
||||
go build -o bin/ocdp-backend cmd/api/main.go
|
||||
|
||||
echo "✅ 编译完成"
|
||||
echo ""
|
||||
|
||||
# 启动应用
|
||||
echo "🚀 启动 OCDP Backend (Production 模式)..."
|
||||
echo ""
|
||||
echo "📍 服务地址:"
|
||||
echo " - API: http://localhost:8080/api/v1"
|
||||
echo " - Health: http://localhost:8080/health"
|
||||
echo ""
|
||||
echo "📍 数据库管理:"
|
||||
echo " - pgAdmin: http://localhost:5050"
|
||||
echo " Email: admin@ocdp.local"
|
||||
echo " Password: admin"
|
||||
echo ""
|
||||
echo "✨ 按 Ctrl+C 停止服务"
|
||||
echo ""
|
||||
|
||||
./bin/ocdp-backend
|
||||
|
||||
395
backend/scripts/test-all-modes.sh
Executable file
395
backend/scripts/test-all-modes.sh
Executable file
@ -0,0 +1,395 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==================================================
|
||||
# OCDP Backend - 三种模式测试脚本
|
||||
# ==================================================
|
||||
# 测试三种部署模式:
|
||||
# 1. Mock 模式(无 docker compose,backend 热重载)
|
||||
# 2. Production 模式(docker compose 仅依赖服务,backend 热重载)
|
||||
# 3. Production 模式(docker compose 包含 backend)
|
||||
# ==================================================
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_section() {
|
||||
echo ""
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BLUE} $1${NC}"
|
||||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 等待服务启动
|
||||
wait_for_service() {
|
||||
local url=$1
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
log_info "等待服务启动: $url"
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if curl -s -f "$url" > /dev/null 2>&1; then
|
||||
log_success "服务已启动!"
|
||||
return 0
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "服务启动超时"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 测试健康检查
|
||||
test_health() {
|
||||
local mode=$1
|
||||
log_info "测试健康检查..."
|
||||
|
||||
response=$(curl -s http://localhost:8080/health)
|
||||
if echo "$response" | grep -q "healthy"; then
|
||||
log_success "$mode 模式健康检查通过"
|
||||
return 0
|
||||
else
|
||||
log_error "$mode 模式健康检查失败"
|
||||
echo "响应: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试 API
|
||||
test_api() {
|
||||
local mode=$1
|
||||
log_info "测试 API..."
|
||||
|
||||
# 测试注册
|
||||
register_response=$(curl -s -X POST http://localhost:8080/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"testuser'"$RANDOM"'","password":"test123","email":"test@example.com"}')
|
||||
|
||||
if echo "$register_response" | grep -q "id"; then
|
||||
log_success "$mode 模式 API 注册测试通过"
|
||||
else
|
||||
log_warning "$mode 模式 API 注册测试失败(可能用户已存在)"
|
||||
fi
|
||||
|
||||
# 测试登录
|
||||
login_response=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}')
|
||||
|
||||
if echo "$login_response" | grep -q "accessToken"; then
|
||||
log_success "$mode 模式 API 登录测试通过"
|
||||
return 0
|
||||
else
|
||||
log_error "$mode 模式 API 登录测试失败"
|
||||
echo "响应: $login_response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理环境
|
||||
cleanup() {
|
||||
log_info "清理环境..."
|
||||
|
||||
# 停止所有容器
|
||||
docker compose down --remove-orphans > /dev/null 2>&1 || true
|
||||
docker compose --profile backend down --remove-orphans > /dev/null 2>&1 || true
|
||||
docker compose --profile mock down --remove-orphans > /dev/null 2>&1 || true
|
||||
|
||||
# 停止可能在运行的本地进程
|
||||
pkill -f "tmp/main" > /dev/null 2>&1 || true
|
||||
pkill -f "cmd/api/main.go" > /dev/null 2>&1 || true
|
||||
|
||||
sleep 2
|
||||
log_success "清理完成"
|
||||
}
|
||||
|
||||
# ==================================================
|
||||
# 测试 1: Mock 模式(无 docker compose,backend 热重载)
|
||||
# ==================================================
|
||||
test_mode_1() {
|
||||
log_section "测试模式 1: Mock 模式(本地运行,热重载)"
|
||||
|
||||
log_info "配置:"
|
||||
log_info " • 运行方式: 本地 Go"
|
||||
log_info " • 适配器: Mock(内存存储)"
|
||||
log_info " • 数据库: 无需"
|
||||
log_info " • 热重载: 支持(Air)"
|
||||
|
||||
# 检查 Air 是否安装
|
||||
if ! command -v air &> /dev/null; then
|
||||
log_error "Air 未安装,跳过热重载测试"
|
||||
log_info "安装命令: go install github.com/air-verse/air@latest"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 启动 Mock 模式(后台)
|
||||
log_info "启动 Mock 模式..."
|
||||
export ADAPTER_MODE=mock
|
||||
export PORT=8080
|
||||
export JWT_SECRET=test-secret
|
||||
|
||||
# 在后台启动
|
||||
nohup air -c .air.toml > /tmp/ocdp-mock.log 2>&1 &
|
||||
local pid=$!
|
||||
log_info "进程 PID: $pid"
|
||||
|
||||
# 等待服务启动
|
||||
if wait_for_service "http://localhost:8080/health"; then
|
||||
# 测试服务
|
||||
test_health "Mock"
|
||||
test_api "Mock"
|
||||
|
||||
# 停止服务
|
||||
log_info "停止服务..."
|
||||
kill $pid 2>/dev/null || true
|
||||
pkill -f "tmp/main" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
log_success "模式 1 测试完成"
|
||||
return 0
|
||||
else
|
||||
log_error "服务启动失败,查看日志: /tmp/ocdp-mock.log"
|
||||
kill $pid 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ==================================================
|
||||
# 测试 2: Production 模式(docker compose 仅依赖,backend 热重载)
|
||||
# ==================================================
|
||||
test_mode_2() {
|
||||
log_section "测试模式 2: Docker Compose 仅依赖服务(本地 backend 热重载)"
|
||||
|
||||
log_info "配置:"
|
||||
log_info " • 数据库: Docker (PostgreSQL)"
|
||||
log_info " • Backend: 本地 Go"
|
||||
log_info " • 适配器: Production"
|
||||
log_info " • 热重载: 支持(Air)"
|
||||
|
||||
# 检查 Air 是否安装
|
||||
if ! command -v air &> /dev/null; then
|
||||
log_error "Air 未安装,跳过热重载测试"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 启动 PostgreSQL
|
||||
log_info "启动 PostgreSQL 容器..."
|
||||
docker compose up -d postgres
|
||||
|
||||
# 等待数据库就绪
|
||||
log_info "等待 PostgreSQL 启动..."
|
||||
sleep 10
|
||||
|
||||
# 检查数据库健康状态
|
||||
if ! docker compose exec -T postgres pg_isready -U postgres > /dev/null 2>&1; then
|
||||
log_error "PostgreSQL 未就绪"
|
||||
docker compose logs postgres
|
||||
return 1
|
||||
fi
|
||||
log_success "PostgreSQL 已启动"
|
||||
|
||||
# 启动 Backend(后台)
|
||||
log_info "启动 Backend (本地)..."
|
||||
export ADAPTER_MODE=production
|
||||
export PORT=8080
|
||||
export JWT_SECRET=test-secret
|
||||
export ENCRYPTION_KEY=12345678901234567890123456789012
|
||||
export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable"
|
||||
|
||||
# 在后台启动
|
||||
nohup air -c .air.toml > /tmp/ocdp-real.log 2>&1 &
|
||||
local pid=$!
|
||||
log_info "进程 PID: $pid"
|
||||
|
||||
# 等待服务启动
|
||||
if wait_for_service "http://localhost:8080/health"; then
|
||||
# 测试服务
|
||||
test_health "Production (本地 backend)"
|
||||
test_api "Production (本地 backend)"
|
||||
|
||||
# 停止服务
|
||||
log_info "停止 backend..."
|
||||
kill $pid 2>/dev/null || true
|
||||
pkill -f "tmp/main" 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# 停止数据库
|
||||
log_info "停止 PostgreSQL..."
|
||||
docker compose down
|
||||
|
||||
log_success "模式 2 测试完成"
|
||||
return 0
|
||||
else
|
||||
log_error "服务启动失败,查看日志: /tmp/ocdp-real.log"
|
||||
kill $pid 2>/dev/null || true
|
||||
docker compose down
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ==================================================
|
||||
# 测试 3: Production 模式(docker compose 包含 backend)
|
||||
# ==================================================
|
||||
test_mode_3() {
|
||||
log_section "测试模式 3: Docker Compose 完整服务(包含 backend)"
|
||||
|
||||
log_info "配置:"
|
||||
log_info " • 数据库: Docker (PostgreSQL)"
|
||||
log_info " • Backend: Docker 容器"
|
||||
log_info " • 适配器: Production"
|
||||
log_info " • 热重载: 不支持"
|
||||
|
||||
# 确保 .env 文件存在
|
||||
if [ ! -f .env ]; then
|
||||
log_info "创建 .env 文件..."
|
||||
cp env.example .env
|
||||
fi
|
||||
|
||||
# 启动完整服务
|
||||
log_info "启动完整服务(PostgreSQL + Backend)..."
|
||||
docker compose --profile backend up -d
|
||||
|
||||
# 等待服务启动
|
||||
log_info "等待服务启动(可能需要构建镜像,请耐心等待)..."
|
||||
|
||||
# 检查容器状态
|
||||
sleep 15
|
||||
if ! docker compose ps | grep -q "ocdp-backend"; then
|
||||
log_error "Backend 容器未启动"
|
||||
docker compose logs backend
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 等待健康检查通过
|
||||
if wait_for_service "http://localhost:8080/health"; then
|
||||
# 测试服务
|
||||
test_health "Production (Docker)"
|
||||
test_api "Production (Docker)"
|
||||
|
||||
# 查看容器状态
|
||||
log_info "容器状态:"
|
||||
docker compose ps
|
||||
|
||||
# 停止服务
|
||||
log_info "停止服务..."
|
||||
docker compose --profile backend down
|
||||
|
||||
log_success "模式 3 测试完成"
|
||||
return 0
|
||||
else
|
||||
log_error "服务启动失败"
|
||||
log_info "Backend 日志:"
|
||||
docker compose logs backend
|
||||
docker compose --profile backend down
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ==================================================
|
||||
# 主函数
|
||||
# ==================================================
|
||||
main() {
|
||||
log_section "OCDP Backend - 三种模式自动化测试"
|
||||
|
||||
log_info "项目目录: $(pwd)"
|
||||
log_info "测试时间: $(date)"
|
||||
|
||||
# 检查是否在项目根目录
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
log_error "请在项目根目录运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 初始清理
|
||||
cleanup
|
||||
|
||||
# 运行测试
|
||||
local failed=0
|
||||
|
||||
# 测试模式 1
|
||||
if test_mode_1; then
|
||||
log_success "✅ 模式 1 测试通过"
|
||||
else
|
||||
log_error "❌ 模式 1 测试失败"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
cleanup
|
||||
sleep 3
|
||||
|
||||
# 测试模式 2
|
||||
if test_mode_2; then
|
||||
log_success "✅ 模式 2 测试通过"
|
||||
else
|
||||
log_error "❌ 模式 2 测试失败"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
cleanup
|
||||
sleep 3
|
||||
|
||||
# 测试模式 3
|
||||
if test_mode_3; then
|
||||
log_success "✅ 模式 3 测试通过"
|
||||
else
|
||||
log_error "❌ 模式 3 测试失败"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
cleanup
|
||||
|
||||
# 总结
|
||||
log_section "测试总结"
|
||||
|
||||
if [ $failed -eq 0 ]; then
|
||||
log_success "🎉 所有测试通过!"
|
||||
log_info ""
|
||||
log_info "三种部署模式总结:"
|
||||
log_info ""
|
||||
log_info "1️⃣ Mock 模式(开发)"
|
||||
log_info " 命令: make dev-mock"
|
||||
log_info " 特点: 无需数据库,快速启动,热重载"
|
||||
log_info ""
|
||||
log_info "2️⃣ Real 模式(开发 + 数据库)"
|
||||
log_info " 命令: docker compose up -d && make dev-real"
|
||||
log_info " 特点: PostgreSQL 容器,Backend 本地热重载"
|
||||
log_info ""
|
||||
log_info "3️⃣ Production 模式(生产)"
|
||||
log_info " 命令: docker compose --profile backend up -d"
|
||||
log_info " 特点: 完全容器化,适合生产环境"
|
||||
exit 0
|
||||
else
|
||||
log_error "❌ $failed 个测试失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main
|
||||
|
||||
Reference in New Issue
Block a user