This commit is contained in:
Alex-larget
2026-03-24 15:44:08 +08:00
parent 346e8ab057
commit 28ad08da84
62 changed files with 814 additions and 840 deletions

View File

@@ -11,6 +11,8 @@ const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version正式版否则用本字段
const APP_DISPLAY_VERSION = '1.7.1'
// 章节总数API 获取失败时的统一兜底,避免 90/62 混用
const FALLBACK_TOTAL_SECTIONS = 62
App({
globalData: {
@@ -40,7 +42,7 @@ App({
// 书籍数据bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters
bookData: null,
totalSections: 90,
totalSections: FALLBACK_TOTAL_SECTIONS, // 来自 book/parts 或 book/stats失败时用常量
// 购买记录
purchasedSections: [],
@@ -910,8 +912,12 @@ App({
if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
this.logout()
}
showError(msg)
reject(new Error(msg))
const err = new Error(msg)
err.response = data
const skipToast = data.needBindWechat === true || data.needBind === true ||
(data.errorCode && String(data.errorCode).indexOf('ERR_') === 0)
if (!silent && !skipToast) showError(msg)
reject(err)
return
}
resolve(data)
@@ -947,8 +953,12 @@ App({
}
// 4xx/5xx优先用返回体的 message/error
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
showError(msg)
reject(new Error(msg))
const err = new Error(msg)
if (data && typeof data === 'object') err.response = data
const skipToast = data && (data.needBindWechat === true || data.needBind === true ||
(data.errorCode && String(data.errorCode).indexOf('ERR_') === 0))
if (!silent && !skipToast) showError(msg)
reject(err)
},
fail: (err) => {
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
@@ -1124,6 +1134,11 @@ App({
if (res.success && res.data) {
const user = res.data.user
const oid = res.data.openId || user.openId
if (oid) {
this.globalData.openId = oid
wx.setStorageSync('openId', oid)
}
this.globalData.userInfo = user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = user.purchasedSections || []
@@ -1154,6 +1169,7 @@ App({
} else {
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
setTimeout(() => this.connectWsHeartbeat(), 2000)
}
return res.data
@@ -1198,9 +1214,10 @@ App({
return (this.globalData.readSectionIds || []).length
},
// 获取章节总数
// 获取章节总数(优先 API 已加载值,失败时返回统一兜底常量)
getTotalSections() {
return this.globalData.totalSections
const v = this.globalData.totalSections
return (v != null && v > 0) ? v : FALLBACK_TOTAL_SECTIONS
},
// 切换TabBar

View File

@@ -19,7 +19,7 @@ Page({
},
bookInfo: {
title: '一场Soul的创业实验',
totalChapters: 62,
totalChapters: 0, // 来自 book/stats 或 app.getTotalSections()
parts: [
{ name: '真实的人', chapters: 10 },
{ name: '真实的行业', chapters: 15 },
@@ -34,7 +34,8 @@ Page({
onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight
statusBarHeight: app.globalData.statusBarHeight,
'bookInfo.totalChapters': app.getTotalSections()
})
this.loadAuthor()
this.loadBookStats()
@@ -59,7 +60,7 @@ Page({
title: d.title || '',
bio: d.bio || '',
stats: Array.isArray(d.stats) ? d.stats : [
{ label: '商业案例', value: '62' },
{ label: '商业案例', value: String(app.getTotalSections()) },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
],
@@ -81,7 +82,7 @@ Page({
try {
const res = await app.request({ url: '/api/miniprogram/book/stats', silent: true })
if (res?.success && res.data) {
const total = res.data?.totalChapters || 62
const total = res.data?.totalChapters ?? app.getTotalSections()
this.setData({ 'bookInfo.totalChapters': total })
const stats = this.data.author?.stats || []
const idx = stats.findIndex((s) => s && (s.label === '商业案例' || s.label === '章节'))

View File

@@ -3,6 +3,7 @@
* 改造后:发起人支付,好友领取。支持单页模式引导、登录检测。
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
Page({
data: {
@@ -180,28 +181,10 @@ Page({
}
const payParams = res.data.payParams
const orderSn = res.data.orderSn
// 与正常章节支付一致:只传 5 个必需参数,不传 appId 等多余字段
await new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: resolve,
fail: reject
})
})
await soulBridge.requestWxJsapiPayment(payParams)
wx.showToast({ title: '支付成功', icon: 'success' })
this.setData({ paying: false })
// 主动同步订单状态(与 read 页一致)
if (orderSn) {
try {
await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true })
} catch (e) {
console.warn('[GiftPay] 主动同步订单失败:', e)
}
}
await soulBridge.syncOrderStatusQuery(app, orderSn)
this.loadDetail()
} catch (e) {
this.setData({ paying: false })

View File

@@ -33,16 +33,12 @@ Page({
hasFullBook: false,
readCount: 0,
// 书籍数据
totalSections: 62,
// 书籍数据totalSections 来自 book/parts初始用 app.getTotalSections() 兜底)
totalSections: 0, // onLoad 后由 loadBookData 更新
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: '真实的赚钱' }
],
// 推荐章节(来自 recommended/hot API初始为空避免占位错误
featuredSections: [],
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters
bannerSection: null,
@@ -277,21 +273,21 @@ Page({
if (res?.success) {
const total = res.totalSections ?? 0
const parts = res.parts || []
app.globalData.totalSections = total || 62
app.globalData.totalSections = (total != null && total > 0) ? total : app.getTotalSections()
this.setData({
totalSections: app.globalData.totalSections,
partCount: parts.length || 5
})
}
} catch (e) {
this.setData({ totalSections: app.globalData.totalSections || 62, partCount: 5 })
this.setData({ totalSections: app.getTotalSections(), partCount: 5 })
}
},
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
const readCount = Math.min(app.getReadCount(), this.data.totalSections || app.getTotalSections())
this.setData({
isLoggedIn,
hasFullBook,

View File

@@ -5,6 +5,7 @@
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
const { checkAndExecute } = require('../../utils/ruleEngine.js')
const { trackClick } = require('../../utils/trackClick')
@@ -426,6 +427,7 @@ Page({
// 从数据库获取真实用户匹配
let matchedUser = null
let matchProfileError = ''
try {
const res = await app.request({ url: '/api/miniprogram/match/users', silent: true,
method: 'POST',
@@ -434,20 +436,38 @@ Page({
userId: app.globalData.userInfo?.id || ''
}
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
}
} catch (e) {
console.log('[Match] 数据库匹配失败:', e)
const r = e.response || {}
if (r.errorCode === 'ERR_PROFILE_INCOMPLETE') {
matchProfileError = r.message || '请先完善手机号或微信号后再发起匹配'
}
}
// 延迟显示结果(模拟匹配过程)
const delay = Math.random() * 2000 + 2000
setTimeout(() => {
clearInterval(timer)
if (matchProfileError) {
this.setData({ isMatching: false })
wx.showModal({
title: '完善资料',
content: matchProfileError,
confirmText: '去完善',
showCancel: false,
success: (mr) => {
if (mr.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
// 如果没有匹配到用户,提示用户
if (!matchedUser) {
this.setData({ isMatching: false })
@@ -632,7 +652,27 @@ Page({
wx.showToast({ title: res.error || '加入失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
const r = e.response || {}
if (r.errorCode === 'ERR_REQUIRE_PURCHASE') {
wx.showModal({
title: '需要先购买',
content: r.message || '请先购买章节或解锁全书后再使用资源对接',
confirmText: '去购买',
cancelText: '取消',
success: (mr) => { if (mr.confirm) this.goToChapters() }
})
} else if (r.errorCode === 'ERR_PROFILE_INCOMPLETE') {
wx.showModal({
title: '完善资料',
content: r.message || '请先完善资料',
confirmText: '去完善',
success: (mr) => {
if (mr.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
} else {
wx.showToast({ title: e.message || '网络异常,请重试', icon: 'none' })
}
} finally {
this.setData({ isJoining: false })
}
@@ -670,9 +710,7 @@ Page({
return
}
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 调用支付接口购买匹配次数
const referralCode = soulBridge.getReferralCodeForPay(app)
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -685,17 +723,11 @@ Page({
referralCode: referralCode || undefined
}
})
if (res.success && res.data?.payParams) {
// 调用微信支付
await new Promise((resolve, reject) => {
wx.requestPayment({
...res.data.payParams,
success: resolve,
fail: reject
})
})
await soulBridge.requestWxJsapiPayment(res.data.payParams)
await soulBridge.syncOrderStatusQuery(app, res.data.orderSn)
// 支付成功,增加匹配次数
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)

View File

@@ -8,6 +8,7 @@
* 点头像:若后台 persons.user_id 已绑定则带 ckbLeadToken走存客宝 CKBLead与阅读页 @ 一致)
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
Page({
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true },
@@ -188,66 +189,14 @@ Page({
wx.showToast({ title: '暂未公开联系方式', icon: 'none' })
},
/** 与 read 页 _doMentionAddFriend 一致targetUserId = Person.token */
/** 与阅读页 @mention 同链路soulBridge.submitCkbLead */
async _doCkbLeadSubmit(targetUserId, targetNickname) {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '完善资料',
content: '请先填写手机号(必填),以便对方通过获客计划联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source: 'member_detail_avatar'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetUserId,
targetNickname,
source: 'member_detail_avatar',
phoneModalContent: '请先填写手机号(必填),以便对方通过获客计划联系您'
})
},
_ensureUnlockedForLink(field) {

View File

@@ -9,6 +9,12 @@ const { formatStatNum } = require('../../utils/util.js')
const { trackClick } = require('../../utils/trackClick')
const { cleanSingleLineField } = require('../../utils/contentParser.js')
/** 与 referral 一致:提现需已绑定微信号(便于到账核对) */
function hasWechatIdBound() {
const ui = app.globalData.userInfo
return !!(ui && (ui.wechat || ui.wechatId || wx.getStorageSync('user_wechat')))
}
/** 是否视为「单章解锁」类订单(排除全书/VIP 等聚合商品名) */
function isSectionUnlockOrder(o) {
const name = String(o.product_name || o.title || '').trim()
@@ -35,7 +41,7 @@ Page({
userInfo: null,
// 统计数据
totalSections: 62,
totalSections: 0, // 来自 app.getTotalSections() 或 dashboard-stats
readCount: 0,
referralCount: 0,
earnings: '-',
@@ -961,7 +967,19 @@ Page({
wx.showToast({ title: '暂无可提现金额', icon: 'none' })
return
}
await this.ensureContactInfo(() => this.doWithdraw(amount))
if (!hasWechatIdBound()) {
wx.showModal({
title: '请先绑定微信号',
content: '提现需先绑定微信号,便于到账核对。请到「设置」中绑定后再提现。',
confirmText: '去绑定',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
return
}
this.doWithdraw(amount)
},
async doWithdraw(amount) {
@@ -980,6 +998,16 @@ Page({
this.loadWalletBalance()
} catch (e) {
wx.hideLoading()
const r = e.response || {}
if (r.needBind || r.needBindWechat) {
wx.showModal({
title: r.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: r.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (mr) => { if (mr.confirm) wx.navigateTo({ url: '/pages/settings/settings' }) }
})
return
}
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
}
}

View File

@@ -17,6 +17,7 @@ const accessManager = require('../../utils/chapterAccessManager')
const readingTracker = require('../../utils/readingTracker')
const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
const soulBridge = require('../../utils/soulBridge.js')
const { trackClick } = require('../../utils/trackClick')
const app = getApp()
@@ -92,7 +93,7 @@ Page({
// 价格
sectionPrice: 1,
fullBookPrice: 9.9,
totalSections: 62,
totalSections: 0, // 来自 app.getTotalSections() 或 book/parts
// 弹窗
showShareModal: false,
@@ -121,6 +122,9 @@ Page({
// 审核模式:隐藏购买按钮
auditMode: false,
// 分润比例(来自 config.shareRate用于分享提示文案
shareRate: 90,
// 好友从代付分享进入:待自动领取的 requestSn
pendingGiftRequestSn: '',
},
@@ -189,7 +193,8 @@ Page({
sectionMid: mid || null,
loading: true,
accessState: 'unknown',
pendingGiftRequestSn: giftRequestSn || ''
pendingGiftRequestSn: giftRequestSn || '',
totalSections: app.getTotalSections()
})
if (ref) {
@@ -200,9 +205,12 @@ Page({
try {
const config = await accessManager.fetchLatestConfig()
const shareRate = (config && config.shareRate != null) ? config.shareRate : 90
this.setData({
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
fullBookPrice: config.prices?.fullbook ?? 9.9,
shareRate,
totalSections: app.getTotalSections()
})
// 统一:先拉章节数据,用 isFree/price===0 判断免费
@@ -675,71 +683,13 @@ Page({
})
},
// 边界:未登录→去登录;无手机/微信号→去资料编辑;重复同一人→本地 key 去重
// 存客宝留资:统一 soulBridge.submitCkbLead与会员详情点头像同链路
async _doMentionAddFriend(targetUserId, targetNickname) {
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
}
})
return
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '完善资料',
content: '请先填写手机号(必填),以便对方联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source: 'article_mention'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetUserId,
targetNickname,
source: 'article_mention'
})
},
// 分享弹窗
@@ -848,24 +798,8 @@ Page({
}
const payParams = payRes.data.payParams
const orderSn = payRes.data.orderSn
await new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: resolve,
fail: reject
})
})
// 3) 主动同步(与其他支付流程一致)
if (orderSn) {
try {
await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true })
} catch (e) {}
}
await soulBridge.requestWxJsapiPayment(payParams)
await soulBridge.syncOrderStatusQuery(app, orderSn)
wx.showToast({ title: '支付成功', icon: 'success' })
this.setData({ giftPaid: true, giftRequestSn: requestSn, giftPaying: false })
@@ -883,8 +817,7 @@ Page({
// 复制链接
copyLink() {
const userInfo = app.globalData.userInfo
const referralCode = userInfo?.referralCode || ''
const referralCode = app.getMyReferralCode() || ''
const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
wx.setClipboardData({
@@ -900,9 +833,10 @@ Page({
copyShareText() {
const { section } = this.data
const total = app.getTotalSections()
const shareText = `🔥 刚看完这篇《${section?.title || '卡若创业派对'}》,太上头了!
62个真实商业案例每个都是从0到1的实战经验。私域运营、资源整合、商业变现干货满满。
${total}个真实商业案例每个都是从0到1的实战经验。私域运营、资源整合、商业变现干货满满。
推荐给正在创业或想创业的朋友,搜"卡若创业派对"小程序就能看!
@@ -1198,7 +1132,7 @@ Page({
try {
// 0. 尝试余额支付(若余额足够)
const userId = app.globalData.userInfo?.id
const referralCode = wx.getStorageSync('referral_code') || ''
const referralCode = soulBridge.getReferralCodeForPay(app)
if (userId) {
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
@@ -1262,14 +1196,9 @@ Page({
let paymentData = null
try {
// 获取章节完整名称用于支付描述
const sectionTitle = this.data.section?.title || sectionId
const description = type === 'fullbook'
? '《一场Soul的创业实验》全书'
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
// 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账
const referralCode = wx.getStorageSync('referral_code') || ''
const description = soulBridge.buildSectionPayDescription(type, sectionId, sectionTitle)
const referralCode = soulBridge.getReferralCodeForPay(app)
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -1321,18 +1250,11 @@ Page({
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
try {
await this.callWechatPay(paymentData)
// 4. 【关键】主动向微信查询订单状态并同步到本地(不依赖回调,解决订单一直 created 的问题)
await soulBridge.requestWxJsapiPayment(paymentData)
const orderSn = paymentData._orderSn || paymentData.orderSn
if (orderSn) {
try {
await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true })
console.log('[Pay] 已主动同步订单状态:', orderSn)
} catch (e) {
console.warn('[Pay] 主动同步订单失败,继续刷新购买状态:', e)
}
}
await soulBridge.syncOrderStatusQuery(app, orderSn)
if (orderSn) console.log('[Pay] 已主动同步订单状态:', orderSn)
// 5. 【标准流程】刷新权限并解锁内容
console.log('[Pay] 微信支付成功!')
@@ -1467,21 +1389,6 @@ Page({
}
},
// 调用微信支付
callWechatPay(paymentData) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType || 'MD5',
paySign: paymentData.paySign,
success: resolve,
fail: reject
})
})
},
// 跳转到上一篇
goToPrev() {
if (this.data.prevSection) {
@@ -1728,11 +1635,13 @@ Page({
try {
const config = await accessManager.fetchLatestConfig()
const shareRate = (config && config.shareRate != null) ? config.shareRate : 90
this.setData({
sectionPrice: config.prices?.section ?? 1,
fullBookPrice: config.prices?.fullbook ?? 9.9
fullBookPrice: config.prices?.fullbook ?? 9.9,
shareRate
})
// 重新拉取章节,用 isFree/price 判断免费
const chapterRes = await app.request({
url: this._getChapterUrl({}),

View File

@@ -106,7 +106,7 @@
</view>
</view>
<view class="share-tip-inline" wx:if="{{!auditMode}}">
<text class="share-tip-text">分享后好友购买,你可获得 90% 收益</text>
<text class="share-tip-text">分享后好友购买,你可获得 {{shareRate || 90}}% 收益</text>
</view>
</view>

View File

@@ -63,7 +63,7 @@ Page({
posterReferralLink: '',
posterNickname: '',
posterNicknameInitial: '',
posterCaseCount: 62
posterCaseCount: 0 // 来自 app.getTotalSections()initData 后更新
},
onLoad() {
@@ -174,6 +174,7 @@ Page({
minWithdrawAmount: minWithdrawAmount,
bindingDays: realData?.bindingDays ?? 30,
userDiscount: realData?.userDiscount ?? 5,
posterCaseCount: app.getTotalSections(),
// 统计
referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
@@ -545,9 +546,10 @@ Page({
// 分享到朋友圈 - 随机文案
shareToMoments() {
const total = app.getTotalSections()
// 10条随机文案基于书的内容
const shareTexts = [
`🔥 在派对房里听到的真实故事比虚构的小说精彩100倍\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`🔥 在派对房里听到的真实故事比虚构的小说精彩100倍\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n${total}个真实案例,搜"卡若创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《卡若创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
@@ -654,37 +656,31 @@ Page({
method: 'POST',
data: { userId, amount }
})
wx.hideLoading()
if (res.success) {
wx.showModal({
title: '提现申请已提交 ✅',
content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
showCancel: false,
confirmText: '知道了'
})
// 刷新数据(此时待审核金额会增加,可提现金额会减少)
this.initData()
} else {
if (res.needBind || res.needBindWechat) {
wx.showModal({
title: res.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: res.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
} else {
wx.showToast({ title: res.message || res.error || '提现失败', icon: 'none', duration: 3000 })
}
}
wx.showModal({
title: '提现申请已提交 ✅',
content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
showCancel: false,
confirmText: '知道了'
})
this.initData()
} catch (e) {
wx.hideLoading()
console.error('[Referral] 提现失败:', e)
wx.showToast({ title: '提现失败,请重试', icon: 'none' })
const r = e.response || {}
if (r.needBind || r.needBindWechat) {
wx.showModal({
title: r.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: r.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
return
}
wx.showToast({ title: e.message || '提现失败,请重试', icon: 'none', duration: 3000 })
}
},
@@ -860,8 +856,9 @@ Page({
onShareTimeline() {
const ref = this.data.referralCode || app.getMyReferralCode()
console.log('[Referral] 分享到朋友圈,推荐码:', ref)
const total = app.getTotalSections()
return {
title: `卡若创业派对 - 62个真实商业案例`,
title: `卡若创业派对 - ${total}个真实商业案例`,
query: ref ? `ref=${ref}` : ''
// 不设置 imageUrl使用小程序默认截图
}

View File

@@ -1,4 +1,5 @@
const accessManager = require('../../utils/chapterAccessManager')
const soulBridge = require('../../utils/soulBridge.js')
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
@@ -90,7 +91,7 @@ Page({
const amount = this.data.price
try {
// 0. 尝试余额支付(若余额足够)
const referralCode = wx.getStorageSync('referral_code') || ''
const referralCode = soulBridge.getReferralCodeForPay(app)
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
const balance = balanceRes?.data?.balance || 0
@@ -117,7 +118,7 @@ Page({
console.warn('[VIP] 余额支付失败,改用微信支付:', e)
}
// 1. 微信支付
// 1. 微信支付(带推荐码,与章节/匹配支付一致,便于分销归因)
const payRes = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
@@ -126,18 +127,21 @@ Page({
productType: 'vip',
productId: 'vip_annual',
amount,
description: '卡若创业派对VIP年度会员365天'
description: '卡若创业派对VIP年度会员365天',
referralCode: soulBridge.getReferralCodeForPay(app) || undefined
}
})
if (payRes?.success && payRes.data?.payParams) {
wx.requestPayment({
...payRes.data.payParams,
success: async () => {
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
await this._onVipPaymentSuccess()
},
fail: () => wx.showToast({ title: '支付取消', icon: 'none' })
})
try {
await soulBridge.requestWxJsapiPayment(payRes.data.payParams)
await soulBridge.syncOrderStatusQuery(app, payRes.data.orderSn)
wx.showToast({ title: 'VIP开通成功', icon: 'success' })
await this._onVipPaymentSuccess()
} catch (e) {
const msg = (e && e.errMsg) ? String(e.errMsg) : ''
if (msg.indexOf('cancel') !== -1) wx.showToast({ title: '支付取消', icon: 'none' })
else wx.showToast({ title: '支付失败', icon: 'none' })
}
} else {
wx.showToast({ title: payRes?.error || '支付参数获取失败', icon: 'none' })
}

View File

@@ -1,4 +1,5 @@
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
const { trackClick } = require('../../utils/trackClick')
Page({
@@ -114,23 +115,22 @@ Page({
})
const params = (payRes && payRes.data && payRes.data.payParams) ? payRes.data.payParams : (payRes && payRes.payParams ? payRes.payParams : null)
if (params) {
wx.requestPayment({
...params,
success: async () => {
// Confirm the recharge
await app.request({
url: '/api/miniprogram/balance/recharge/confirm',
method: 'POST',
data: { orderSn: res.data.orderSn }
})
wx.showToast({ title: '充值成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
},
fail: () => {
wx.showToast({ title: '支付取消', icon: 'none' })
}
})
try {
await soulBridge.requestWxJsapiPayment(params)
await soulBridge.syncOrderStatusQuery(app, payRes.data && payRes.data.orderSn)
await app.request({
url: '/api/miniprogram/balance/recharge/confirm',
method: 'POST',
data: { orderSn: res.data.orderSn }
})
wx.showToast({ title: '充值成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
} catch (e) {
const msg = (e && e.errMsg) ? String(e.errMsg) : ''
if (msg.indexOf('cancel') !== -1) wx.showToast({ title: '支付取消', icon: 'none' })
else wx.showToast({ title: '支付失败', icon: 'none' })
}
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
}

View File

@@ -25,14 +25,16 @@ class ChapterAccessManager {
const res = await app.getConfig()
if (res && res.success && res.prices) {
return {
prices: res.prices || { section: 1, fullbook: 9.9 }
prices: res.prices || { section: 1, fullbook: 9.9 },
shareRate: res.shareRate != null ? res.shareRate : 90
}
}
} catch (e) {
console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
}
return {
prices: { section: 1, fullbook: 9.9 }
prices: { section: 1, fullbook: 9.9 },
shareRate: 90
}
}

View File

@@ -4,8 +4,8 @@
*
* segment 类型:
* { type: 'text', text }
* { type: 'mention', userId, nickname } — @某人,点击加好友
* { type: 'linkTag', label, url } — #链接标签,点击跳转
* { type: 'mention', userId, nickname } — @某人,点击加好友(提交存客宝见 utils/soulBridge.submitCkbLead
* { type: 'linkTag', label, url, ... } — #链接标签,点击跳转(阅读页 onLinkTagTap外链→link-preview、小程序→navigateToMiniProgram
* { type: 'image', src, alt } — 图片
*/

View File

@@ -0,0 +1,155 @@
/**
* 分销 / 微信支付 / 代付链路 / 存客宝留资 — 小程序侧统一桥接
* 阅读页 @mention、会员详情点头像、章节与代付支付等共用。
*/
/**
* 支付订单携带的推荐码:优先落地页写入的 storage否则当前用户自己的码便于自购归因一致
*/
function getReferralCodeForPay(app) {
try {
const s = wx.getStorageSync('referral_code')
if (s != null && String(s).trim() !== '') return String(s).trim()
} catch (e) {}
if (app && typeof app.getMyReferralCode === 'function') {
const c = app.getMyReferralCode()
if (c) return String(c).trim()
}
return ''
}
/** 章节 / 全书支付描述(与 read 页原逻辑一致) */
function buildSectionPayDescription(productType, sectionId, sectionTitle) {
if (productType === 'fullbook') return '《一场Soul的创业实验》全书'
if (productType === 'section') {
const t = sectionTitle || sectionId || ''
const short = t.length > 20 ? t.slice(0, 20) + '...' : t
return `章节${sectionId}-${short}`
}
return ''
}
/**
* 调起微信 JSAPI 支付(字段与 soul-api GetJSAPIPayParams 一致,勿 spread 全对象以免带入多余字段)
*/
function requestWxJsapiPayment(payParams) {
return new Promise((resolve, reject) => {
if (!payParams || payParams.timeStamp == null) {
reject(new Error('支付参数异常'))
return
}
wx.requestPayment({
timeStamp: String(payParams.timeStamp),
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: resolve,
fail: reject
})
})
}
/** 支付成功后主动查单,缓解回调延迟导致订单长期 created */
function syncOrderStatusQuery(app, orderSn) {
if (!app || !orderSn) return Promise.resolve()
return app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { silent: true }).catch(() => null)
}
/**
* 提交存客宝 lead与阅读页 @、会员详情点头像同接口)
* @param {object} app getApp()
* @param {{ targetUserId: string, targetNickname?: string, source: string, phoneModalContent?: string }} opts
* @returns {Promise<boolean>} 是否提交成功
*/
async function submitCkbLead(app, opts) {
const targetUserId = (opts && opts.targetUserId) || ''
const targetNickname = ((opts && opts.targetNickname) || 'TA').trim() || 'TA'
const source = (opts && opts.source) || 'article_mention'
const phoneModalContent = (opts && opts.phoneModalContent) || '请先填写手机号(必填),以便对方联系您'
if (!targetUserId) return false
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
return await new Promise((resolve) => {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
resolve(false)
}
})
})
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes && profileRes.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
return await new Promise((resolve) => {
wx.showModal({
title: '完善资料',
content: phoneModalContent,
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
resolve(false)
}
})
})
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source
}
})
wx.hideLoading()
if (res && res.success) {
try {
wx.setStorageSync('lead_last_submit_ts', Date.now())
} catch (e) {}
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
return true
}
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
return false
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
return false
}
}
module.exports = {
getReferralCodeForPay,
buildSectionPayDescription,
requestWxJsapiPayment,
syncOrderStatusQuery,
submitCkbLead
}