上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段); 修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品 - dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开; settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch - 新增用户管理(users API/admin service/改密页) + alembic 022/023/024 - 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example - A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
27
backend/scripts/backup.sh
Executable file
27
backend/scripts/backup.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
BACKUP_DIR="${BACKUP_DIR:-./backups/$(date +%Y%m%d-%H%M%S)}"
|
||||
MYSQL_CONTAINER="${MYSQL_CONTAINER:-clover-mysql-1}"
|
||||
MONGO_CONTAINER="${MONGO_CONTAINER:-clover-mongo-1}"
|
||||
UPLOADS_DIR="${UPLOADS_DIR:-/app/uploads}"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
if [ -z "${MYSQL_USER:-}" ] || [ -z "${MYSQL_PASSWORD:-}" ] || [ -z "${MYSQL_DB:-}" ]; then
|
||||
echo "MYSQL_USER/MYSQL_PASSWORD/MYSQL_DB must be set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[1/3] dump mysql -> $BACKUP_DIR/mysql.sql.gz"
|
||||
docker exec "$MYSQL_CONTAINER" sh -c "mysqldump -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DB" | gzip > "$BACKUP_DIR/mysql.sql.gz"
|
||||
gzip -t "$BACKUP_DIR/mysql.sql.gz"
|
||||
|
||||
echo "[2/3] dump mongo -> $BACKUP_DIR/mongo.archive.gz"
|
||||
docker exec "$MONGO_CONTAINER" sh -c "mongodump --archive --gzip --db clover_trace" > "$BACKUP_DIR/mongo.archive.gz"
|
||||
gzip -t "$BACKUP_DIR/mongo.archive.gz" || true
|
||||
|
||||
echo "[3/3] archive uploads -> $BACKUP_DIR/uploads.tgz"
|
||||
tar -czf "$BACKUP_DIR/uploads.tgz" "$UPLOADS_DIR"
|
||||
|
||||
echo "backup completed: $BACKUP_DIR"
|
||||
@@ -16,7 +16,7 @@ from app.core.database import SessionLocal # noqa: E402
|
||||
from app.models.user import User # noqa: E402
|
||||
from app.models.workspace import Workspace, WorkspaceMember # noqa: E402
|
||||
from app.models.product import Product, BannedWord # noqa: E402
|
||||
from app.services.auth_service import hash_password # noqa: E402
|
||||
from app.services.auth_service import DEFAULT_SEED_PASSWORD, hash_password # noqa: E402
|
||||
|
||||
get_settings() # 触发 .env 校验,缺变量早报错
|
||||
|
||||
@@ -24,10 +24,18 @@ get_settings() # 触发 .env 校验,缺变量早报错
|
||||
WORKSPACE_NAME = "北哥小红书车间"
|
||||
WORKSPACE_SLUG = "beige-xhs"
|
||||
|
||||
SEED_PASSWORDS = {
|
||||
"admin": os.environ.get("CLOVER_SEED_PASSWORD_ADMIN", os.environ.get("CLOVER_SEED_PASSWORD", DEFAULT_SEED_PASSWORD)),
|
||||
"supervisor": os.environ.get("CLOVER_SEED_PASSWORD_SUPERVISOR", os.environ.get("CLOVER_SEED_PASSWORD", DEFAULT_SEED_PASSWORD)),
|
||||
"operator": os.environ.get("CLOVER_SEED_PASSWORD_OPERATOR", os.environ.get("CLOVER_SEED_PASSWORD", DEFAULT_SEED_PASSWORD)),
|
||||
}
|
||||
if get_settings().APP_ENV == "production" and any(p == DEFAULT_SEED_PASSWORD for p in SEED_PASSWORDS.values()):
|
||||
raise RuntimeError("Production seed requires CLOVER_SEED_PASSWORD; default password is forbidden")
|
||||
|
||||
USERS = [
|
||||
{"username": "admin", "email": "admin@clover.local", "password": "Clover2026!", "role": "admin"},
|
||||
{"username": "supervisor", "email": "supervisor@clover.local", "password": "Clover2026!", "role": "supervisor"},
|
||||
{"username": "operator", "email": "operator@clover.local", "password": "Clover2026!", "role": "operator"},
|
||||
{"username": "admin", "email": "admin@clover.local", "password": SEED_PASSWORDS["admin"], "role": "admin"},
|
||||
{"username": "supervisor", "email": "supervisor@clover.local", "password": SEED_PASSWORDS["supervisor"], "role": "supervisor"},
|
||||
{"username": "operator", "email": "operator@clover.local", "password": SEED_PASSWORDS["operator"], "role": "operator"},
|
||||
]
|
||||
|
||||
PRODUCT = {
|
||||
@@ -72,6 +80,7 @@ def run():
|
||||
email=u_spec["email"],
|
||||
hashed_password=hash_password(u_spec["password"]),
|
||||
is_active=True,
|
||||
must_change_password=True,
|
||||
)
|
||||
db.add(user)
|
||||
db.flush()
|
||||
@@ -109,38 +118,9 @@ def run():
|
||||
else:
|
||||
print(f"[=] banned_word 已存在: {bw_spec['word']}")
|
||||
|
||||
# ── user_api_keys(R3:明文key只在加密瞬间用,不打印不落明文)──
|
||||
apiports_key = os.environ.get("APIPORTS_KEY", "").strip()
|
||||
if apiports_key:
|
||||
from app.models.workspace import UserApiKey
|
||||
from app.utils.fernet_utils import encrypt_key
|
||||
for u_spec in USERS:
|
||||
u = db.query(User).filter(User.username == u_spec["username"]).first()
|
||||
if not u:
|
||||
continue
|
||||
existing_key = db.query(UserApiKey).filter(
|
||||
UserApiKey.user_id == u.id,
|
||||
UserApiKey.workspace_id == ws.id,
|
||||
UserApiKey.provider == "apiports",
|
||||
).first()
|
||||
encrypted = encrypt_key(apiports_key)
|
||||
last4 = apiports_key[-4:] if len(apiports_key) >= 4 else "****"
|
||||
if existing_key:
|
||||
existing_key.encrypted_key = encrypted
|
||||
existing_key.key_last4 = last4
|
||||
print(f"[=] api_key updated: {u.username} apiports ***{last4}")
|
||||
else:
|
||||
db.add(UserApiKey(
|
||||
user_id=u.id,
|
||||
workspace_id=ws.id,
|
||||
provider="apiports",
|
||||
encrypted_key=encrypted,
|
||||
key_last4=last4,
|
||||
))
|
||||
print(f"[+] api_key: {u.username} apiports ***{last4}")
|
||||
apiports_key = None # 明文用完即清(基石B)
|
||||
else:
|
||||
print("[!] APIPORTS_KEY 未设置,跳过 user_api_keys 录入(部署时需手动录入或重跑种子)")
|
||||
# ── user_api_keys:不再预置(倩倩姐2026-06-12拍板:系统埋的key彻底拔干净)──
|
||||
# 每个用户登录后到「设置 → API Key」自录自用,录一次即记住。
|
||||
# 种子不再从 APIPORTS_KEY 环境变量批量塞 key,避免"免key可用"。
|
||||
|
||||
db.commit()
|
||||
print("\n种子数据初始化完成。")
|
||||
|
||||
70
backend/scripts/watch.sh
Executable file
70
backend/scripts/watch.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# watch.sh — Clover 全站实时监控
|
||||
# 用途: 倩倩姐操作页面时, 实时显示哪个请求出错(4xx/5xx)、哪里抛异常。
|
||||
# 用法: bash backend/scripts/watch.sh # 只看错误(默认, 干净)
|
||||
# bash backend/scripts/watch.sh --all # 看全部请求(含正常)
|
||||
# 退出: Ctrl-C
|
||||
set -u
|
||||
|
||||
MODE="${1:-errors}" # errors(默认只看错误) | --all(全部)
|
||||
|
||||
# 颜色
|
||||
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[36m'; DIM=$'\033[2m'; N=$'\033[0m'
|
||||
|
||||
echo "${B}========================================${N}"
|
||||
echo "${B} Clover 实时监控 (Ctrl-C 退出)${N}"
|
||||
if [ "$MODE" = "--all" ]; then
|
||||
echo " 模式: 全部请求"
|
||||
else
|
||||
echo " 模式: 只看错误 (加 --all 看全部)"
|
||||
fi
|
||||
echo "${B}========================================${N}"
|
||||
echo "${DIM}盯着: clover_api + clover_worker. 现在去浏览器操作, 出错会标红.${N}"
|
||||
echo ""
|
||||
|
||||
# 同时跟随 api 和 worker 日志, 每行打上来源标签
|
||||
{
|
||||
docker logs -f --tail 0 clover_api 2>&1 | sed "s/^/[API] /" &
|
||||
API_PID=$!
|
||||
docker logs -f --tail 0 clover_worker 2>&1 | sed "s/^/[WRK] /" &
|
||||
WRK_PID=$!
|
||||
trap "kill $API_PID $WRK_PID 2>/dev/null" EXIT INT TERM
|
||||
wait
|
||||
} | while IFS= read -r line; do
|
||||
# 过滤纯噪音: 健康检查 + SQLAlchemy 回显
|
||||
case "$line" in
|
||||
*"/health"*) continue ;;
|
||||
*"sqlalchemy"*|*"SELECT "*|*"FROM "*|*"WHERE "*|*"LIMIT "*|*"INSERT "*|*"UPDATE "*|*"BEGIN "*|*"COMMIT"*|*"ROLLBACK"*) continue ;;
|
||||
esac
|
||||
|
||||
ts=$(date '+%H:%M:%S')
|
||||
|
||||
# 1) HTTP 请求行: 提取状态码
|
||||
if echo "$line" | grep -qE '"(GET|POST|PUT|DELETE|PATCH) '; then
|
||||
code=$(echo "$line" | grep -oE 'HTTP/1\.1" [0-9]{3}' | grep -oE '[0-9]{3}$')
|
||||
req=$(echo "$line" | grep -oE '"(GET|POST|PUT|DELETE|PATCH) [^"]*"')
|
||||
src=$(echo "$line" | grep -oE '^\[[A-Z]+\]')
|
||||
case "$code" in
|
||||
2*) [ "$MODE" = "--all" ] && echo "${DIM}${ts} ${src} ${G}${code}${N}${DIM} ${req}${N}" ;;
|
||||
4*) echo "${ts} ${src} ${Y}${code} ⚠ 客户端错误${N} ${req}" ;;
|
||||
5*) echo "${ts} ${src} ${R}${code} ✖ 服务端错误${N} ${req}" ;;
|
||||
*) [ "$MODE" = "--all" ] && echo "${ts} ${src} ${code} ${req}" ;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
# 2) 异常/报错/堆栈: 永远显示并标红
|
||||
if echo "$line" | grep -qiE "error|exception|traceback|critical|失败|拒绝|unauthorized|forbidden|raise"; then
|
||||
echo "${ts} ${R}${line}${N}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 3) WARNING 级: 黄色提示
|
||||
if echo "$line" | grep -qE "WARNING|warn"; then
|
||||
echo "${ts} ${Y}${line}${N}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 4) 其余行仅在 --all 模式显示
|
||||
[ "$MODE" = "--all" ] && echo "${DIM}${ts} ${line}${N}"
|
||||
done
|
||||
Reference in New Issue
Block a user