Files
Mycontent/scripts/upload_solo_company_book.py
卡若 b5f5180654 chore: 以本地工作区为准全量快照同步 GitHub
包含小程序、管理端、soul-api、脚本与静态资源等当前本地全部已跟踪与新增文件(排除 .DS_Store 与 .obsidian)。

Made-with: Cursor
2026-04-13 11:49:38 +08:00

368 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"<strong><em>\1</em></strong>", text)
# 粗体
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
# 斜体
text = re.sub(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"<em>\1</em>", text)
# 行内代码
text = re.sub(r"`(.+?)`", r"<code>\1</code>", text)
# 链接
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', 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("<hr>")
i += 1
continue
# h2
if stripped.startswith("## "):
heading_text = inline_format(stripped[3:].strip())
html_parts.append(f"<h2>{heading_text}</h2>")
i += 1
continue
# h3
if stripped.startswith("### "):
heading_text = inline_format(stripped[4:].strip())
html_parts.append(f"<h3>{heading_text}</h3>")
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"<blockquote>{'<br>'.join(quote_lines)}</blockquote>")
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"<li>{check_mark} {text}</li>")
i += 1
html_parts.append(f"<ul>{''.join(items)}</ul>")
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"<li>{text}</li>")
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("</li>", f"<br>{inline_format(lines[i].strip())}</li>")
i += 1
html_parts.append(f"<ol>{''.join(items)}</ol>")
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"<li>{text}</li>")
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("</li>", f"<br>• {inline_format(sub_m.group(1))}</li>")
else:
items[-1] = items[-1].replace("</li>", f"<br>{inline_format(sub)}</li>")
i += 1
else:
break
html_parts.append(f"<ul>{''.join(items)}</ul>")
continue
# 普通段落
para_lines = [inline_format(stripped)]
i += 1
html_parts.append(f"<p>{para_lines[0]}</p>")
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()