#!/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()