#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 上传《一人公司——你能掌控的自由人生》到小程序。 book_key : solo-company 共 10 章 + 1 术语手册,分 4 个篇。 用法: python3 scripts/upload_solo_company_book.py --dry-run # 预览 python3 scripts/upload_solo_company_book.py # 执行上传 """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent BOOK_DIR = Path("/Users/karuo/Documents/个人/2、我写的书/《一人公司》") BOOK_KEY = "solo-company" BOOK_TITLE = "一人公司" BOOK_SUBTITLE = "你能掌控的自由人生" BOOK_EMOJI = "🏢" BOOK_SORT_ORDER = 10 # 篇章结构定义 PARTS = { "part-solo-1": { "title": "启程·方向", "chapters": { "chapter-solo-1": { "title": "找准方向,别再瞎忙", "sections": [1, 2, 3], } }, }, "part-solo-2": { "title": "实战·方法", "chapters": { "chapter-solo-2": { "title": "用对方法,高效变现", "sections": [4, 5, 6], } }, }, "part-solo-3": { "title": "进阶·系统", "chapters": { "chapter-solo-3": { "title": "搭建系统,持续增长", "sections": [7, 8, 9, 10], } }, }, "part-solo-appendix": { "title": "附录", "chapters": { "ch-solo-appendix": { "title": "术语手册", "sections": ["glossary"], } }, }, } # 章节文件映射 SECTION_FILES = { 1: "1、你越努力越穷,是因为你连方向都没找对.md", 2: "2、9个人干出千万业绩:一人公司到底怎么运转的.md", 3: "3、别再找项目了——你自己就是最值钱的产品.md", 4: "4、我从不追求原创,但我每篇内容都赚到了钱.md", 5: "5、30个账号每天自动来客户——你也能复制这套打法.md", 6: "6、你认识谁,比你会什么值钱10倍.md", 7: "7、搭一台你睡着了也在赚钱的机器.md", 8: "8、你赚多少钱,取决于你把自己当成什么人.md", 9: "9、一天重启你的人生:我用了10年验证的方法.md", 10: "10、别等准备好再开始——这游戏根本没有终点.md", "glossary": "术语手册:这本书里的黑话全在这.md", } # 章节标题(去掉序号前缀) SECTION_TITLES = { 1: "你越努力越穷,是因为你连方向都没找对", 2: "9个人干出千万业绩:一人公司到底怎么运转的", 3: "别再找项目了——你自己就是最值钱的产品", 4: "我从不追求原创,但我每篇内容都赚到了钱", 5: "30个账号每天自动来客户——你也能复制这套打法", 6: "你认识谁,比你会什么值钱10倍", 7: "搭一台你睡着了也在赚钱的机器", 8: "你赚多少钱,取决于你把自己当成什么人", 9: "一天重启你的人生:我用了10年验证的方法", 10: "别等准备好再开始——这游戏根本没有终点", "glossary": "术语手册:这本书里的黑话全在这", } DB_CONFIG = { "host": "56b4c23f6853c.gz.cdb.myqcloud.com", "port": 14413, "user": "cdb_outerroot", "password": "Zhiqun1984", "database": "soul_miniprogram", } def md_to_html(md_text: str) -> str: """将 Markdown 转为美观的 HTML(适配小程序 contentParser)""" lines = md_text.splitlines() # 去掉第一行 # 标题 if lines and lines[0].lstrip().startswith("# "): lines = lines[1:] html_parts: list[str] = [] i = 0 def inline_format(text: str) -> str: """处理行内格式:粗体、斜体、行内代码、链接""" # 粗体+斜体 text = re.sub(r"\*\*\*(.+?)\*\*\*", r"\1", text) # 粗体 text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) # 斜体 text = re.sub(r"(?\1", text) # 行内代码 text = re.sub(r"`(.+?)`", r"\1", text) # 链接 text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', text) return text while i < len(lines): line = lines[i] stripped = line.strip() # 空行跳过 if not stripped: i += 1 continue # 图片 → 跳过(小程序不支持外链图片) if re.match(r"^!\[.*\]\(.*\)$", stripped): i += 1 continue # 水平线 if stripped == "---": html_parts.append("
") i += 1 continue # h2 if stripped.startswith("## "): heading_text = inline_format(stripped[3:].strip()) html_parts.append(f"

{heading_text}

") i += 1 continue # h3 if stripped.startswith("### "): heading_text = inline_format(stripped[4:].strip()) html_parts.append(f"

{heading_text}

") i += 1 continue # 引用块 if stripped.startswith("> "): quote_lines = [] while i < len(lines) and lines[i].strip().startswith("> "): quote_lines.append(inline_format(lines[i].strip()[2:].strip())) i += 1 html_parts.append(f"
{'
'.join(quote_lines)}
") continue # 待办列表 - [ ] / - [x] if re.match(r"^- \[[ x]\] ", stripped): items = [] while i < len(lines) and re.match(r"^- \[[ x]\] ", lines[i].strip()): s = lines[i].strip() checked = s[3] == "x" text = inline_format(s[6:].strip()) check_mark = "✅" if checked else "☐" items.append(f"
  • {check_mark} {text}
  • ") i += 1 html_parts.append(f"") continue # 有序列表 if re.match(r"^\d+\.\s+", stripped): items = [] while i < len(lines): s = lines[i].strip() m = re.match(r"^\d+\.\s+(.+)", s) if not m: break text = inline_format(m.group(1)) items.append(f"
  • {text}
  • ") i += 1 # 如果下一行是缩进的续行,合并进当前 li while i < len(lines) and lines[i].strip() and not re.match(r"^\d+\.\s+", lines[i].strip()) and not lines[i].strip().startswith("- ") and not lines[i].strip().startswith("#") and not lines[i].strip().startswith(">") and lines[i].startswith(" "): items[-1] = items[-1].replace("", f"
    {inline_format(lines[i].strip())}") i += 1 html_parts.append(f"
      {''.join(items)}
    ") continue # 无序列表(含多级缩进 *) if re.match(r"^[-*]\s+", stripped) and not re.match(r"^- \[[ x]\] ", stripped): items = [] while i < len(lines): s = lines[i].strip() m = re.match(r"^[-*]\s+(.+)", s) if not m: break text = inline_format(m.group(1)) items.append(f"
  • {text}
  • ") i += 1 # 缩进续行 while i < len(lines) and lines[i].strip() and not re.match(r"^[-*]\s+", lines[i].strip()) and not re.match(r"^\d+\.\s+", lines[i].strip()) and not lines[i].strip().startswith("#") and not lines[i].strip().startswith(">"): if lines[i].startswith(" ") or lines[i].startswith("\t"): sub = lines[i].strip() sub_m = re.match(r"^[-*]\s+(.+)", sub) if sub_m: items[-1] = items[-1].replace("", f"
    • {inline_format(sub_m.group(1))}") else: items[-1] = items[-1].replace("", f"
    {inline_format(sub)}") i += 1 else: break html_parts.append(f"") continue # 普通段落 para_lines = [inline_format(stripped)] i += 1 html_parts.append(f"

    {para_lines[0]}

    ") continue return "\n".join(html_parts) def find_section_location(section_key) -> tuple[str, str, str, str]: """根据 section key 找到对应的 part/chapter 信息""" for part_id, part_info in PARTS.items(): for ch_id, ch_info in part_info["chapters"].items(): if section_key in ch_info["sections"]: return part_id, part_info["title"], ch_id, ch_info["title"] return "part-solo-1", "启程·方向", "chapter-solo-1", "找准方向,别再瞎忙" def main(): try: import pymysql except ImportError: print("需要: pip install pymysql", file=sys.stderr) sys.exit(1) dry_run = "--dry-run" in sys.argv price_free = 0.0 price_paid = 1.0 conn = pymysql.connect(**DB_CONFIG) cur = conn.cursor() # ── Step 1: 确保书籍存在 ── cur.execute("SELECT id FROM books WHERE book_key = %s", (BOOK_KEY,)) row = cur.fetchone() if row: book_id = row[0] print(f"📖 书籍已存在: id={book_id}, key={BOOK_KEY}") if not dry_run: cur.execute( "UPDATE books SET title=%s, subtitle=%s, icon_emoji=%s, sort_order=%s, status='published' WHERE id=%s", (BOOK_TITLE, BOOK_SUBTITLE, BOOK_EMOJI, BOOK_SORT_ORDER, book_id), ) conn.commit() else: if dry_run: print(f"📖 [dry] 将创建书籍: {BOOK_KEY} - {BOOK_TITLE}") book_id = 999 else: cur.execute( "INSERT INTO books (book_key, title, subtitle, icon_emoji, sort_order, status) VALUES (%s, %s, %s, %s, %s, 'published')", (BOOK_KEY, BOOK_TITLE, BOOK_SUBTITLE, BOOK_EMOJI, BOOK_SORT_ORDER), ) conn.commit() book_id = cur.lastrowid print(f"📖 已创建书籍: id={book_id}, key={BOOK_KEY}") # ── Step 2: 上传各章节 ── total_ok = 0 total_err = 0 sort_order = 0 for section_key in list(range(1, 11)) + ["glossary"]: filename = SECTION_FILES[section_key] title = SECTION_TITLES[section_key] section_id = f"solo-{section_key}" if isinstance(section_key, int) else "solo-glossary" part_id, part_title, chapter_id, chapter_title = find_section_location(section_key) filepath = BOOK_DIR / filename if not filepath.exists(): print(f" ⚠ 文件不存在: {filepath}") total_err += 1 continue raw = filepath.read_text(encoding="utf-8") html_content = md_to_html(raw) word_count = len(re.sub(r"<[^>]+>", "", html_content)) is_free = section_key in [1, "glossary"] price = price_free if is_free else price_paid if dry_run: preview = html_content[:80].replace("\n", " ") print(f" [dry] {section_id:16s} | {title[:35]:35s} | {word_count:5d}字 | {'免费' if is_free else '付费'} | {preview}…") total_ok += 1 sort_order += 1 continue try: cur.execute("SELECT mid FROM chapters WHERE id = %s", (section_id,)) existing = cur.fetchone() if existing: cur.execute( """UPDATE chapters SET book_id=%s, section_title=%s, content=%s, word_count=%s, part_id=%s, part_title=%s, chapter_id=%s, chapter_title=%s, price=%s, is_free=%s, sort_order=%s, status='published', edition_standard=1, edition_premium=0 WHERE id=%s""", (book_id, title, html_content, word_count, part_id, part_title, chapter_id, chapter_title, price, is_free, sort_order, section_id), ) conn.commit() print(f" ✓ [updated] {section_id:16s} | {title[:35]:35s} | {word_count:5d}字") else: cur.execute( """INSERT INTO chapters (id, book_id, section_title, content, word_count, part_id, part_title, chapter_id, chapter_title, price, is_free, sort_order, status, edition_standard, edition_premium) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'published', 1, 0)""", (section_id, book_id, title, html_content, word_count, part_id, part_title, chapter_id, chapter_title, price, is_free, sort_order), ) conn.commit() print(f" ✓ [created] {section_id:16s} | {title[:35]:35s} | {word_count:5d}字") total_ok += 1 except Exception as e: conn.rollback() print(f" ✗ 失败 {section_id}: {e}") total_err += 1 sort_order += 1 conn.close() print(f"\n{'='*60}") print(f"完成:成功 {total_ok} | 失败 {total_err}") if dry_run: print("(dry-run 模式,未实际写库)") else: print("✅ 上传完毕!请在管理端或小程序验证。") if __name__ == "__main__": main()