- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go) - 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken) - 阅读页分享朋友圈复制与 toast 去重 - soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整 - 脚本:content_upload、miniprogram 上传辅助等 Made-with: Cursor
238 lines
7.1 KiB
Python
238 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
将书稿 md 上传到小程序对应 chapters 表(与 Soul创业实验 Skill「上传」一致)。
|
||
|
||
依赖: pip install pymysql
|
||
数据库配置:复用 scripts/migrate_2026_sections.py 中的 DB_CONFIG(与现网一致)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import importlib.util
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent
|
||
|
||
PART_2026 = "part-2026-daily"
|
||
CHAPTER_2026 = "chapter-2026-daily"
|
||
TITLE_2026 = "2026每日派对干货"
|
||
|
||
|
||
def load_db_config() -> dict:
|
||
mig = ROOT / "scripts" / "migrate_2026_sections.py"
|
||
if not mig.is_file():
|
||
print("缺少 scripts/migrate_2026_sections.py,无法读取 DB_CONFIG", file=sys.stderr)
|
||
sys.exit(1)
|
||
spec = importlib.util.spec_from_file_location("_mig_db", mig)
|
||
mod = importlib.util.module_from_spec(spec)
|
||
assert spec.loader is not None
|
||
spec.loader.exec_module(mod)
|
||
cfg = getattr(mod, "DB_CONFIG", None)
|
||
if not isinstance(cfg, dict):
|
||
print("migrate_2026_sections.py 中无有效 DB_CONFIG", file=sys.stderr)
|
||
sys.exit(1)
|
||
return cfg
|
||
|
||
|
||
def strip_md_title_line(text: str) -> str:
|
||
lines = text.splitlines()
|
||
if lines and lines[0].lstrip().startswith("#"):
|
||
return "\n".join(lines[1:]).lstrip("\n")
|
||
return text
|
||
|
||
|
||
def for_miniprogram_body(text: str) -> str:
|
||
"""上传 README:少用 --- 分割线;正文内独立一行的 --- 改为空行分段。"""
|
||
out_lines: list[str] = []
|
||
for line in text.splitlines():
|
||
if line.strip() == "---":
|
||
out_lines.append("")
|
||
out_lines.append("")
|
||
else:
|
||
out_lines.append(line)
|
||
return "\n".join(out_lines).strip() + "\n"
|
||
|
||
|
||
def next_10_id(cur) -> str:
|
||
cur.execute(
|
||
"""
|
||
SELECT id FROM chapters
|
||
WHERE id REGEXP '^10\\\\.[0-9]+$'
|
||
ORDER BY CAST(SUBSTRING_INDEX(id, '.', -1) AS UNSIGNED) DESC
|
||
LIMIT 1
|
||
"""
|
||
)
|
||
row = cur.fetchone()
|
||
if not row:
|
||
return "10.01"
|
||
last = row[0]
|
||
n = int(last.split(".")[-1])
|
||
return f"10.{n + 1:02d}"
|
||
|
||
|
||
def list_structure(cur):
|
||
cur.execute(
|
||
"""
|
||
SELECT DISTINCT part_id, part_title, chapter_id, chapter_title
|
||
FROM chapters
|
||
ORDER BY part_id, chapter_id
|
||
"""
|
||
)
|
||
print("篇章结构(distinct part/chapter):")
|
||
for r in cur.fetchall():
|
||
print(f" part={r[0]!r} chapter={r[2]!r} | {r[1]} / {r[3]}")
|
||
|
||
|
||
def list_chapters_2026(cur):
|
||
cur.execute(
|
||
"""
|
||
SELECT id, section_title, sort_order
|
||
FROM chapters
|
||
WHERE part_id = %s AND chapter_id = %s
|
||
ORDER BY COALESCE(sort_order, 999999) ASC, id ASC
|
||
""",
|
||
(PART_2026, CHAPTER_2026),
|
||
)
|
||
print(f"2026每日派对干货 ({PART_2026} / {CHAPTER_2026}):")
|
||
for r in cur.fetchall():
|
||
print(f" {r[0]}\torder={r[2]}\t{r[1]}")
|
||
|
||
|
||
def main():
|
||
try:
|
||
import pymysql
|
||
except ImportError:
|
||
print("需要: pip install pymysql", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
p = argparse.ArgumentParser(description="上传书稿 md 到 soul_miniprogram.chapters")
|
||
p.add_argument("--id", help="业务 id,如 10.27;省略则自动取当前最大 10.xx +1")
|
||
p.add_argument("--title", help="小节标题,如 第128场|主题")
|
||
p.add_argument("--content-file", type=Path, help="文章 md 绝对或相对路径")
|
||
p.add_argument("--part", default=PART_2026)
|
||
p.add_argument("--chapter", default=CHAPTER_2026)
|
||
p.add_argument("--part-title", default=TITLE_2026)
|
||
p.add_argument("--chapter-title", default=TITLE_2026)
|
||
p.add_argument("--price", type=float, default=1.0)
|
||
p.add_argument("--free", action="store_true", help="标记为免费")
|
||
p.add_argument("--list-structure", action="store_true")
|
||
p.add_argument("--list-chapters", action="store_true")
|
||
p.add_argument("--dry-run", action="store_true")
|
||
args = p.parse_args()
|
||
|
||
cfg = load_db_config()
|
||
conn = pymysql.connect(**cfg)
|
||
cur = conn.cursor()
|
||
|
||
if args.list_structure:
|
||
list_structure(cur)
|
||
conn.close()
|
||
return
|
||
if args.list_chapters:
|
||
list_chapters_2026(cur)
|
||
conn.close()
|
||
return
|
||
|
||
if not args.title or not args.content_file:
|
||
p.error("上传时必须提供 --title 与 --content-file")
|
||
|
||
path = args.content_file.expanduser().resolve()
|
||
if not path.is_file():
|
||
print(f"文件不存在: {path}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
raw = path.read_text(encoding="utf-8")
|
||
body = for_miniprogram_body(strip_md_title_line(raw))
|
||
word_count = len(body)
|
||
is_free = 1 if args.free else 0
|
||
price = 0.0 if args.free else float(args.price)
|
||
|
||
section_id = args.id
|
||
if not section_id:
|
||
section_id = next_10_id(cur)
|
||
print(f"未指定 --id,使用新 id: {section_id}")
|
||
|
||
cur.execute("SELECT mid FROM chapters WHERE id = %s", (section_id,))
|
||
row = cur.fetchone()
|
||
exists = row is not None
|
||
|
||
cur.execute("SELECT COALESCE(MAX(sort_order), -1) FROM chapters")
|
||
max_sort = cur.fetchone()[0]
|
||
next_sort = int(max_sort) + 1
|
||
|
||
if args.dry_run:
|
||
print(f"id={section_id} exists={exists} next_sort={next_sort} words={word_count}")
|
||
print(body[:500] + ("..." if len(body) > 500 else ""))
|
||
conn.close()
|
||
return
|
||
|
||
if exists:
|
||
cur.execute(
|
||
"""
|
||
UPDATE chapters SET
|
||
section_title = %s,
|
||
content = %s,
|
||
word_count = %s,
|
||
price = %s,
|
||
is_free = %s,
|
||
part_id = %s,
|
||
part_title = %s,
|
||
chapter_id = %s,
|
||
chapter_title = %s,
|
||
updated_at = NOW()
|
||
WHERE id = %s
|
||
""",
|
||
(
|
||
args.title,
|
||
body,
|
||
word_count,
|
||
price,
|
||
is_free,
|
||
args.part,
|
||
args.part_title,
|
||
args.chapter,
|
||
args.chapter_title,
|
||
section_id,
|
||
),
|
||
)
|
||
print(f"已更新 {section_id} | {args.title}")
|
||
else:
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO chapters (
|
||
id, part_id, part_title, chapter_id, chapter_title,
|
||
section_title, content, word_count, is_free, price,
|
||
sort_order, status, edition_standard, edition_premium,
|
||
hot_score, created_at, updated_at
|
||
) VALUES (
|
||
%s, %s, %s, %s, %s,
|
||
%s, %s, %s, %s, %s,
|
||
%s, 'published', 1, 0,
|
||
0, NOW(), NOW()
|
||
)
|
||
""",
|
||
(
|
||
section_id,
|
||
args.part,
|
||
args.part_title,
|
||
args.chapter,
|
||
args.chapter_title,
|
||
args.title,
|
||
body,
|
||
word_count,
|
||
is_free,
|
||
price,
|
||
next_sort,
|
||
),
|
||
)
|
||
print(f"已创建 {section_id} | {args.title} | sort_order={next_sort}")
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|