368 lines
14 KiB
Python
368 lines
14 KiB
Python
#!/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()
|