生产编排移除内置MinIO图片存储全面改走阿里云OSS
This commit is contained in:
+6
-4
@@ -40,16 +40,18 @@ LOG_LEVEL=info
|
|||||||
LOG_INTEGRATION_LEVEL=warn
|
LOG_INTEGRATION_LEVEL=warn
|
||||||
LOG_RETENTION_DAYS=30
|
LOG_RETENTION_DAYS=30
|
||||||
|
|
||||||
# 文件存储:服务器迁移期间设 dual;校验通过、停止 MinIO 前改为 oss。
|
# 文件存储:生产使用阿里云 OSS(生产编排已移除 MinIO,仅本地开发使用)。
|
||||||
STORAGE_MODE=minio
|
# STORAGE_MODE 取值 minio | dual | oss;dual 仅在 MinIO→OSS 迁移/回滚期间使用。
|
||||||
|
STORAGE_MODE=oss
|
||||||
|
# 以下 MinIO 配置仅在 STORAGE_MODE=dual/minio(迁移回滚场景)时使用。
|
||||||
STORAGE_ENDPOINT=http://minio:9000
|
STORAGE_ENDPOINT=http://minio:9000
|
||||||
STORAGE_BUCKET=order-site
|
STORAGE_BUCKET=order-site
|
||||||
STORAGE_ACCESS_KEY_ID=change-me-storage-access-key
|
STORAGE_ACCESS_KEY_ID=change-me-storage-access-key
|
||||||
STORAGE_SECRET_ACCESS_KEY=change-me-storage-secret-key
|
STORAGE_SECRET_ACCESS_KEY=change-me-storage-secret-key
|
||||||
STORAGE_REGION=us-east-1
|
STORAGE_REGION=us-east-1
|
||||||
STORAGE_MAX_UPLOAD_SIZE_MB=10
|
STORAGE_MAX_UPLOAD_SIZE_MB=10
|
||||||
# 阿里云 OSS S3 兼容端点(不要填写 bucket 前缀)
|
# 阿里云 OSS S3 兼容端点:ECS 与桶同地域时用内网地址(免流量费、低延迟)
|
||||||
STORAGE_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
STORAGE_OSS_ENDPOINT=https://oss-cn-hangzhou-internal.aliyuncs.com
|
||||||
STORAGE_OSS_BUCKET=change-me-oss-bucket
|
STORAGE_OSS_BUCKET=change-me-oss-bucket
|
||||||
STORAGE_OSS_ACCESS_KEY_ID=change-me-oss-access-key
|
STORAGE_OSS_ACCESS_KEY_ID=change-me-oss-access-key
|
||||||
STORAGE_OSS_SECRET_ACCESS_KEY=change-me-oss-secret-key
|
STORAGE_OSS_SECRET_ACCESS_KEY=change-me-oss-secret-key
|
||||||
|
|||||||
+19
-52
@@ -16,11 +16,6 @@ HEALTH_URL=""
|
|||||||
READY_TIMEOUT="${READY_TIMEOUT:-120}"
|
READY_TIMEOUT="${READY_TIMEOUT:-120}"
|
||||||
RESET_DB=false
|
RESET_DB=false
|
||||||
RESET_DB_CONFIRM="${RESET_DB_CONFIRM:-}"
|
RESET_DB_CONFIRM="${RESET_DB_CONFIRM:-}"
|
||||||
# 激活 Compose profile:默认启用 storage,让内置 MinIO 随部署自动启动。
|
|
||||||
# 若改用外部 S3/OSS,可在环境变量前置 COMPOSE_PROFILES= 赋空值跳过内置 MinIO。
|
|
||||||
if [[ -z "${COMPOSE_PROFILES+x}" ]]; then
|
|
||||||
COMPOSE_PROFILES=(--profile storage)
|
|
||||||
fi
|
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '\033[1;34m[deploy]\033[0m %s\n' "$*"
|
printf '\033[1;34m[deploy]\033[0m %s\n' "$*"
|
||||||
@@ -51,8 +46,7 @@ print_usage() {
|
|||||||
日常结构变更请新增 002_xxx.sql 等渐进迁移,不要改已应用的历史迁移文件。
|
日常结构变更请新增 002_xxx.sql 等渐进迁移,不要改已应用的历史迁移文件。
|
||||||
它不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 Caddy 证书卷。
|
它不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 Caddy 证书卷。
|
||||||
|
|
||||||
默认激活 storage profile 启动内置 MinIO。若使用外部 S3/OSS,可在环境变量前置
|
图片存储使用阿里云 OSS(.env 的 STORAGE_OSS_*),生产编排不含 MinIO。
|
||||||
COMPOSE_PROFILES= 赋空值跳过内置 MinIO,例如:COMPOSE_PROFILES= bash deploy/ubuntu-deploy.sh
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +89,6 @@ ensure_docker() {
|
|||||||
command -v docker >/dev/null 2>&1 || fail "未找到 docker,请先在 Ubuntu 上安装 Docker Engine。"
|
command -v docker >/dev/null 2>&1 || fail "未找到 docker,请先在 Ubuntu 上安装 Docker Engine。"
|
||||||
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose 插件,请安装 Docker Compose plugin。"
|
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose 插件,请安装 Docker Compose plugin。"
|
||||||
command -v curl >/dev/null 2>&1 || fail "未找到 curl,请先安装:sudo apt-get update && sudo apt-get install -y curl"
|
command -v curl >/dev/null 2>&1 || fail "未找到 curl,请先安装:sudo apt-get update && sudo apt-get install -y curl"
|
||||||
|
|
||||||
# 允许通过环境变量 COMPOSE_PROFILES= 跳过内置 MinIO(用外部 S3/OSS 时)。
|
|
||||||
if [[ "${#COMPOSE_PROFILES[@]}" -eq 0 ]]; then
|
|
||||||
log "COMPOSE_PROFILES 为空,跳过内置 MinIO(请确保 .env 的 STORAGE_ENDPOINT 指向外部存储)"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_env_file() {
|
ensure_env_file() {
|
||||||
@@ -197,20 +186,20 @@ ensure_data_dir() {
|
|||||||
|
|
||||||
check_compose_config() {
|
check_compose_config() {
|
||||||
log "检查 Docker Compose 配置"
|
log "检查 Docker Compose 配置"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" config >/dev/null || fail "Docker Compose 配置校验失败。"
|
docker compose config >/dev/null || fail "Docker Compose 配置校验失败。"
|
||||||
}
|
}
|
||||||
|
|
||||||
pull_base_images() {
|
pull_base_images() {
|
||||||
log "拉取基础镜像"
|
log "拉取基础镜像"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" pull --ignore-buildable
|
docker compose pull --ignore-buildable
|
||||||
}
|
}
|
||||||
|
|
||||||
build_images() {
|
build_images() {
|
||||||
log "构建后端镜像"
|
log "构建后端镜像"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" build backend || fail "后端镜像构建失败。"
|
docker compose build backend || fail "后端镜像构建失败。"
|
||||||
|
|
||||||
log "构建 Web 镜像"
|
log "构建 Web 镜像"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" build web || fail "Web 镜像构建失败。"
|
docker compose build web || fail "Web 镜像构建失败。"
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve_compose_project_name() {
|
resolve_compose_project_name() {
|
||||||
@@ -227,7 +216,7 @@ resolve_compose_project_name() {
|
|||||||
resolve_postgres_volume_name() {
|
resolve_postgres_volume_name() {
|
||||||
local container_id volume_name project_name
|
local container_id volume_name project_name
|
||||||
|
|
||||||
container_id="$(docker compose "${COMPOSE_PROFILES[@]}" ps -aq postgres 2>/dev/null | head -n 1 || true)"
|
container_id="$(docker compose ps -aq postgres 2>/dev/null | head -n 1 || true)"
|
||||||
if [[ -n "${container_id}" ]]; then
|
if [[ -n "${container_id}" ]]; then
|
||||||
volume_name="$(
|
volume_name="$(
|
||||||
docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{if eq .Type "volume"}}{{.Name}}{{end}}{{end}}{{end}}' "${container_id}" 2>/dev/null || true
|
docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{if eq .Type "volume"}}{{.Name}}{{end}}{{end}}{{end}}' "${container_id}" 2>/dev/null || true
|
||||||
@@ -239,7 +228,7 @@ resolve_postgres_volume_name() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
volume_name="$(
|
volume_name="$(
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" volumes 2>/dev/null \
|
docker compose volumes 2>/dev/null \
|
||||||
| awk '$2 ~ /(^|_)postgres_data$/ { print $2; exit }' || true
|
| awk '$2 ~ /(^|_)postgres_data$/ { print $2; exit }' || true
|
||||||
)"
|
)"
|
||||||
if [[ -n "${volume_name}" ]]; then
|
if [[ -n "${volume_name}" ]]; then
|
||||||
@@ -260,8 +249,8 @@ reset_database() {
|
|||||||
volume_name="$(resolve_postgres_volume_name)"
|
volume_name="$(resolve_postgres_volume_name)"
|
||||||
|
|
||||||
warn "开始重置数据库,将删除 Docker volume:${volume_name}"
|
warn "开始重置数据库,将删除 Docker volume:${volume_name}"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" stop web backend postgres >/dev/null 2>&1 || true
|
docker compose stop web backend postgres >/dev/null 2>&1 || true
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" rm -f -s postgres >/dev/null 2>&1 || true
|
docker compose rm -f -s postgres >/dev/null 2>&1 || true
|
||||||
|
|
||||||
if docker volume inspect "${volume_name}" >/dev/null 2>&1; then
|
if docker volume inspect "${volume_name}" >/dev/null 2>&1; then
|
||||||
docker volume rm "${volume_name}" >/dev/null || fail "删除 PostgreSQL 数据卷失败:${volume_name}"
|
docker volume rm "${volume_name}" >/dev/null || fail "删除 PostgreSQL 数据卷失败:${volume_name}"
|
||||||
@@ -280,7 +269,7 @@ wait_for_service() {
|
|||||||
log "等待 ${label} 就绪"
|
log "等待 ${label} 就绪"
|
||||||
|
|
||||||
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
|
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
|
||||||
container_id="$(docker compose "${COMPOSE_PROFILES[@]}" ps -q "${service}" 2>/dev/null || true)"
|
container_id="$(docker compose ps -q "${service}" 2>/dev/null || true)"
|
||||||
if [[ -n "${container_id}" ]]; then
|
if [[ -n "${container_id}" ]]; then
|
||||||
status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)"
|
status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)"
|
||||||
health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"
|
health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"
|
||||||
@@ -309,7 +298,7 @@ wait_for_service() {
|
|||||||
|
|
||||||
start_postgres() {
|
start_postgres() {
|
||||||
log "启动 PostgreSQL"
|
log "启动 PostgreSQL"
|
||||||
if docker compose "${COMPOSE_PROFILES[@]}" up -d --no-deps postgres; then
|
if docker compose up -d --no-deps postgres; then
|
||||||
wait_for_service postgres "PostgreSQL"
|
wait_for_service postgres "PostgreSQL"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -318,30 +307,9 @@ start_postgres() {
|
|||||||
fail "PostgreSQL 启动失败,请根据上面的日志排查。"
|
fail "PostgreSQL 启动失败,请根据上面的日志排查。"
|
||||||
}
|
}
|
||||||
|
|
||||||
start_minio() {
|
|
||||||
# 未激活 storage profile 时跳过内置 MinIO(用外部 S3/OSS 的场景)。
|
|
||||||
local has_storage_profile=false
|
|
||||||
for arg in "${COMPOSE_PROFILES[@]}"; do
|
|
||||||
[[ "${arg}" == "storage" ]] && has_storage_profile=true
|
|
||||||
done
|
|
||||||
if [[ "${has_storage_profile}" != "true" ]]; then
|
|
||||||
log "跳过内置 MinIO(COMPOSE_PROFILES 未包含 storage)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "启动 MinIO"
|
|
||||||
if docker compose "${COMPOSE_PROFILES[@]}" up -d --no-deps minio; then
|
|
||||||
wait_for_service minio "MinIO"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
print_diagnostics
|
|
||||||
fail "MinIO 启动失败,请根据上面的日志排查。"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_database_migrations() {
|
run_database_migrations() {
|
||||||
log "执行数据库迁移"
|
log "执行数据库迁移"
|
||||||
if docker compose "${COMPOSE_PROFILES[@]}" run --rm --no-deps backend node dist/db/migrate.js; then
|
if docker compose run --rm --no-deps backend node dist/db/migrate.js; then
|
||||||
log "数据库迁移完成"
|
log "数据库迁移完成"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -352,7 +320,7 @@ run_database_migrations() {
|
|||||||
|
|
||||||
restart_backend() {
|
restart_backend() {
|
||||||
log "重建并启动后端"
|
log "重建并启动后端"
|
||||||
if docker compose "${COMPOSE_PROFILES[@]}" up -d --no-deps --force-recreate backend; then
|
if docker compose up -d --no-deps --force-recreate backend; then
|
||||||
wait_for_service backend "后端"
|
wait_for_service backend "后端"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -363,7 +331,7 @@ restart_backend() {
|
|||||||
|
|
||||||
restart_web() {
|
restart_web() {
|
||||||
log "重建并启动 Web 入口"
|
log "重建并启动 Web 入口"
|
||||||
if docker compose "${COMPOSE_PROFILES[@]}" up -d --no-deps --force-recreate --remove-orphans web; then
|
if docker compose up -d --no-deps --force-recreate --remove-orphans web; then
|
||||||
wait_for_service web "Web"
|
wait_for_service web "Web"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -378,7 +346,6 @@ deploy_stack() {
|
|||||||
build_images
|
build_images
|
||||||
reset_database
|
reset_database
|
||||||
start_postgres
|
start_postgres
|
||||||
start_minio
|
|
||||||
run_database_migrations
|
run_database_migrations
|
||||||
restart_backend
|
restart_backend
|
||||||
restart_web
|
restart_web
|
||||||
@@ -403,7 +370,7 @@ print_applied_migrations() {
|
|||||||
log "已应用数据库迁移"
|
log "已应用数据库迁移"
|
||||||
|
|
||||||
local migrations
|
local migrations
|
||||||
if migrations="$(docker compose "${COMPOSE_PROFILES[@]}" exec -T backend node --input-type=module -e '
|
if migrations="$(docker compose exec -T backend node --input-type=module -e '
|
||||||
const { query, closeDb } = await import("./dist/db/client.js");
|
const { query, closeDb } = await import("./dist/db/client.js");
|
||||||
try {
|
try {
|
||||||
const result = await query("SELECT filename FROM schema_migrations ORDER BY filename");
|
const result = await query("SELECT filename FROM schema_migrations ORDER BY filename");
|
||||||
@@ -422,11 +389,11 @@ try {
|
|||||||
|
|
||||||
print_diagnostics() {
|
print_diagnostics() {
|
||||||
warn "当前容器状态:"
|
warn "当前容器状态:"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" ps || true
|
docker compose ps || true
|
||||||
|
|
||||||
warn "后端最近日志:"
|
warn "后端最近日志:"
|
||||||
local backend_logs
|
local backend_logs
|
||||||
backend_logs="$(docker compose "${COMPOSE_PROFILES[@]}" logs --tail=120 backend 2>&1 || true)"
|
backend_logs="$(docker compose logs --tail=120 backend 2>&1 || true)"
|
||||||
printf '%s\n' "${backend_logs}"
|
printf '%s\n' "${backend_logs}"
|
||||||
|
|
||||||
if printf '%s\n' "${backend_logs}" | grep -q 'password authentication failed for user "postgres"'; then
|
if printf '%s\n' "${backend_logs}" | grep -q 'password authentication failed for user "postgres"'; then
|
||||||
@@ -436,7 +403,7 @@ print_diagnostics() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
warn "后端 ready 接口:"
|
warn "后端 ready 接口:"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" exec -T backend sh -lc 'curl -fsS http://127.0.0.1:3000/health/ready || true' || true
|
docker compose exec -T backend sh -lc 'curl -fsS http://127.0.0.1:3000/health/ready || true' || true
|
||||||
|
|
||||||
print_applied_migrations || true
|
print_applied_migrations || true
|
||||||
}
|
}
|
||||||
@@ -456,7 +423,7 @@ main() {
|
|||||||
print_applied_migrations
|
print_applied_migrations
|
||||||
|
|
||||||
log "部署完成"
|
log "部署完成"
|
||||||
docker compose "${COMPOSE_PROFILES[@]}" ps
|
docker compose ps
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
+2
-18
@@ -71,7 +71,8 @@ services:
|
|||||||
DATA_ROOT: /app/data
|
DATA_ROOT: /app/data
|
||||||
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30}
|
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30}
|
||||||
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
||||||
STORAGE_MODE: ${STORAGE_MODE:-minio}
|
# 生产图片存储只走阿里云 OSS;MinIO 仅保留在本地开发编排(docker-compose.dev.yml)。
|
||||||
|
STORAGE_MODE: ${STORAGE_MODE:-oss}
|
||||||
STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-http://minio:9000}
|
STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-http://minio:9000}
|
||||||
STORAGE_BUCKET: ${STORAGE_BUCKET:-order-site}
|
STORAGE_BUCKET: ${STORAGE_BUCKET:-order-site}
|
||||||
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-minioadmin}
|
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-minioadmin}
|
||||||
@@ -90,22 +91,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./apps/backend/data:/app/data
|
- ./apps/backend/data:/app/data
|
||||||
|
|
||||||
minio:
|
|
||||||
image: docker.m.daocloud.io/minio/minio:latest
|
|
||||||
profiles: ["storage"]
|
|
||||||
restart: unless-stopped
|
|
||||||
logging: *json-log-rotation
|
|
||||||
command: server /data --address ":9000" --console-address ":9001"
|
|
||||||
environment:
|
|
||||||
TZ: ${TZ:-Asia/Shanghai}
|
|
||||||
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-minioadmin}
|
|
||||||
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-minioadmin}
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -134,6 +119,5 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
minio_data:
|
|
||||||
caddy_data:
|
caddy_data:
|
||||||
caddy_config:
|
caddy_config:
|
||||||
|
|||||||
+16
-3
@@ -46,13 +46,26 @@ curl -fsS https://你的域名/health/ready
|
|||||||
|
|
||||||
此时新旧图片都只从 OSS 读取,数据库中的对象 key 和访问 URL 不需要改动。
|
此时新旧图片都只从 OSS 读取,数据库中的对象 key 和访问 URL 不需要改动。
|
||||||
|
|
||||||
## 4. 停止 MinIO
|
## 4. MinIO 退场
|
||||||
|
|
||||||
先观察 OSS 模式运行一段时间,确认图片上传、后台预览、打手端验收图片均正常,再停止并移除 MinIO 容器。不要删除 `minio_data` 卷,至少保留一个回滚窗口:
|
先观察 OSS 模式运行一段时间,确认图片上传、后台预览、打手端验收图片均正常,再停止并移除 MinIO 容器(若仍由旧版编排管理):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose --profile storage stop minio
|
docker compose --profile storage stop minio
|
||||||
docker compose --profile storage rm -f minio
|
docker compose --profile storage rm -f minio
|
||||||
```
|
```
|
||||||
|
|
||||||
若需要回滚,恢复 `STORAGE_MODE=dual`(或 `minio`),重新启动 MinIO 即可。确认 OSS 已稳定且备份完成后,再单独评估删除 `minio_data` 卷。
|
生产编排(`docker-compose.yml` 与 `deploy/ubuntu-deploy.sh`)已移除 MinIO 服务与 `storage` profile,此后部署不会再启动 MinIO。
|
||||||
|
|
||||||
|
不要删除 `minio_data` 卷(实际名称 `order_site_minio_data`),至少保留一个回滚窗口。若需要回滚,把 `.env` 的 `STORAGE_MODE` 改回 `dual`,并手动用旧卷拉起一个 MinIO 容器接入 compose 网络(`--network-alias minio` 保证 backend 仍可用 `http://minio:9000` 访问它):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name minio-rollback \
|
||||||
|
--network order_site_default \
|
||||||
|
--network-alias minio \
|
||||||
|
-v order_site_minio_data:/data \
|
||||||
|
docker.m.daocloud.io/minio/minio:latest \
|
||||||
|
server /data --address ":9000" --console-address ":9001"
|
||||||
|
```
|
||||||
|
|
||||||
|
确认 OSS 已稳定且完成密钥轮换后,再单独评估删除 `minio_data` 卷释放磁盘。
|
||||||
|
|||||||
Reference in New Issue
Block a user