包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
674 lines
21 KiB
Python
674 lines
21 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
管理端路由 — 注册体系(连接器/租户/店铺/简写ID/店铺同步)。
|
||
|
||
端点清单:
|
||
- GET /api/admin/tenants — 所有活跃租户列表
|
||
- GET /api/admin/tenants/{tenant_id}/sites — 指定租户下所有活跃店铺
|
||
- PUT /api/admin/sites/{site_id}/site-code — 设置/修改简写ID
|
||
- GET /api/admin/sites/{site_id}/site-code-history — 简写ID 变更历史
|
||
- POST /api/admin/sites/sync — 手动触发店铺同步
|
||
- POST /api/admin/sites/sync/internal — 内部 API:ETL DWD 完成后触发同步(无认证,隐藏)
|
||
|
||
除 /sites/sync/internal 外,所有端点要求 JWT + site_admin 或 tenant_admin 角色。
|
||
|
||
需求: A2.1, A2.2, A2.4, A2.5, A3.1, A3.2, A3.3, A3.4, A5.1, A5.2, A5.3, A5.4
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
|
||
import psycopg2
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from psycopg2.extensions import connection as PgConnection
|
||
|
||
from app.auth.dependencies import CurrentUser, get_current_user
|
||
from app.config import (
|
||
ETL_DB_HOST,
|
||
ETL_DB_NAME,
|
||
ETL_DB_PASSWORD,
|
||
ETL_DB_PORT,
|
||
ETL_DB_USER,
|
||
)
|
||
from app.database import get_connection
|
||
from app.schemas.admin_registry import (
|
||
CreateSiteRequest,
|
||
SiteCodeHistoryItem,
|
||
SiteCodeResult,
|
||
SiteItem,
|
||
SiteSyncResult,
|
||
TenantItem,
|
||
UpdateSiteCodeRequest,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/admin", tags=["admin-registry"])
|
||
|
||
# 简写ID 格式:前 3 位字母/数字 + 后 3 位数字(共 6 位)
|
||
_SITE_CODE_PATTERN = re.compile(r"^[A-Z0-9]{3}\d{3}$")
|
||
|
||
|
||
# ── ETL 库直连(无 RLS,管理端同步专用) ─────────────────
|
||
|
||
|
||
def _get_etl_admin_connection() -> PgConnection:
|
||
"""获取 ETL 库只读连接(无 RLS 隔离),用于管理端跨站点同步。
|
||
|
||
与 database.get_etl_readonly_connection 不同:
|
||
- 不设置 app.current_site_id(需要读取所有站点数据)
|
||
- 仍设置 read_only 防止误写
|
||
"""
|
||
conn = psycopg2.connect(
|
||
host=ETL_DB_HOST,
|
||
port=ETL_DB_PORT,
|
||
user=ETL_DB_USER,
|
||
password=ETL_DB_PASSWORD,
|
||
dbname=ETL_DB_NAME,
|
||
)
|
||
try:
|
||
conn.autocommit = False
|
||
with conn.cursor() as cur:
|
||
cur.execute("SET default_transaction_read_only = on")
|
||
conn.commit()
|
||
except Exception:
|
||
conn.close()
|
||
raise
|
||
return conn
|
||
|
||
|
||
# ── 店铺同步核心逻辑 ─────────────────────────────────────
|
||
|
||
|
||
def sync_sites_from_etl() -> SiteSyncResult:
|
||
"""从 ETL 库 dwd.dim_site 同步店铺到 biz.sites。
|
||
|
||
逻辑:
|
||
1. 读取 dwd.dim_site(scd2_is_current=1)获取当前有效店铺
|
||
2. 对比 biz.sites:
|
||
- 新 site_id → INSERT(site_code 留空,tenant_id 通过 dim_site.tenant_id 映射 biz.tenants)
|
||
- 已有 site_id 且 shop_name/site_label 变更 → UPDATE
|
||
3. 不删除已有记录
|
||
|
||
需求: A5.1, A5.2
|
||
"""
|
||
# 1. 从 ETL 库读取当前有效店铺
|
||
etl_conn = _get_etl_admin_connection()
|
||
try:
|
||
with etl_conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT site_id, tenant_id, shop_name, site_label
|
||
FROM dwd.dim_site
|
||
WHERE scd2_is_current = 1
|
||
"""
|
||
)
|
||
etl_sites = cur.fetchall()
|
||
finally:
|
||
etl_conn.close()
|
||
|
||
if not etl_sites:
|
||
return SiteSyncResult(inserted=0, updated=0)
|
||
|
||
# 2. 在 app 库中执行对比和写入
|
||
app_conn = get_connection()
|
||
inserted = 0
|
||
updated = 0
|
||
try:
|
||
with app_conn.cursor() as cur:
|
||
# 构建 tenant_id → biz.tenants.id 映射
|
||
cur.execute("SELECT tenant_id, id FROM biz.tenants WHERE is_active = true")
|
||
tenant_map: dict[int, int] = {row[0]: row[1] for row in cur.fetchall()}
|
||
|
||
# 获取 biz.sites 现有数据(site_id → (biz_id, site_name, site_label))
|
||
cur.execute(
|
||
"SELECT site_id, id, site_name, site_label FROM biz.sites"
|
||
)
|
||
existing: dict[int, tuple[int, str | None, str | None]] = {
|
||
row[0]: (row[1], row[2], row[3]) for row in cur.fetchall()
|
||
}
|
||
|
||
for etl_site_id, etl_tenant_id, etl_shop_name, etl_site_label in etl_sites:
|
||
biz_tenant_id = tenant_map.get(etl_tenant_id)
|
||
if biz_tenant_id is None:
|
||
# 租户未注册,跳过
|
||
logger.warning(
|
||
"同步跳过: site_id=%s 的 tenant_id=%s 在 biz.tenants 中不存在",
|
||
etl_site_id, etl_tenant_id,
|
||
)
|
||
continue
|
||
|
||
if etl_site_id not in existing:
|
||
# 新增店铺:site_code 留空
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO biz.sites (tenant_id, site_id, site_name, site_label)
|
||
VALUES (%s, %s, %s, %s)
|
||
""",
|
||
(biz_tenant_id, etl_site_id, etl_shop_name, etl_site_label),
|
||
)
|
||
inserted += 1
|
||
else:
|
||
# 已有店铺:检查名称/标签是否变更
|
||
_, cur_name, cur_label = existing[etl_site_id]
|
||
if cur_name != etl_shop_name or cur_label != etl_site_label:
|
||
cur.execute(
|
||
"""
|
||
UPDATE biz.sites
|
||
SET site_name = %s, site_label = %s, updated_at = NOW()
|
||
WHERE site_id = %s
|
||
""",
|
||
(etl_shop_name, etl_site_label, etl_site_id),
|
||
)
|
||
updated += 1
|
||
|
||
app_conn.commit()
|
||
except Exception:
|
||
app_conn.rollback()
|
||
raise
|
||
finally:
|
||
app_conn.close()
|
||
|
||
logger.info("店铺同步完成: 新增 %d, 更新 %d", inserted, updated)
|
||
return SiteSyncResult(inserted=inserted, updated=updated)
|
||
|
||
|
||
# ── 管理端权限依赖 ──────────────────────────────────────
|
||
|
||
|
||
def _require_admin():
|
||
"""管理端依赖:要求 JWT 中角色包含 site_admin 或 tenant_admin。"""
|
||
|
||
async def _dependency(
|
||
user: CurrentUser = Depends(get_current_user),
|
||
) -> CurrentUser:
|
||
admin_roles = {"site_admin", "tenant_admin"}
|
||
if not admin_roles.intersection(user.roles):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="需要管理员权限(site_admin 或 tenant_admin)",
|
||
)
|
||
return user
|
||
|
||
return _dependency
|
||
|
||
|
||
# ── GET /api/admin/tenants ────────────────────────────────
|
||
|
||
|
||
@router.get("/tenants")
|
||
async def list_tenants(
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
) -> list[TenantItem]:
|
||
"""
|
||
所有活跃租户列表(含连接器名称)。
|
||
|
||
JOIN biz.connectors 获取 connector_name。
|
||
需求 A2.1
|
||
"""
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""
|
||
SELECT t.id, t.tenant_id, t.tenant_name,
|
||
c.display_name AS connector_name, t.is_active
|
||
FROM biz.tenants t
|
||
JOIN biz.connectors c ON c.id = t.connector_id
|
||
WHERE t.is_active = true
|
||
ORDER BY t.id
|
||
"""
|
||
)
|
||
rows = cur.fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
return [
|
||
TenantItem(
|
||
id=r[0],
|
||
tenant_id=r[1],
|
||
tenant_name=r[2],
|
||
connector_name=r[3],
|
||
is_active=r[4],
|
||
)
|
||
for r in rows
|
||
]
|
||
|
||
|
||
# ── GET /api/admin/tenants/{tenant_id}/sites ──────────────
|
||
|
||
|
||
@router.get("/tenants/{tenant_id}/sites")
|
||
async def list_tenant_sites(
|
||
tenant_id: int,
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
) -> list[SiteItem]:
|
||
"""
|
||
指定租户下所有活跃店铺。
|
||
|
||
tenant_id 参数支持两种格式:
|
||
- 上游系统租户 ID(BIGINT,如 2790683160709957)
|
||
- 内部主键(SERIAL,如 1, 2, 3...)
|
||
自动判断:> 10000 视为上游 ID,否则视为内部 PK。
|
||
|
||
需求 A2.2
|
||
"""
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
# CHANGE 2026-03-22 | Prompt: 管辖门店下拉为空 | 兼容上游 tenant_id 和内部 PK
|
||
# 自动判断:上游 ID 是 BIGINT(远大于内部 SERIAL),阈值 10000 足够区分
|
||
if tenant_id > 10000:
|
||
cur.execute(
|
||
"SELECT id FROM biz.tenants WHERE tenant_id = %s AND is_active = true",
|
||
(tenant_id,),
|
||
)
|
||
else:
|
||
cur.execute(
|
||
"SELECT id FROM biz.tenants WHERE id = %s AND is_active = true",
|
||
(tenant_id,),
|
||
)
|
||
row = cur.fetchone()
|
||
if row is None:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="租户不存在",
|
||
)
|
||
internal_tenant_id = row[0]
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT id, site_id, site_name, site_code, site_label, is_active
|
||
FROM biz.sites
|
||
WHERE tenant_id = %s AND is_active = true
|
||
ORDER BY site_id
|
||
""",
|
||
(internal_tenant_id,),
|
||
)
|
||
rows = cur.fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
return [
|
||
SiteItem(
|
||
id=r[0],
|
||
site_id=r[1],
|
||
site_name=r[2],
|
||
site_code=r[3],
|
||
site_label=r[4],
|
||
is_active=r[5],
|
||
)
|
||
for r in rows
|
||
]
|
||
|
||
|
||
# ── PUT /api/admin/sites/{site_id}/site-code ──────────────
|
||
|
||
|
||
@router.put("/sites/{site_id}/site-code")
|
||
async def update_site_code(
|
||
site_id: int,
|
||
body: UpdateSiteCodeRequest,
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
) -> SiteCodeResult:
|
||
"""
|
||
设置/修改店铺简写ID,事务内执行历史记录管理。
|
||
|
||
校验规则:
|
||
- 格式:6 位,前 3 位字母/数字 + 后 3 位数字,统一大写
|
||
- 全局唯一:biz.sites.site_code + biz.site_code_history.site_code
|
||
|
||
事务步骤:
|
||
a. 旧 code 在 site_code_history 中标记 is_current=false, retired_at=NOW()
|
||
b. 新 code 插入 site_code_history(is_current=true)
|
||
c. 更新 biz.sites.site_code
|
||
d. 检查旧 code 是否有未审核申请引用,无引用则从 history 中删除旧记录
|
||
|
||
需求 A2.4, A3.1, A3.2, A3.3, A3.4
|
||
"""
|
||
new_code = body.new_code.strip().upper()
|
||
|
||
# ── 格式校验 ──
|
||
if not _SITE_CODE_PATTERN.match(new_code):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||
detail="简写ID 格式错误,需 6 位(3+3 模式:前 3 位字母/数字 + 后 3 位数字)",
|
||
)
|
||
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
# ── 校验店铺存在 ──
|
||
cur.execute(
|
||
"SELECT site_id, site_code FROM biz.sites WHERE id = %s",
|
||
(site_id,),
|
||
)
|
||
site_row = cur.fetchone()
|
||
if site_row is None:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="店铺不存在",
|
||
)
|
||
db_site_id = site_row[0] # biz.sites.site_id(上游 ID)
|
||
old_code = site_row[1]
|
||
|
||
# ── 全局唯一性校验(biz.sites + biz.site_code_history) ──
|
||
cur.execute(
|
||
"""
|
||
SELECT 1 FROM biz.sites
|
||
WHERE site_code = %s AND id != %s
|
||
LIMIT 1
|
||
""",
|
||
(new_code, site_id),
|
||
)
|
||
if cur.fetchone():
|
||
raise HTTPException(
|
||
status_code=status.HTTP_409_CONFLICT,
|
||
detail=f"简写ID '{new_code}' 已被使用",
|
||
)
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT 1 FROM biz.site_code_history
|
||
WHERE site_code = %s AND site_id != %s
|
||
LIMIT 1
|
||
""",
|
||
(new_code, db_site_id),
|
||
)
|
||
if cur.fetchone():
|
||
raise HTTPException(
|
||
status_code=status.HTTP_409_CONFLICT,
|
||
detail=f"简写ID '{new_code}' 已被使用",
|
||
)
|
||
|
||
# ── 事务内执行变更 ──
|
||
|
||
# a. 旧 code 标记 retired
|
||
history_cleaned = False
|
||
if old_code:
|
||
cur.execute(
|
||
"""
|
||
UPDATE biz.site_code_history
|
||
SET is_current = false, retired_at = NOW()
|
||
WHERE site_id = %s AND site_code = %s AND is_current = true
|
||
""",
|
||
(db_site_id, old_code),
|
||
)
|
||
|
||
# b. 新 code 插入 history
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO biz.site_code_history (site_id, site_code, is_current)
|
||
VALUES (%s, %s, true)
|
||
""",
|
||
(db_site_id, new_code),
|
||
)
|
||
|
||
# c. 更新 biz.sites.site_code
|
||
cur.execute(
|
||
"""
|
||
UPDATE biz.sites SET site_code = %s, updated_at = NOW()
|
||
WHERE id = %s
|
||
""",
|
||
(new_code, site_id),
|
||
)
|
||
|
||
# d. 检查旧 code 是否有未审核申请引用,无引用则清理历史
|
||
if old_code:
|
||
cur.execute(
|
||
"""
|
||
SELECT 1 FROM auth.user_applications
|
||
WHERE site_code = %s AND status = 'pending'
|
||
LIMIT 1
|
||
""",
|
||
(old_code,),
|
||
)
|
||
has_pending = cur.fetchone() is not None
|
||
|
||
if not has_pending:
|
||
cur.execute(
|
||
"""
|
||
DELETE FROM biz.site_code_history
|
||
WHERE site_id = %s AND site_code = %s AND is_current = false
|
||
""",
|
||
(db_site_id, old_code),
|
||
)
|
||
history_cleaned = True
|
||
|
||
conn.commit()
|
||
except HTTPException:
|
||
conn.rollback()
|
||
raise
|
||
except Exception:
|
||
conn.rollback()
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
return SiteCodeResult(
|
||
site_id=db_site_id,
|
||
old_code=old_code,
|
||
new_code=new_code,
|
||
history_cleaned=history_cleaned,
|
||
)
|
||
|
||
|
||
# ── GET /api/admin/sites/{site_id}/site-code-history ──────
|
||
|
||
|
||
@router.get("/sites/{site_id}/site-code-history")
|
||
async def get_site_code_history(
|
||
site_id: int,
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
) -> list[SiteCodeHistoryItem]:
|
||
"""
|
||
查看简写ID 变更历史。
|
||
|
||
需求 A2.5
|
||
"""
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
# 校验店铺存在,获取上游 site_id
|
||
cur.execute(
|
||
"SELECT site_id FROM biz.sites WHERE id = %s",
|
||
(site_id,),
|
||
)
|
||
site_row = cur.fetchone()
|
||
if site_row is None:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="店铺不存在",
|
||
)
|
||
db_site_id = site_row[0]
|
||
|
||
cur.execute(
|
||
"""
|
||
SELECT id, site_code, is_current, created_at, retired_at
|
||
FROM biz.site_code_history
|
||
WHERE site_id = %s
|
||
ORDER BY created_at DESC
|
||
""",
|
||
(db_site_id,),
|
||
)
|
||
rows = cur.fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
return [
|
||
SiteCodeHistoryItem(
|
||
id=r[0],
|
||
site_code=r[1],
|
||
is_current=r[2],
|
||
created_at=r[3],
|
||
retired_at=r[4],
|
||
)
|
||
for r in rows
|
||
]
|
||
|
||
|
||
# ── POST /api/admin/sites/sync ────────────────────────────
|
||
|
||
|
||
@router.post("/sites/sync")
|
||
async def sync_sites(
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
) -> SiteSyncResult:
|
||
"""
|
||
手动触发店铺同步:从 ETL 库 dwd.dim_site 同步到 biz.sites。
|
||
|
||
返回同步结果(新增数/更新数)。
|
||
|
||
需求 A5.3
|
||
"""
|
||
return sync_sites_from_etl()
|
||
|
||
# ── POST /api/admin/sites/sync/internal ───────────────────
|
||
|
||
|
||
@router.post("/sites/sync/internal", include_in_schema=False)
|
||
async def sync_sites_internal() -> SiteSyncResult:
|
||
"""内部 API:ETL DWD 完成后触发店铺同步。
|
||
|
||
不需要 JWT 认证(内部调用),通过 include_in_schema=False 隐藏。
|
||
后续可添加 API key 或 IP 白名单认证。
|
||
|
||
需求 A5.4
|
||
"""
|
||
return sync_sites_from_etl()
|
||
|
||
|
||
# ── POST /api/admin/sites(测试功能:手动创建店铺) ────────
|
||
|
||
|
||
@router.post("/sites", status_code=status.HTTP_201_CREATED)
|
||
async def create_site(
|
||
body: CreateSiteRequest,
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
):
|
||
"""
|
||
手动创建店铺(测试功能)。
|
||
|
||
向 biz.sites 插入一条记录,可选指定 site_code。
|
||
site_id 和 site_code 需全局唯一,冲突返回 409。
|
||
"""
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
# 校验 tenant_id 存在
|
||
cur.execute(
|
||
"SELECT id FROM biz.tenants WHERE tenant_id = %s AND is_active = true",
|
||
(body.tenant_id,),
|
||
)
|
||
tenant_row = cur.fetchone()
|
||
if tenant_row is None:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="租户不存在",
|
||
)
|
||
internal_tenant_id = tenant_row[0]
|
||
|
||
# site_code 格式校验(如果提供)
|
||
site_code = None
|
||
if body.site_code:
|
||
site_code = body.site_code.strip().upper()
|
||
if not _SITE_CODE_PATTERN.match(site_code):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||
detail="简写ID 格式错误,需 6 位(3+3 格式)",
|
||
)
|
||
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO biz.sites (tenant_id, site_id, site_name, site_code)
|
||
VALUES (%s, %s, %s, %s)
|
||
RETURNING id, site_id, site_name, site_code
|
||
""",
|
||
(internal_tenant_id, body.site_id, body.site_name, site_code),
|
||
)
|
||
row = cur.fetchone()
|
||
|
||
# 如果有 site_code,同步插入 history
|
||
if site_code:
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO biz.site_code_history (site_id, site_code, is_current)
|
||
VALUES (%s, %s, true)
|
||
""",
|
||
(body.site_id, site_code),
|
||
)
|
||
|
||
conn.commit()
|
||
except HTTPException:
|
||
conn.rollback()
|
||
raise
|
||
except psycopg2.errors.UniqueViolation as e:
|
||
conn.rollback()
|
||
detail = str(e)
|
||
if "site_id" in detail:
|
||
raise HTTPException(status_code=409, detail="site_id 已存在")
|
||
if "site_code" in detail:
|
||
raise HTTPException(status_code=409, detail="简写ID 已被占用")
|
||
raise HTTPException(status_code=409, detail="唯一约束冲突")
|
||
except Exception:
|
||
conn.rollback()
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
return {"id": row[0], "siteId": row[1], "siteName": row[2], "siteCode": row[3]}
|
||
|
||
|
||
# ── DELETE /api/admin/sites/{site_id}(测试功能:删除店铺) ─
|
||
|
||
|
||
@router.delete("/sites/{site_id}")
|
||
async def delete_site(
|
||
site_id: int,
|
||
user: CurrentUser = Depends(_require_admin()),
|
||
):
|
||
"""
|
||
删除店铺(测试功能,硬删除)。
|
||
|
||
同时清理 site_code_history 中的关联记录。
|
||
site_id 参数为 biz.sites.id(内部主键)。
|
||
"""
|
||
conn = get_connection()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
# 获取上游 site_id 用于清理 history
|
||
cur.execute(
|
||
"SELECT site_id FROM biz.sites WHERE id = %s",
|
||
(site_id,),
|
||
)
|
||
row = cur.fetchone()
|
||
if row is None:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_404_NOT_FOUND,
|
||
detail="店铺不存在",
|
||
)
|
||
upstream_site_id = row[0]
|
||
|
||
# 清理 site_code_history
|
||
cur.execute(
|
||
"DELETE FROM biz.site_code_history WHERE site_id = %s",
|
||
(upstream_site_id,),
|
||
)
|
||
|
||
# 删除店铺
|
||
cur.execute("DELETE FROM biz.sites WHERE id = %s", (site_id,))
|
||
|
||
conn.commit()
|
||
except HTTPException:
|
||
conn.rollback()
|
||
raise
|
||
except Exception:
|
||
conn.rollback()
|
||
raise
|
||
finally:
|
||
conn.close()
|
||
|
||
return {"id": site_id}
|