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()
|