sync: Gitea 同步配置、miniprogram 页面逻辑、miniprogram 页面样式、脚本与配置、soul-admin 前端、soul-admin 页面、soul-api 接口逻辑、soul-api 路由等 | 原因: 多模块开发更新

This commit is contained in:
卡若
2026-03-08 08:00:39 +08:00
parent b7c35a89b0
commit 66cd90e511
43 changed files with 2559 additions and 809 deletions

View File

@@ -141,15 +141,22 @@ App({
}
},
// 绑定推荐码到用户
// 绑定推荐码到用户(自己的推荐码不请求接口,避免 400 与控制台报错)
async bindReferralCode(refCode) {
try {
const userId = this.globalData.userInfo?.id
if (!userId || !refCode) return
const myCode = this.getMyReferralCode()
if (myCode && this._normalizeReferralCode(refCode) === this._normalizeReferralCode(myCode)) {
console.log('[App] 跳过绑定:不能使用自己的推荐码')
this.globalData.pendingReferralCode = null
wx.removeStorageSync('pendingReferralCode')
return
}
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
// 调用API绑定推荐关系
const res = await this.request('/api/miniprogram/referral/bind', {
method: 'POST',
data: {
@@ -158,19 +165,31 @@ App({
},
silent: true
})
if (res.success) {
console.log('[App] 推荐码绑定成功')
// 仅记录当前已绑定的推荐码,用于展示/调试是否允许更换由后端根据30天规则判断
wx.setStorageSync('boundReferralCode', refCode)
this.globalData.pendingReferralCode = null
wx.removeStorageSync('pendingReferralCode')
}
} catch (e) {
console.error('[App] 绑定推荐码失败:', e)
const msg = (e && e.message) ? String(e.message) : ''
if (msg.indexOf('不能使用自己的推荐码') !== -1) {
console.log('[App] 跳过绑定:不能使用自己的推荐码')
this.globalData.pendingReferralCode = null
wx.removeStorageSync('pendingReferralCode')
} else {
console.error('[App] 绑定推荐码失败:', e)
}
}
},
// 推荐码归一化后比较(忽略大小写、短横线等)
_normalizeReferralCode(code) {
if (!code || typeof code !== 'string') return ''
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
},
// 根据业务 id 从 bookData 查 mid用于跳转
getSectionMid(sectionId) {
const list = this.globalData.bookData || []

View File

@@ -18,178 +18,9 @@ Page({
hasFullBook: false,
purchasedSections: [],
// 书籍数据 - 完整真实标题
totalSections: 62,
bookData: [
{
id: 'part-1',
number: '一',
title: '真实的人',
subtitle: '人与人之间的底层逻辑',
chapters: [
{
id: 'chapter-1',
title: '第1章人与人之间的底层逻辑',
sections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', isFree: true, price: 1 },
{ id: '1.2', title: '老墨:资源整合高手的社交方法', isFree: false, price: 1 },
{ id: '1.3', title: '笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统', isFree: false, price: 1 },
{ id: '1.4', title: '人性的三角结构:利益、情感、价值观', isFree: false, price: 1 },
{ id: '1.5', title: '沟通差的问题:为什么你说的别人听不懂', isFree: false, price: 1 }
]
},
{
id: 'chapter-2',
title: '第2章人性困境案例',
sections: [
{ id: '2.1', title: '相亲故事:你以为找的是人,实际是在找模式', isFree: false, price: 1 },
{ id: '2.2', title: '找工作迷茫者:为什么简历解决不了人生', isFree: false, price: 1 },
{ id: '2.3', title: '撸运费险:小钱困住大脑的真实心理', isFree: false, price: 1 },
{ id: '2.4', title: '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力', isFree: false, price: 1 },
{ id: '2.5', title: '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-2',
number: '二',
title: '真实的行业',
subtitle: '电商、内容、传统行业解析',
chapters: [
{
id: 'chapter-3',
title: '第3章电商篇',
sections: [
{ id: '3.1', title: '3000万流水如何跑出来(退税模式解析)', isFree: false, price: 1 },
{ id: '3.2', title: '供应链之王 vs 打工人:利润不在前端', isFree: false, price: 1 },
{ id: '3.3', title: '社区团购的底层逻辑', isFree: false, price: 1 },
{ id: '3.4', title: '跨境电商与退税套利', isFree: false, price: 1 }
]
},
{
id: 'chapter-4',
title: '第4章内容商业篇',
sections: [
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', isFree: false, price: 1 },
{ id: '4.2', title: '做号工厂:如何让一个号变成一个机器', isFree: false, price: 1 },
{ id: '4.3', title: '情绪内容为什么比专业内容更赚钱', isFree: false, price: 1 },
{ id: '4.4', title: '猫与宠物号:为什么宠物赛道永不过时', isFree: false, price: 1 },
{ id: '4.5', title: '直播间里的三种人:演员、技术工、系统流', isFree: false, price: 1 }
]
},
{
id: 'chapter-5',
title: '第5章传统行业篇',
sections: [
{ id: '5.1', title: '拍卖行抱朴一天240万的摇号生意', isFree: false, price: 1 },
{ id: '5.2', title: '土地拍卖:招拍挂背后的游戏规则', isFree: false, price: 1 },
{ id: '5.3', title: '地摊经济数字化一个月900块的餐车生意', isFree: false, price: 1 },
{ id: '5.4', title: '不良资产拍卖:我错过的一个亿佣金', isFree: false, price: 1 },
{ id: '5.5', title: '桶装水李总:跟物业合作的轻资产模式', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-3',
number: '三',
title: '真实的错误',
subtitle: '我和别人犯过的错',
chapters: [
{
id: 'chapter-6',
title: '第6章我人生错过的4件大钱',
sections: [
{ id: '6.1', title: '电商财税窗口2016年的千万级机会', isFree: false, price: 1 },
{ id: '6.2', title: '供应链金融:我不懂的杠杆游戏', isFree: false, price: 1 },
{ id: '6.3', title: '内容红利2019年我为什么没做抖音', isFree: false, price: 1 },
{ id: '6.4', title: '数据资产化:我还在观望的未来机会', isFree: false, price: 1 }
]
},
{
id: 'chapter-7',
title: '第7章别人犯的错误',
sections: [
{ id: '7.1', title: '投资房年轻人的迷茫:资金 vs 能力', isFree: false, price: 1 },
{ id: '7.2', title: '信息差骗局:永远有人靠卖学习赚钱', isFree: false, price: 1 },
{ id: '7.3', title: '在Soul找恋爱但想赚钱的人', isFree: false, price: 1 },
{ id: '7.4', title: '创业者的三种死法:冲动、轻信、没结构', isFree: false, price: 1 },
{ id: '7.5', title: '人情生意的终点:关系越多亏得越多', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-4',
number: '四',
title: '真实的赚钱',
subtitle: '底层结构与真实案例',
chapters: [
{
id: 'chapter-8',
title: '第8章底层结构',
sections: [
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', isFree: false, price: 1 },
{ id: '8.2', title: '价格杠杆:供应链与信息差', isFree: false, price: 1 },
{ id: '8.3', title: '时间杠杆:自动化 + AI', isFree: false, price: 1 },
{ id: '8.4', title: '情绪杠杆:咨询、婚恋、生意场', isFree: false, price: 1 },
{ id: '8.5', title: '社交杠杆:认识谁比你会什么更重要', isFree: false, price: 1 },
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', isFree: false, price: 1 }
]
},
{
id: 'chapter-9',
title: '第9章我在Soul上亲访的赚钱案例',
sections: [
{ id: '9.1', title: '游戏账号私域:账号即资产', isFree: false, price: 1 },
{ id: '9.2', title: '健康包模式:高复购、高毛利', isFree: false, price: 1 },
{ id: '9.3', title: '药物私域:长期关系赛道', isFree: false, price: 1 },
{ id: '9.4', title: '残疾机构合作:退税 × AI × 人力成本', isFree: false, price: 1 },
{ id: '9.5', title: '私域银行:粉丝即小股东', isFree: false, price: 1 },
{ id: '9.6', title: 'Soul派对房:陌生人成交的最快场景', isFree: false, price: 1 },
{ id: '9.7', title: '飞书中台:从聊天到成交的流程化体系', isFree: false, price: 1 },
{ id: '9.8', title: '餐饮女孩6万营收、1万利润的死撑生意', isFree: false, price: 1 },
{ id: '9.9', title: '电竞生态:从陪玩到签约到酒店的完整链条', isFree: false, price: 1 },
{ id: '9.10', title: '淘客大佬损耗30%的白色通道', isFree: false, price: 1 },
{ id: '9.11', title: '蔬菜供应链:农户才是最赚钱的人', isFree: false, price: 1 },
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', isFree: false, price: 1 },
{ id: '9.13', title: 'AI工具推广一个隐藏的高利润赛道', isFree: false, price: 1 },
{ id: '9.14', title: '大健康私域一个月150万的70后', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-5',
number: '五',
title: '真实的社会',
subtitle: '未来职业与商业生态',
chapters: [
{
id: 'chapter-10',
title: '第10章未来职业的变化趋势',
sections: [
{ id: '10.1', title: 'AI时代哪些工作会消失哪些会崛起', isFree: false, price: 1 },
{ id: '10.2', title: '一人公司:为什么越来越多人选择单干', isFree: false, price: 1 },
{ id: '10.3', title: '为什么链接能力会成为第一价值', isFree: false, price: 1 },
{ id: '10.4', title: '新型公司:Soul-飞书-线下的三位一体', isFree: false, price: 1 }
]
},
{
id: 'chapter-11',
title: '第11章中国社会商业生态的未来',
sections: [
{ id: '11.1', title: '私域经济:为什么流量越来越贵', isFree: false, price: 1 },
{ id: '11.2', title: '银发经济与孤独经济:两个被忽视的万亿市场', isFree: false, price: 1 },
{ id: '11.3', title: '流量红利的终局', isFree: false, price: 1 },
{ id: '11.4', title: '大模型 + 供应链的组合拳', isFree: false, price: 1 },
{ id: '11.5', title: '社会分层的最终逻辑', isFree: false, price: 1 }
]
}
]
}
],
// 书籍数据:以后台内容管理为准,仅用接口 /api/miniprogram/book/all-chapters 返回的数据
totalSections: 0,
bookData: [],
// 展开状态:默认不展开任何篇章,直接显示目录
expandedPart: null,
@@ -222,23 +53,37 @@ Page({
return p.includes('序言') || p.includes('尾声') || p.includes('附录')
},
// 一次请求拉取全量目录,同时更新 totalSections / bookData / dailyChapters
// 一次请求拉取全量目录,以后台内容管理为准;同时更新 totalSections / bookData / dailyChapters
async loadChaptersOnce() {
try {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
const rows = (res && res.data) || (res && res.chapters) || []
if (rows.length === 0) return
// 1. totalSections
// 无数据时清空目录,避免展示旧数据
if (rows.length === 0) {
app.globalData.bookData = []
wx.setStorageSync('bookData', [])
this.setData({
bookData: [],
totalSections: 0,
dailyChapters: [],
expandedPart: null
})
return
}
const totalSections = res.total ?? rows.length
app.globalData.bookData = rows
wx.setStorageSync('bookData', rows)
// 2. bookData过滤序言/尾声/附录,中间篇章按 part 聚合)
// bookData过滤序言/尾声/附录,按 part 聚合,篇章顺序按 sort_order 与后台一致含「2026每日派对干货」等
const filtered = rows.filter(r => !this._isFixedPart(r.partTitle || r.part_title))
const partMap = new Map()
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
filtered.forEach((r) => {
const pid = r.partId || r.part_id || 'part-1'
const cid = r.chapterId || r.chapter_id || 'chapter-1'
const sortOrder = r.sectionOrder ?? r.sort_order ?? 999999
if (!partMap.has(pid)) {
const partIdx = partMap.size
partMap.set(pid, {
@@ -246,10 +91,12 @@ Page({
number: numbers[partIdx] || String(partIdx + 1),
title: r.partTitle || r.part_title || '未分类',
subtitle: r.chapterTitle || r.chapter_title || '',
chapters: new Map()
chapters: new Map(),
minSortOrder: sortOrder
})
}
const part = partMap.get(pid)
if (sortOrder < part.minSortOrder) part.minSortOrder = sortOrder
if (!part.chapters.has(cid)) {
part.chapters.set(cid, {
id: cid,
@@ -267,13 +114,16 @@ Page({
isNew: r.isNew === true || r.is_new === true
})
})
const bookData = Array.from(partMap.values()).map(p => ({
...p,
const partList = Array.from(partMap.values())
partList.sort((a, b) => (a.minSortOrder ?? 999999) - (b.minSortOrder ?? 999999))
const bookData = partList.map((p, idx) => ({
id: p.id,
number: numbers[idx] || String(idx + 1),
title: p.title,
subtitle: p.subtitle,
chapters: Array.from(p.chapters.values())
}))
const firstPart = bookData[0] && bookData[0].id
// 3. dailyChapterssort_order > 62 的新增章节按更新时间取前20
const baseSort = 62
const daily = rows
.filter(r => (r.sectionOrder ?? r.sort_order ?? 0) > baseSort)
@@ -296,7 +146,14 @@ Page({
dailyChapters: daily,
expandedPart: this.data.expandedPart
})
} catch (e) { console.log('[Chapters] 加载目录失败:', e) }
} catch (e) {
console.log('[Chapters] 加载目录失败:', e)
this.setData({ bookData: [], totalSections: 0 })
}
},
onPullDownRefresh() {
this.loadChaptersOnce().then(() => wx.stopPullDownRefresh()).catch(() => wx.stopPullDownRefresh())
},
onShow() {

View File

@@ -1,6 +1,6 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"enablePullDownRefresh": true,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
}

View File

@@ -23,12 +23,8 @@ Page({
totalSections: 62,
bookData: [],
// 推荐章节
featuredSections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
],
// 推荐章节:以服务端推荐算法为准,不再预置写死内容
featuredSections: [],
// 最新章节(动态计算)
latestSection: null,

View File

@@ -4,7 +4,7 @@
<!-- 自定义导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 顶部区域按设计稿S 图标 + 标题副标题 | 点击链接卡若 + 章数 -->
<!-- 顶部区域按设计稿S 图标 + 标题副标题 | 点击链接卡若) -->
<view class="header">
<view class="header-content">
<view class="logo-section">
@@ -21,7 +21,6 @@
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
<text class="contact-text">点击链接卡若</text>
</view>
<view class="chapter-badge">{{totalSections}}章</view>
</view>
</view>
@@ -53,37 +52,6 @@
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
</view>
<!-- 阅读进度卡 -->
<view class="progress-card card">
<view class="progress-header">
<text class="progress-title">我的阅读</text>
<text class="progress-count">{{readCount}}/{{totalSections}}章</text>
</view>
<view class="progress-bar-wrapper">
<view class="progress-bar-bg">
<view class="progress-bar-fill" style="width: {{totalSections > 0 ? (readCount / totalSections) * 100 : 0}}%;"></view>
</view>
</view>
<view class="progress-stats">
<view class="stat-item">
<text class="stat-value brand-color">{{readCount}}</text>
<text class="stat-label">已读</text>
</view>
<view class="stat-item">
<text class="stat-value">{{totalSections - readCount}}</text>
<text class="stat-label">待读</text>
</view>
<view class="stat-item">
<text class="stat-value">{{partCount}}</text>
<text class="stat-label">篇章</text>
</view>
<view class="stat-item">
<text class="stat-value">{{totalSections}}</text>
<text class="stat-label">章节</text>
</view>
</view>
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
<view class="section">
<view class="section-header">
@@ -170,7 +138,6 @@
</view>
<view class="timeline-right">
<text class="timeline-price">¥{{item.price}}</text>
<text class="timeline-date">{{item.dateStr}}</text>
</view>
</view>
</view>

View File

@@ -12,7 +12,7 @@ const app = getApp()
// 导师顾问:跳转到存客宝添加微信
// 团队招募:跳转到存客宝添加微信
let MATCH_TYPES = [
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'partner', label: '超级个体', matchLabel: '超级个体', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '导师顾问', icon: '❤️', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: '🎮', matchFromDB: true, showJoinAfterMatch: true }
@@ -27,7 +27,7 @@ Page({
// 匹配类型
matchTypes: MATCH_TYPES,
selectedType: 'partner',
currentTypeLabel: '找伙伴',
currentTypeLabel: '超级个体',
// 用户状态
isLoggedIn: false,

View File

@@ -111,31 +111,26 @@ Page({
const { isLoggedIn, userInfo } = app.globalData
if (isLoggedIn && userInfo) {
const readIds = app.globalData.readSectionIds || []
const recentList = readIds.slice(-5).reverse().map(id => ({
id,
mid: app.getSectionMid(id),
title: `章节 ${id}`
}))
const userId = userInfo.id || ''
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
// 先设基础信息;收益由 loadMyEarnings 专用接口拉取,加载前用 - 占位
// 先设基础信息;阅读统计与收益再分别从后端刷新
this.setData({
isLoggedIn: true,
userInfo,
userIdShort,
userWechat,
readCount: Math.min(app.getReadCount(), this.data.totalSections || 62),
readCount: 0,
referralCount: userInfo.referralCount || 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: true,
recentChapters: recentList,
totalReadTime: Math.floor(Math.random() * 200) + 50
recentChapters: [],
totalReadTime: 0,
matchHistory: 0
})
this.loadDashboardStats()
this.loadMyEarnings()
this.loadPendingConfirm()
this.loadVipStatus()
@@ -149,11 +144,48 @@ Page({
earnings: '-',
pendingEarnings: '-',
earningsLoading: false,
recentChapters: []
recentChapters: [],
totalReadTime: 0,
matchHistory: 0
})
}
},
async loadDashboardStats() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request({
url: `/api/miniprogram/user/dashboard-stats?userId=${encodeURIComponent(userId)}`,
silent: true
})
if (!res?.success || !res.data) return
const readSectionIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds : []
app.globalData.readSectionIds = readSectionIds
wx.setStorageSync('readSectionIds', readSectionIds)
const recentChapters = Array.isArray(res.data.recentChapters)
? res.data.recentChapters.map((item) => ({
id: item.id,
mid: item.mid || app.getSectionMid(item.id),
title: item.title || `章节 ${item.id}`
}))
: []
this.setData({
readCount: Number(res.data.readCount || 0),
totalReadTime: Number(res.data.totalReadMinutes || 0),
matchHistory: Number(res.data.matchHistory || 0),
recentChapters
})
} catch (e) {
console.log('[My] 拉取阅读统计失败:', e && e.message)
}
},
// 拉取待确认收款列表(用于「确认收款」按钮)
async loadPendingConfirm() {
const userInfo = app.globalData.userInfo

View File

@@ -442,31 +442,54 @@ Page({
},
// 加载导航
loadNavigation(id) {
const sectionOrder = [
'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
'2.1', '2.2', '2.3', '2.4', '2.5',
'3.1', '3.2', '3.3', '3.4',
'4.1', '4.2', '4.3', '4.4', '4.5',
'5.1', '5.2', '5.3', '5.4', '5.5',
'6.1', '6.2', '6.3', '6.4',
'7.1', '7.2', '7.3', '7.4', '7.5',
'8.1', '8.2', '8.3', '8.4', '8.5', '8.6',
'9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '9.7', '9.8', '9.9', '9.10', '9.11', '9.12', '9.13', '9.14',
'10.1', '10.2', '10.3', '10.4',
'11.1', '11.2', '11.3', '11.4', '11.5',
'epilogue'
]
const currentIndex = sectionOrder.indexOf(id)
const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
this.setData({
prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
})
// 加载导航:以后台章节真实 sort_order 为准
async loadNavigation(id) {
try {
let rows = app.globalData.bookData || []
if (!Array.isArray(rows) || rows.length === 0) {
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
rows = (res && (res.data || res.chapters)) || []
if (Array.isArray(rows) && rows.length > 0) {
app.globalData.bookData = rows
}
}
if (!Array.isArray(rows) || rows.length === 0) {
this.setData({ prevSection: null, nextSection: null })
return
}
const orderedSections = rows
.slice()
.sort((a, b) => {
const sortA = a.sortOrder ?? a.sort_order ?? 999999
const sortB = b.sortOrder ?? b.sort_order ?? 999999
if (sortA !== sortB) return sortA - sortB
const midA = a.mid ?? a.MID ?? 0
const midB = b.mid ?? b.MID ?? 0
if (midA !== midB) return midA - midB
return String(a.id || '').localeCompare(String(b.id || ''))
})
.map((item) => ({
id: item.id,
mid: item.mid ?? item.MID ?? 0,
title: item.sectionTitle || item.section_title || item.title || item.chapterTitle || this.getSectionTitle(item.id)
}))
const currentIndex = orderedSections.findIndex((item) => item.id === id)
if (currentIndex === -1) {
this.setData({ prevSection: null, nextSection: null })
return
}
const prevSection = currentIndex > 0 ? orderedSections[currentIndex - 1] : null
const nextSection = currentIndex < orderedSections.length - 1 ? orderedSections[currentIndex + 1] : null
this.setData({ prevSection, nextSection })
} catch (e) {
console.error('[Read] 加载上下篇导航失败:', e)
this.setData({ prevSection: null, nextSection: null })
}
},
// 返回(从分享进入无栈时回首页)
@@ -523,8 +546,9 @@ Page({
const { section, sectionId, sectionMid } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const shareTitle = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
const safeSectionTitle = this.getSafeShareText(section?.title || '')
const shareTitle = safeSectionTitle
? `📚 ${safeSectionTitle.length > 20 ? safeSectionTitle.slice(0, 20) + '...' : safeSectionTitle}`
: '📚 Soul创业派对 - 真实商业故事'
return {
title: shareTitle,
@@ -533,17 +557,51 @@ Page({
}
},
// 分享到朋友圈:文案用本章节正文前文,便于不折叠时展示(朋友圈卡片标题约 64 字节)
// 分享到朋友圈:使用安全模板,避免正文敏感词触发平台风控提示
onShareTimeline() {
const { sectionId, sectionMid, shareTimelineTitle, section } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const title = (shareTimelineTitle && shareTimelineTitle.trim())
? (shareTimelineTitle.length > 32 ? shareTimelineTitle.slice(0, 32) + '...' : shareTimelineTitle)
: ((section?.title || '').trim().slice(0, 32) || 'Soul创业派对 - 真实商业故事')
const safeSectionTitle = this.getSafeShareText(section?.title || '')
const safePreviewTitle = this.getSafeShareText(shareTimelineTitle || '')
// 优先使用章节名,兜底使用预览文案;都为空时使用固定安全标题
const baseTitle = safeSectionTitle || safePreviewTitle || 'Soul创业派对真实商业案例'
const title = baseTitle.length > 32 ? `${baseTitle.slice(0, 32)}...` : baseTitle
return { title, query: ref ? `${q}&ref=${ref}` : q }
},
// 清洗分享文案,规避高风险收益承诺类词汇,降低平台风控误判
getSafeShareText(text = '') {
let safeText = String(text || '').trim()
if (!safeText) return ''
// 统一替换常见风险词
const riskyPatterns = [
/90%\s*收益/gi,
/百分之九十\s*收益/gi,
/收益/gi,
/锁定\s*\d+\s*天/gi,
/锁定期/gi,
/稳赚/gi,
/保本/gi,
/高回报/gi,
/返利/gi,
/理财/gi,
/投资/gi
]
riskyPatterns.forEach((pattern) => {
safeText = safeText.replace(pattern, '')
})
// 去掉多余空白和标点残留
safeText = safeText
.replace(/[,。;、,:\-\s]{2,}/g, ' ')
.replace(/^[,。;、,:\-\s]+|[,。;、,:\-\s]+$/g, '')
.trim()
return safeText
},
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
showLoginModal() {
try {
@@ -992,14 +1050,16 @@ Page({
// 跳转到上一篇
goToPrev() {
if (this.data.prevSection) {
wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
const q = this.data.prevSection.mid ? `mid=${this.data.prevSection.mid}` : `id=${this.data.prevSection.id}`
wx.redirectTo({ url: `/pages/read/read?${q}` })
}
},
// 跳转到下一篇
goToNext() {
if (this.data.nextSection) {
wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
const q = this.data.nextSection.mid ? `mid=${this.data.nextSection.mid}` : `id=${this.data.nextSection.id}`
wx.redirectTo({ url: `/pages/read/read?${q}` })
}
},

View File

@@ -81,8 +81,8 @@
<view class="action-section">
<view class="action-row-inline">
<button class="action-btn-inline btn-share-inline" open-type="share">
<text class="action-icon-small">📷</text>
<text class="action-text-small">分享到朋友圈</text>
<text class="action-icon-small">👥</text>
<text class="action-text-small">分享好友</text>
</button>
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
<text class="action-icon-small">🖼️</text>

View File

@@ -37,6 +37,9 @@ class ReadingTracker {
// 开始定期上报每30秒
this.startProgressReport()
// 立即上报一次「打开/点击」,确保内容管理后台的「点击」数据有记录(与 reading_progress 表直接捆绑)
setTimeout(() => this.reportProgressToServer(false), 0)
}
/**