精简「我的」页面菜单 + 数据库去重 + 内容上传接口
- 移除扫一扫/提现记录/设置独立菜单项,设置功能整合到页面内 - 新增绑定微信号、清缓存、退出登录的内联设置区 - 添加 content_upload.py:Skill 可直接调用上传内容到数据库 - 数据库已去重(196→69条)并添加唯一索引防止再次重复 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
275
content_upload.py
Normal file
275
content_upload.py
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Soul 内容上传接口
|
||||
可从 Cursor Skill / 命令行直接调用,将新内容写入数据库
|
||||
|
||||
用法:
|
||||
python3 content_upload.py --title "标题" --price 1.0 --content "正文" \
|
||||
--part part-1 --chapter chapter-1 --format markdown
|
||||
|
||||
python3 content_upload.py --json '{
|
||||
"title": "标题",
|
||||
"price": 1.0,
|
||||
"content": "正文内容...",
|
||||
"part_id": "part-1",
|
||||
"chapter_id": "chapter-1",
|
||||
"format": "markdown",
|
||||
"images": ["https://xxx.com/img1.png"]
|
||||
}'
|
||||
|
||||
python3 content_upload.py --list-structure # 查看篇章结构
|
||||
|
||||
环境依赖: pip install pymysql
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
print("需要安装 pymysql: pip3 install pymysql")
|
||||
sys.exit(1)
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": "56b4c23f6853c.gz.cdb.myqcloud.com",
|
||||
"port": 14413,
|
||||
"user": "cdb_outerroot",
|
||||
"password": "Zhiqun1984",
|
||||
"database": "soul_miniprogram",
|
||||
"charset": "utf8mb4",
|
||||
}
|
||||
|
||||
PART_MAP = {
|
||||
"part-1": "第一篇|真实的人",
|
||||
"part-2": "第二篇|真实的行业",
|
||||
"part-3": "第三篇|真实的错误",
|
||||
"part-4": "第四篇|真实的赚钱",
|
||||
"part-5": "第五篇|真实的社会",
|
||||
"appendix": "附录",
|
||||
"intro": "序言",
|
||||
"outro": "尾声",
|
||||
}
|
||||
|
||||
CHAPTER_MAP = {
|
||||
"chapter-1": "第1章|人与人之间的底层逻辑",
|
||||
"chapter-2": "第2章|人性困境案例",
|
||||
"chapter-3": "第3章|电商篇",
|
||||
"chapter-4": "第4章|内容商业篇",
|
||||
"chapter-5": "第5章|传统行业篇",
|
||||
"chapter-6": "第6章|我人生错过的4件大钱",
|
||||
"chapter-7": "第7章|别人犯的错误",
|
||||
"chapter-8": "第8章|底层结构",
|
||||
"chapter-9": "第9章|我在Soul上亲访的赚钱案例",
|
||||
"chapter-10": "第10章|未来职业的变化趋势",
|
||||
"chapter-11": "第11章|中国社会商业生态的未来",
|
||||
"appendix": "附录",
|
||||
"preface": "序言",
|
||||
"epilogue": "尾声",
|
||||
}
|
||||
|
||||
|
||||
def get_connection():
|
||||
return pymysql.connect(**DB_CONFIG)
|
||||
|
||||
|
||||
def list_structure():
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT part_id, part_title, chapter_id, chapter_title, COUNT(*) as sections
|
||||
FROM chapters
|
||||
GROUP BY part_id, part_title, chapter_id, chapter_title
|
||||
ORDER BY part_id, chapter_id
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
print("篇章结构:")
|
||||
for part_id, part_title, ch_id, ch_title, cnt in rows:
|
||||
print(f" {part_id} ({part_title}) / {ch_id} ({ch_title}) - {cnt}节")
|
||||
|
||||
cur.execute("SELECT COUNT(*) FROM chapters")
|
||||
total = cur.fetchone()[0]
|
||||
print(f"\n总计: {total} 节")
|
||||
conn.close()
|
||||
|
||||
|
||||
def generate_section_id(cur, chapter_id):
|
||||
"""根据 chapter 编号自动生成下一个 section id"""
|
||||
ch_num = re.search(r"\d+", chapter_id)
|
||||
if not ch_num:
|
||||
cur.execute("SELECT MAX(CAST(REPLACE(id, '.', '') AS UNSIGNED)) FROM chapters")
|
||||
max_id = cur.fetchone()[0] or 0
|
||||
return str(max_id + 1)
|
||||
|
||||
prefix = ch_num.group()
|
||||
cur.execute(
|
||||
"SELECT id FROM chapters WHERE id LIKE %s ORDER BY CAST(SUBSTRING_INDEX(id, '.', -1) AS UNSIGNED) DESC LIMIT 1",
|
||||
(f"{prefix}.%",),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
last_num = int(row[0].split(".")[-1])
|
||||
return f"{prefix}.{last_num + 1}"
|
||||
return f"{prefix}.1"
|
||||
|
||||
|
||||
def upload_content(data):
|
||||
title = data.get("title", "").strip()
|
||||
if not title:
|
||||
print("错误: 标题不能为空")
|
||||
return False
|
||||
|
||||
content = data.get("content", "").strip()
|
||||
if not content:
|
||||
print("错误: 内容不能为空")
|
||||
return False
|
||||
|
||||
price = float(data.get("price", 1.0))
|
||||
is_free = 1 if price == 0 else 0
|
||||
part_id = data.get("part_id", "part-1")
|
||||
chapter_id = data.get("chapter_id", "chapter-1")
|
||||
fmt = data.get("format", "markdown")
|
||||
images = data.get("images", [])
|
||||
section_id = data.get("id", "")
|
||||
|
||||
if images:
|
||||
for i, img_url in enumerate(images):
|
||||
placeholder = f"{{{{image_{i+1}}}}}"
|
||||
if placeholder in content:
|
||||
if fmt == "markdown":
|
||||
content = content.replace(placeholder, f"")
|
||||
else:
|
||||
content = content.replace(placeholder, img_url)
|
||||
|
||||
word_count = len(re.sub(r"\s+", "", content))
|
||||
|
||||
part_title = PART_MAP.get(part_id, part_id)
|
||||
chapter_title = CHAPTER_MAP.get(chapter_id, chapter_id)
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
if not section_id:
|
||||
section_id = generate_section_id(cur, chapter_id)
|
||||
|
||||
cur.execute("SELECT mid FROM chapters WHERE id = %s", (section_id,))
|
||||
existing = cur.fetchone()
|
||||
|
||||
try:
|
||||
if existing:
|
||||
cur.execute("""
|
||||
UPDATE chapters SET
|
||||
section_title = %s, content = %s, word_count = %s,
|
||||
is_free = %s, price = %s, part_id = %s, part_title = %s,
|
||||
chapter_id = %s, chapter_title = %s, status = 'published'
|
||||
WHERE id = %s
|
||||
""", (title, content, word_count, is_free, price, part_id, part_title,
|
||||
chapter_id, chapter_title, section_id))
|
||||
action = "更新"
|
||||
else:
|
||||
cur.execute("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM chapters")
|
||||
next_order = cur.fetchone()[0]
|
||||
|
||||
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)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'published')
|
||||
""", (section_id, part_id, part_title, chapter_id, chapter_title,
|
||||
title, content, word_count, is_free, price, next_order))
|
||||
action = "创建"
|
||||
|
||||
conn.commit()
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"action": action,
|
||||
"data": {
|
||||
"id": section_id,
|
||||
"title": title,
|
||||
"part": f"{part_id} ({part_title})",
|
||||
"chapter": f"{chapter_id} ({chapter_title})",
|
||||
"price": price,
|
||||
"is_free": bool(is_free),
|
||||
"word_count": word_count,
|
||||
"format": fmt,
|
||||
"images_count": len(images),
|
||||
}
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return True
|
||||
|
||||
except pymysql.err.IntegrityError as e:
|
||||
print(json.dumps({"success": False, "error": f"ID冲突: {e}"}, ensure_ascii=False))
|
||||
return False
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False))
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Soul 内容上传接口")
|
||||
parser.add_argument("--json", help="JSON格式的完整数据")
|
||||
parser.add_argument("--title", help="标题")
|
||||
parser.add_argument("--price", type=float, default=1.0, help="定价(0=免费)")
|
||||
parser.add_argument("--content", help="内容正文")
|
||||
parser.add_argument("--content-file", help="从文件读取内容")
|
||||
parser.add_argument("--format", default="markdown", choices=["markdown", "text", "html"])
|
||||
parser.add_argument("--part", default="part-1", help="所属篇 (part-1 ~ part-5)")
|
||||
parser.add_argument("--chapter", default="chapter-1", help="所属章 (chapter-1 ~ chapter-11)")
|
||||
parser.add_argument("--id", help="指定 section ID (如 1.6),不指定则自动生成")
|
||||
parser.add_argument("--images", nargs="*", help="图片URL列表")
|
||||
parser.add_argument("--list-structure", action="store_true", help="查看篇章结构")
|
||||
parser.add_argument("--list-chapters", action="store_true", help="列出所有章节")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_structure:
|
||||
list_structure()
|
||||
return
|
||||
|
||||
if args.list_chapters:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT id, section_title, is_free, price FROM chapters ORDER BY sort_order")
|
||||
for row in cur.fetchall():
|
||||
free_tag = "[免费]" if row[2] else f"[¥{row[3]}]"
|
||||
print(f" {row[0]} {row[1]} {free_tag}")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
if args.json:
|
||||
data = json.loads(args.json)
|
||||
else:
|
||||
if not args.title or (not args.content and not args.content_file):
|
||||
parser.print_help()
|
||||
print("\n错误: 需要 --title 和 --content (或 --content-file)")
|
||||
sys.exit(1)
|
||||
|
||||
content = args.content
|
||||
if args.content_file:
|
||||
with open(args.content_file, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
data = {
|
||||
"title": args.title,
|
||||
"price": args.price,
|
||||
"content": content,
|
||||
"format": args.format,
|
||||
"part_id": args.part,
|
||||
"chapter_id": args.chapter,
|
||||
"images": args.images or [],
|
||||
}
|
||||
if args.id:
|
||||
data["id"] = args.id
|
||||
|
||||
upload_content(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -33,13 +33,10 @@ Page({
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
|
||||
// 菜单列表:扫一扫整合在「我的」内;提现记录与设置合并为「设置与提现」入口
|
||||
menuList: [
|
||||
{ id: 'scan', title: '扫一扫', icon: '📷', iconBg: 'brand' },
|
||||
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 },
|
||||
{ id: 'referral', title: '推广中心', icon: '🎁', badge: '' },
|
||||
{ id: 'about', title: '关于作者', icon: '👤', iconBg: 'brand' },
|
||||
{ id: 'settings', title: '设置与提现', icon: '⚙️', iconBg: 'gray' }
|
||||
{ id: 'about', title: '关于作者', icon: '👤', iconBg: 'brand' }
|
||||
],
|
||||
|
||||
// 登录弹窗
|
||||
@@ -282,32 +279,15 @@ Page({
|
||||
handleMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
if (!this.data.isLoggedIn && id !== 'about' && id !== 'scan') {
|
||||
if (!this.data.isLoggedIn && id !== 'about') {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
|
||||
// 扫一扫:在「我的」内直接调起扫码
|
||||
if (id === 'scan') {
|
||||
wx.scanCode({
|
||||
onlyFromCamera: false,
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => {
|
||||
wx.showToast({ title: '已识别', icon: 'success' })
|
||||
if (res.result) {
|
||||
wx.setClipboardData({ data: res.result })
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
about: '/pages/about/about',
|
||||
settings: '/pages/settings/settings'
|
||||
about: '/pages/about/about'
|
||||
}
|
||||
|
||||
if (routes[id]) {
|
||||
@@ -315,6 +295,55 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 绑定微信号
|
||||
bindWechat() {
|
||||
wx.showModal({
|
||||
title: '绑定微信号',
|
||||
editable: true,
|
||||
placeholderText: '请输入微信号',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const wechat = res.content.trim()
|
||||
if (!wechat) return
|
||||
try {
|
||||
wx.setStorageSync('user_wechat', wechat)
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.wechat = wechat
|
||||
this.setData({ userInfo, userWechat: wechat })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
await app.request('/api/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, wechat }
|
||||
})
|
||||
wx.showToast({ title: '绑定成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.log('绑定微信号失败', e)
|
||||
wx.showToast({ title: '已保存到本地', icon: 'success' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
wx.showModal({
|
||||
title: '清除缓存',
|
||||
content: '确定要清除本地缓存吗?不会影响账号数据',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
const token = wx.getStorageSync('token')
|
||||
wx.clearStorageSync()
|
||||
if (userInfo) wx.setStorageSync('userInfo', userInfo)
|
||||
if (token) wx.setStorageSync('token', token)
|
||||
wx.showToast({ title: '缓存已清除', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到阅读页
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
@@ -107,6 +107,30 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账号设置 -->
|
||||
<view class="settings-card card">
|
||||
<view class="card-title">
|
||||
<text class="title-icon">⚙️</text>
|
||||
<text>账号设置</text>
|
||||
</view>
|
||||
<view class="settings-list">
|
||||
<view class="settings-item" bindtap="bindWechat">
|
||||
<text class="settings-label">绑定微信号</text>
|
||||
<view class="settings-right">
|
||||
<text class="settings-value">{{userWechat || '未绑定'}}</text>
|
||||
<text class="menu-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-item" bindtap="clearCache">
|
||||
<text class="settings-label">清除缓存</text>
|
||||
<text class="menu-arrow">→</text>
|
||||
</view>
|
||||
<view class="settings-item logout-item" bindtap="handleLogout">
|
||||
<text class="settings-label logout-text">退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 足迹内容 -->
|
||||
|
||||
@@ -994,3 +994,51 @@
|
||||
font-size: 28rpx;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
/* 账号设置 */
|
||||
.settings-card {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 28rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.settings-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.settings-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.settings-value {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.logout-item {
|
||||
justify-content: center;
|
||||
margin-top: 16rpx;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
color: #ff4d4f;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user