feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本

- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整
- soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新
- soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL
- .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle

Made-with: Cursor
This commit is contained in:
卡若
2026-03-23 18:38:23 +08:00
parent cb6e2bff56
commit fa3da12b16
82 changed files with 5621 additions and 2723 deletions

View File

@@ -1,13 +1,92 @@
/**
* Soul创业实验 - 订单页
* Soul创业实验 - 订单页(已支付消费流水:章节/VIP/余额/代付等)
*/
const app = getApp()
const { cleanSingleLineField } = require('../../utils/contentParser.js')
const PAID_STATUSES = new Set(['paid', 'completed', 'success'])
function parseOrderTimeMs(o) {
const raw = o.created_at || o.createdAt || o.pay_time || 0
const t = new Date(raw).getTime()
return Number.isFinite(t) ? t : 0
}
function formatShortDate(ms) {
if (!ms) return '--'
const d = new Date(ms)
const m = (d.getMonth() + 1).toString().padStart(2, '0')
const day = d.getDate().toString().padStart(2, '0')
return `${m}-${day}`
}
function midForSection(sectionId, bookFlat) {
const row = bookFlat.find((s) => s.id === sectionId)
return row?.mid ?? row?.MID ?? 0
}
function classifyNav(productType, productId, mid) {
const pt = String(productType || '').toLowerCase()
if (pt === 'section' && productId) {
return { kind: 'read', id: productId, mid: mid || 0, label: '阅读' }
}
if (pt === 'fullbook') {
return { kind: 'switchTab', path: '/pages/chapters/chapters', label: '去目录' }
}
if (pt === 'vip') {
return { kind: 'page', path: '/pages/vip/vip', label: '会员中心' }
}
if (pt === 'match') {
return { kind: 'switchTab', path: '/pages/match/match', label: '找伙伴' }
}
if (pt === 'balance_recharge') {
return { kind: 'page', path: '/pages/wallet/wallet', label: '余额' }
}
if (pt === 'gift_pay' || pt === 'gift_pay_batch') {
return { kind: 'page', path: '/pages/gift-pay/list', label: '代付记录' }
}
if (productId && (/^\d+\.\d+/.test(productId) || productId.length > 0)) {
return { kind: 'read', id: productId, mid: mid || 0, label: '阅读' }
}
return { kind: 'none', label: '--' }
}
function mapApiOrderToRow(item, bookFlat) {
const status = String(item.status || '').toLowerCase()
if (!PAID_STATUSES.has(status)) return null
const pt = String(item.product_type || '').toLowerCase()
const productId = String(item.product_id || item.section_id || '').trim()
let mid = Number(item.section_mid ?? item.mid ?? item.MID ?? 0) || 0
if (pt === 'section' && productId && !mid) mid = midForSection(productId, bookFlat)
const titleRaw = cleanSingleLineField(item.product_name || '')
const title =
titleRaw ||
(pt === 'balance_recharge' ? '余额充值' : productId ? `订单 ${productId}` : '消费记录')
const amt = Number(item.amount)
const amountStr = Number.isFinite(amt) ? amt.toFixed(2) : '--'
const t = parseOrderTimeMs(item)
const nav = classifyNav(pt, productId, mid)
return {
rowKey: String(item.order_sn || item.id || `o_${t}`),
title,
subLine: `¥${amountStr} · ${formatShortDate(t)}`,
actionLabel: nav.label,
nav,
_sortMs: t
}
}
Page({
data: {
statusBarHeight: 44,
orders: [],
loading: true
loading: true,
allRows: [],
displayRows: [],
historyExpanded: false
},
onLoad() {
@@ -16,63 +95,95 @@ Page({
this.loadOrders()
},
onShow() {
if (!this._purchasesFirstOnShowSkipped) {
this._purchasesFirstOnShowSkipped = true
return
}
if (app.globalData.isLoggedIn) this.loadOrders()
},
applyDisplay(expanded) {
const all = this.data.allRows || []
const display = expanded || all.length <= 5 ? all : all.slice(0, 5)
this.setData({ displayRows: display, historyExpanded: !!expanded })
},
expandHistory() {
if (this.data.historyExpanded) return
this.applyDisplay(true)
},
async loadOrders() {
this.setData({ loading: true })
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
const userId = app.globalData.userInfo?.id
try {
const userId = app.globalData.userInfo?.id
if (userId) {
const res = await app.request(`/api/miniprogram/orders?userId=${userId}`)
if (res && res.success && res.data) {
const raw = (res.data || []).map(item => ({
id: item.id || item.order_sn,
sectionId: item.product_id || item.section_id,
sectionMid: item.section_mid ?? item.mid ?? 0,
title: item.product_name || `章节 ${item.product_id || ''}`,
amount: item.amount || 0,
status: item.status || 'completed',
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--',
_sortMs: new Date(item.created_at || item.pay_time || 0).getTime() || 0
}))
raw.sort((a, b) => b._sortMs - a._sortMs)
const orders = raw.map(({ _sortMs, ...rest }) => rest)
this.setData({ orders })
const res = await app.request({
url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`,
silent: true
})
if (res && res.success && Array.isArray(res.data)) {
const rows = res.data
.map((item) => mapApiOrderToRow(item, bookFlat))
.filter(Boolean)
.sort((a, b) => b._sortMs - a._sortMs)
.map(({ _sortMs, ...rest }) => rest)
this.setData({ allRows: rows, loading: false })
this.applyDisplay(false)
return
}
}
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
const orders = purchasedSections.map((id, index) => ({
id: `order_${index}`,
sectionId: id,
sectionMid: 0,
title: `章节 ${id}`,
amount: 1,
status: 'completed',
createTime: new Date(Date.now() - index * 86400000).toLocaleDateString()
}))
this.setData({ orders })
const ids = [...(app.globalData.purchasedSections || [])].reverse()
const rows = ids.map((id, index) => {
const mid = midForSection(id, bookFlat)
const row = bookFlat.find((s) => s.id === id)
const title =
cleanSingleLineField(
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
) || `章节 ${id}`
const t = Date.now() - index * 86400000
return {
rowKey: `p_${id}_${index}`,
title,
subLine: `已解锁 · ${formatShortDate(t)}`,
actionLabel: '阅读',
nav: { kind: 'read', id, mid, label: '阅读' }
}
})
this.setData({ allRows: rows, loading: false })
this.applyDisplay(false)
} catch (e) {
console.error('加载订单失败:', e)
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
this.setData({
orders: purchasedSections.map((id, i) => ({
id: `order_${i}`, sectionId: id, sectionMid: 0, title: `章节 ${id}`, amount: 1, status: 'completed',
createTime: new Date(Date.now() - i * 86400000).toLocaleDateString()
}))
})
} finally {
this.setData({ loading: false })
this.setData({ allRows: [], loading: false })
this.applyDisplay(false)
}
},
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid
if (!id) return
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
onOrderRowTap(e) {
const index = e.currentTarget.dataset.index
const row = (this.data.displayRows || [])[index]
if (!row || !row.nav) return
const { nav } = row
if (nav.kind === 'read' && nav.id) {
const q = nav.mid ? `mid=${nav.mid}` : `id=${nav.id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
return
}
if (nav.kind === 'page' && nav.path) {
wx.navigateTo({ url: nav.path })
return
}
if (nav.kind === 'switchTab' && nav.path) {
wx.switchTab({ url: nav.path })
}
},
goBack() { getApp().goBackOrToHome() },
goBack() {
getApp().goBackOrToHome()
},
onShareAppMessage() {
const ref = app.getMyReferralCode()