删除不再使用的文件,包括开放 API 鉴权规范文档、数据库迁移脚本和旧版图标组件,优化项目结构和资源管理。更新小程序代码以支持代付功能,增加代付分享弹窗和支付逻辑,提升用户体验。

This commit is contained in:
Alex-larget
2026-03-18 20:33:50 +08:00
parent 0f3933fabd
commit d6cdd6fdba
57 changed files with 1672 additions and 2761 deletions

View File

@@ -13,8 +13,8 @@ const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
App({
globalData: {
// API 基础地址:开发时修改下面一行切换环境
baseUrl: "https://soulapi.quwanzhi.com",
// baseUrl: 'http://localhost:8080', // 开发
// baseUrl: "https://soulapi.quwanzhi.com",
baseUrl: 'http://localhost:8080', // 开发
// baseUrl: 'https://souldev.quwanzhi.com', // 测试
// 小程序配置 - 真实AppID
appId: DEFAULT_APP_ID,

View File

@@ -124,6 +124,10 @@
<image class="btn-icon" src="/assets/icons/share.svg" mode="aspectFit"/>
<text>发送给好友</text>
</button>
<!-- 已退款:不可再分享/领取 -->
<view wx:elif="{{detail.status === 'refunded' || detail.action === 'refunded'}}" class="footer-btn footer-btn-disabled">
<text>已退款</text>
</view>
<!-- 好友 action=redeem领取并阅读 -->
<button wx:elif="{{!isInitiator && detail.action === 'redeem'}}" class="footer-btn redeem-btn" bindtap="doRedeem" disabled="{{redeeming}}">
<image class="btn-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"/>

View File

@@ -31,7 +31,7 @@
<view class="card-row card-meta">
<text class="quantity" wx:if="{{item.quantity > 1}}">{{item.quantity}}份</text>
<text class="redeemed" wx:if="{{item.status === 'paid'}}">已领 {{item.redeemedCount || 0}}/{{item.quantity || 1}}</text>
<text class="status {{item.status}}">{{item.status === 'pending' || item.status === 'pending_pay' ? '待支付' : item.status === 'paid' ? '已支付' : item.status === 'cancelled' ? '已取消' : '已过期'}}</text>
<text class="status {{item.status}}">{{item.status === 'pending' || item.status === 'pending_pay' ? '待支付' : item.status === 'paid' ? '已支付' : item.status === 'refunded' ? '已退款' : item.status === 'cancelled' ? '已取消' : '已过期'}}</text>
<view class="actions" wx:if="{{item.status === 'pending' || item.status === 'pending_pay'}}">
<text class="action-text cancel" catchtap="cancelRequest" data-sn="{{item.requestSn}}">取消</text>
</view>

View File

@@ -37,7 +37,7 @@
<text>去阅读</text>
</view>
<view class="btn-link" bindtap="goToDetail">
<text>{{detail.status === 'paid' ? '去分享' : detail.status === 'pending_pay' ? '去支付' : '查看详情'}}</text>
<text>{{detail.status === 'paid' ? '去分享' : detail.status === 'pending_pay' ? '去支付' : detail.status === 'refunded' ? '已退款' : '查看详情'}}</text>
</view>
</view>
</section>

View File

@@ -35,10 +35,6 @@
<view class="profile-name-row">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<view class="profile-name-actions">
<view class="profile-edit-btn" bindtap="goToProfileShow">
<image class="profile-edit-icon" src="/assets/icons/edit-gray.svg" mode="aspectFit"/>
<text class="profile-edit-text">编辑</text>
</view>
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" wx:if="{{!auditMode}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
</view>
</view>

View File

@@ -67,7 +67,12 @@ Page({
// 弹窗
showShareModal: false,
showGiftModal: false,
giftQuantity: 1,
giftQuantity: 6,
giftUnitPrice: 0,
giftTotalPrice: '0.00',
giftPaying: false,
giftPaid: false,
giftRequestSn: '',
showLoginModal: false,
agreeProtocol: false,
showPosterModal: false,
@@ -82,6 +87,9 @@ Page({
// 审核模式:隐藏购买按钮
auditMode: false,
// 好友从代付分享进入:待自动领取的 requestSn
pendingGiftRequestSn: '',
},
onShow() {
@@ -111,13 +119,11 @@ Page({
// 支持 scene扫码、mid、id、ref、gift代付
const sceneStr = (options && options.scene) || ''
const parsed = parseScene(sceneStr)
const ref = options.ref || parsed.ref
const isGift = options.gift === '1' || options.gift === 'true'
// 代付统一到代付页:gift=1&ref=requestSn 时直接跳转,禁止在阅读页代付
if (isGift && ref) {
wx.redirectTo({ url: `/pages/gift-pay/detail?requestSn=${encodeURIComponent(ref)}` })
return
}
// 代付:分享链路使用 requestSn优先 options.requestSn兼容旧链路 gift=1&ref=requestSn
const giftRequestSn = (options.requestSn || (isGift ? (options.ref || parsed.ref) : '') || '').trim()
// 推荐码:仅在非代付链路使用 ref
const ref = (!isGift ? (options.ref || parsed.ref) : '') || ''
const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0)
let id = options.id || parsed.id || app.globalData.initialSectionId
if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid
@@ -149,7 +155,8 @@ Page({
sectionId: id,
sectionMid: mid || null,
loading: true,
accessState: 'unknown'
accessState: 'unknown',
pendingGiftRequestSn: giftRequestSn || ''
})
if (ref) {
@@ -167,8 +174,8 @@ Page({
// 统一:先拉章节数据,用 isFree/price===0 判断免费
const chapterRes = await app.request({ url: this._getChapterUrl({ id, mid }), silent: true })
const accessState = await accessManager.determineAccessState(id, chapterRes)
const canAccess = accessManager.canAccessFullContent(accessState)
let accessState = await accessManager.determineAccessState(id, chapterRes)
let canAccess = accessManager.canAccessFullContent(accessState)
this.setData({
accessState,
@@ -179,6 +186,23 @@ Page({
// 加载内容(复用已拉取的章节数据,避免二次请求)
await this.loadContent(id, accessState, chapterRes)
// 代付自动领取:好友打开阅读页时自动领取并解锁
if (this.data.pendingGiftRequestSn) {
const redeemed = await this._tryAutoRedeemGift(this.data.pendingGiftRequestSn)
if (redeemed) {
// 领取成功后刷新章节与权限(保守:重新拉章节数据 + 重新判断权限)
await accessManager.refreshUserPurchaseStatus()
const freshChapterRes = await app.request({ url: this._getChapterUrl({ id, mid }), silent: true })
accessState = await accessManager.determineAccessState(id, freshChapterRes)
canAccess = accessManager.canAccessFullContent(accessState)
this.setData({ accessState, canAccess, showPaywall: !canAccess, pendingGiftRequestSn: '' })
if (canAccess) {
await this.loadContent(id, accessState, freshChapterRes)
readingTracker.init(id)
}
}
}
// 【标准流程】4. 如果有权限,初始化阅读追踪
if (canAccess) {
@@ -196,6 +220,58 @@ Page({
this.setData({ loading: false })
}
},
_getGiftUnitPrice() {
const p = this.data.section?.price
const cfg = this.data.sectionPrice
const v = (p != null && p !== '') ? Number(p) : Number(cfg || 0)
return isNaN(v) ? 0 : v
},
_updateGiftTotalPrice() {
const unit = this.data.giftUnitPrice || this._getGiftUnitPrice()
const q = parseInt(this.data.giftQuantity, 10) || 0
const total = unit * q
this.setData({
giftUnitPrice: unit,
giftTotalPrice: (isNaN(total) ? 0 : total).toFixed(2)
})
},
async _tryAutoRedeemGift(requestSn) {
// 单页模式(朋友圈)不做自动领取,避免隐式登录/支付能力限制
try {
const sys = wx.getSystemInfoSync()
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
if (isSinglePage) return false
} catch (e) {}
const userId = app.globalData.userInfo?.id
if (!userId) {
// 记住 requestSn登录后自动领取
this.setData({ pendingGiftRequestSn: requestSn })
wx.showToast({ title: '登录后将自动领取并解锁', icon: 'none', duration: 2500 })
this.showLoginModal()
return false
}
try {
const res = await app.request({
url: '/api/miniprogram/gift-pay/redeem',
method: 'POST',
data: { requestSn, userId }
})
if (res && res.success) return true
// 已领取/已无名额等都视为无需再重试
if (res && (res.error || res.message)) {
wx.showToast({ title: res.error || res.message || '领取失败', icon: 'none' })
}
this.setData({ pendingGiftRequestSn: '' })
return false
} catch (e) {
console.warn('[Read][Gift] 自动领取失败:', e)
return false
}
},
// 从后端加载免费章节配置
onPageScroll(e) {
@@ -652,7 +728,123 @@ Page({
wx.showToast({ title: '章节信息异常', icon: 'none' })
return
}
wx.navigateTo({ url: `/pages/gift-pay/detail?sectionId=${encodeURIComponent(sectionId)}` })
this.setData({
showGiftModal: true,
giftPaid: false,
giftRequestSn: '',
giftPaying: false,
giftQuantity: 6
})
this._updateGiftTotalPrice()
},
closeGiftModal() {
this.setData({ showGiftModal: false })
},
selectGiftQuantity(e) {
const q = parseInt(e.currentTarget.dataset.q, 10)
if (!q || q < 1) return
this.setData({ giftQuantity: q })
this._updateGiftTotalPrice()
},
async confirmGiftPay() {
if (this.data.giftPaying) return
// 朋友圈单页模式禁止支付
try {
const sys = wx.getSystemInfoSync()
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
if (isSinglePage) {
wx.showModal({
title: '朋友圈单页',
content: '当前为朋友圈单页,无法发起代付支付。请点击底部「前往小程序」进入完整版后再操作。',
showCancel: false
})
return
}
} catch (e) {}
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const sectionId = this.data.sectionId
const quantity = parseInt(this.data.giftQuantity, 10)
if (!sectionId || !quantity) {
wx.showToast({ title: '参数异常', icon: 'none' })
return
}
let openId = app.globalData.openId || wx.getStorageSync('openId')
if (!openId) {
wx.showLoading({ title: '获取支付凭证...', mask: true })
openId = await app.getOpenId()
wx.hideLoading()
}
if (!openId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
this.setData({ giftPaying: true })
wx.showLoading({ title: '创建订单中...', mask: true })
try {
// 1) 创建代付请求
const createRes = await app.request({
url: '/api/miniprogram/gift-pay/create',
method: 'POST',
data: { userId, productType: 'section', productId: sectionId, quantity }
})
if (!createRes?.success || !createRes.requestSn) {
throw new Error(createRes?.error || '创建失败')
}
const requestSn = createRes.requestSn
// 2) 发起人支付(微信支付)
const payRes = await app.request({
url: '/api/miniprogram/gift-pay/initiator-pay',
method: 'POST',
data: { requestSn, openId, userId }
})
wx.hideLoading()
if (!payRes || !payRes.success || !payRes.data?.payParams) {
throw new Error(payRes?.error || '创建订单失败')
}
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) {}
}
wx.showToast({ title: '支付成功', icon: 'success' })
this.setData({ giftPaid: true, giftRequestSn: requestSn, giftPaying: false })
} catch (e) {
wx.hideLoading()
const msg = e?.message || e?.error || e?.errMsg || '支付失败'
if (e?.errMsg && String(e.errMsg).includes('cancel')) {
wx.showToast({ title: '已取消支付', icon: 'none' })
} else {
wx.showToast({ title: msg, icon: 'none', duration: 2500 })
}
this.setData({ giftPaying: false })
}
},
// 复制链接
@@ -691,11 +883,22 @@ Page({
},
// 分享到微信 - 自动带分享人ID
onShareAppMessage() {
onShareAppMessage(e) {
trackClick('read', 'btn_click', '分享_' + this.data.sectionId)
const { section, sectionId, sectionMid } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
// 代付分享按钮(支付后):好友打开阅读页自动领取解锁
const isGiftShare = e?.from === 'button' && e?.target?.dataset?.gift === '1'
const requestSn = (e?.target?.dataset?.requestSn || '').trim()
if (isGiftShare && requestSn) {
let path = `/pages/read/read?${q}&gift=1&requestSn=${encodeURIComponent(requestSn)}`
if (ref) path += `&ref=${encodeURIComponent(ref)}`
const t = section?.title || 'Soul创业派对'
const title = `我已为你买单:${t.length > 18 ? t.slice(0, 18) + '...' : t}`
return { title, path }
}
const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
const title = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
@@ -840,6 +1043,25 @@ Page({
wx.showLoading({ title: '更新状态中...', mask: true })
try {
// 0. 若有代付待领取,先领取再刷新购买状态
if (this.data.pendingGiftRequestSn) {
try {
const userId = app.globalData.userInfo?.id
const requestSn = this.data.pendingGiftRequestSn
if (userId && requestSn) {
const res = await app.request({
url: '/api/miniprogram/gift-pay/redeem',
method: 'POST',
data: { requestSn, userId }
})
if (res && res.success) {
this.setData({ pendingGiftRequestSn: '' })
}
}
} catch (e) {
console.warn('[Read][Gift] 登录后自动领取失败:', e)
}
}
// 1. 刷新用户购买状态(从 orders 表拉取最新)
await accessManager.refreshUserPurchaseStatus()

View File

@@ -288,6 +288,51 @@
</view>
</view>
<!-- 代付分享弹窗:阅读页内发起代付并支付 -->
<view class="modal-overlay modal-overlay-center" wx:if="{{showGiftModal}}" bindtap="closeGiftModal">
<view class="modal-content modal-content-center gift-modal-v2" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">{{giftPaid ? '代付已完成' : '生成代付链接'}}</text>
<view class="modal-close" bindtap="closeGiftModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
</view>
<view class="gift-article-card">
<text class="gift-article-title">{{section.title || '代付商品'}}</text>
<text class="gift-article-desc" wx:if="{{section.desc}}">{{section.desc}}</text>
</view>
<view wx:if="{{!giftPaid}}">
<text class="gift-label">选择代付名额数</text>
<view class="gift-spots-grid">
<view class="gift-spot-btn {{giftQuantity===6?'gift-spot-active':''}}" bindtap="selectGiftQuantity" data-q="6">6</view>
<view class="gift-spot-btn {{giftQuantity===30?'gift-spot-active':''}}" bindtap="selectGiftQuantity" data-q="30">30</view>
<view class="gift-spot-btn {{giftQuantity===100?'gift-spot-active':''}}" bindtap="selectGiftQuantity" data-q="100">100</view>
<view class="gift-spot-btn {{giftQuantity===1000?'gift-spot-active':''}}" bindtap="selectGiftQuantity" data-q="1000">1000</view>
</view>
<view class="gift-price-box">
<text class="gift-price-label">待支付总价格</text>
<view class="gift-price-row">
<text class="gift-price-formula">¥{{giftUnitPrice}} × {{giftQuantity}} =</text>
<text class="gift-price-total">¥{{giftTotalPrice}}</text>
</view>
</view>
<button class="gift-pay-btn" bindtap="confirmGiftPay" disabled="{{giftPaying}}">
{{giftPaying ? '支付中...' : '确认并支付'}}
</button>
<view class="gift-cancel-text" bindtap="closeGiftModal">取消</view>
</view>
<view wx:else class="gift-paid-wrap">
<text class="gift-paid-tip">支付成功,点击下方按钮直接分享给好友。好友打开阅读页将自动领取并解锁。</text>
<button class="gift-share-btn" open-type="share" data-gift="1" data-request-sn="{{giftRequestSn}}">
发送给好友
</button>
</view>
</view>
</view>
<!-- 登录弹窗 - 须勾选同意协议,《用户协议》《隐私政策》可点击查看 -->
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal" catchtap="stopPropagation">

View File

@@ -600,6 +600,10 @@
z-index: 1000;
}
.modal-overlay-center {
align-items: center;
}
.modal-content {
width: 100%;
max-width: 750rpx;
@@ -610,11 +614,25 @@
animation: slideUp 0.3s ease;
}
.modal-content-center {
width: 640rpx;
max-width: calc(100vw - 80rpx);
border-radius: 32rpx;
padding: 40rpx;
padding-bottom: 40rpx;
animation: popIn 0.18s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes popIn {
from { transform: scale(0.94); opacity: 0.6; }
to { transform: scale(1); opacity: 1; }
}
.modal-header {
display: flex;
align-items: center;
@@ -706,6 +724,117 @@
font-weight: 600;
}
/* 阅读页内代付弹窗v2档位按钮 + 价格计算 + 支付后分享) */
.gift-modal-v2 {
padding: 40rpx;
}
.gift-article-card {
padding: 28rpx;
background: rgba(255, 255, 255, 0.06);
border: 1rpx solid rgba(255, 255, 255, 0.08);
border-radius: 24rpx;
margin-bottom: 32rpx;
}
.gift-article-title {
font-size: 32rpx;
font-weight: 700;
color: #fff;
display: block;
line-height: 1.3;
margin-bottom: 10rpx;
}
.gift-article-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.55);
display: block;
line-height: 1.5;
}
.gift-spots-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18rpx;
margin-bottom: 28rpx;
}
.gift-spot-btn {
text-align: center;
padding: 22rpx 0;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.06);
border: 1rpx solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.85);
font-weight: 600;
font-size: 28rpx;
}
.gift-spot-active {
border-color: rgba(0, 206, 209, 0.9);
color: #00CED1;
background: rgba(0, 206, 209, 0.12);
}
.gift-price-box {
padding: 26rpx;
border-radius: 24rpx;
background: rgba(0, 0, 0, 0.25);
border: 1rpx solid rgba(255, 255, 255, 0.08);
margin-bottom: 28rpx;
text-align: center;
}
.gift-price-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
display: block;
margin-bottom: 10rpx;
}
.gift-price-row {
display: flex;
align-items: baseline;
justify-content: center;
gap: 14rpx;
}
.gift-price-formula {
font-size: 24rpx;
color: rgba(0, 206, 209, 0.8);
}
.gift-price-total {
font-size: 44rpx;
font-weight: 800;
color: #00CED1;
}
.gift-pay-btn {
width: 100%;
border-radius: 999rpx;
background: #00CED1;
color: #000;
font-weight: 800;
font-size: 32rpx;
padding: 26rpx 0;
}
.gift-cancel-text {
text-align: center;
margin-top: 18rpx;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.45);
}
.gift-paid-wrap {
padding-top: 8rpx;
}
.gift-paid-tip {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
margin-bottom: 24rpx;
}
.gift-share-btn {
width: 100%;
border-radius: 999rpx;
background: rgba(0, 206, 209, 0.12);
border: 1rpx solid rgba(0, 206, 209, 0.45);
color: #00CED1;
font-weight: 800;
font-size: 32rpx;
padding: 26rpx 0;
}
.share-modal-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);

View File

@@ -85,6 +85,20 @@ Page({
return
}
}
// 支付前:若头像/昵称仍为默认值,引导先完善(仅头像+昵称)
if (this._shouldGuideAvatarNickname()) {
wx.showModal({
title: '完善资料',
content: '开通超级个体前,请先设置头像和昵称,让他人更好地认识你',
confirmText: '去完善',
cancelText: '稍后',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
}
})
return
}
this.setData({ purchasing: true })
const amount = this.data.price
try {
@@ -158,12 +172,26 @@ Page({
if (typeof p.initUserStatus === 'function') p.initUserStatus()
else if (typeof p.updateUserStatus === 'function') p.updateUserStatus()
})
// 开通成功后兜底:仍为默认头像/昵称则引导完善
if (this._shouldGuideAvatarNickname()) {
wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
}
} catch (e) {
console.error('[VIP] 支付后同步失败:', e)
}
wx.hideLoading()
},
_shouldGuideAvatarNickname() {
const user = app.globalData.userInfo || {}
const avatar = (user.avatar || user.avatarUrl || '').trim()
const nickname = (user.nickname || user.nickName || '').trim()
// 与 ruleEngine.checkRule_FillAvatar 保持同口径(允许前端兜底)
if (avatar && !avatar.includes('default') && nickname && nickname !== '微信用户' && !nickname.startsWith('微信用户')) return false
return true
},
goBack() { getApp().goBackOrToHome() },
onShareAppMessage() {