diff --git a/miniprogram/app.js b/miniprogram/app.js
index 4d7ad146..6d3cdf3e 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -82,10 +82,24 @@ App({
this.handleReferralCode(options)
},
- // 处理推荐码绑定
+ // 处理推荐码绑定(支持 query.ref 与扫码 scene;scene 可能为 ref=SOULXXX 或 id=1.1_ref=xxx,后端会把 & 转成 _)
handleReferralCode(options) {
const query = options?.query || {}
- const refCode = query.ref || query.referralCode
+ let refCode = query.ref || query.referralCode
+ if (options?.scene) {
+ const scene = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim()
+ // 支持 _ 或 & 作为参数分隔符(后端生成小程序码时会把 & 转为 _)
+ const parts = scene.split(/[&_]/)
+ for (const part of parts) {
+ const eq = part.indexOf('=')
+ if (eq > 0) {
+ const key = part.slice(0, eq)
+ const val = part.slice(eq + 1)
+ if (key === 'ref') refCode = val
+ if (key === 'id' && val) this.globalData.initialSectionId = val
+ }
+ }
+ }
if (refCode) {
console.log('[App] 检测到推荐码:', refCode)
@@ -161,6 +175,15 @@ App({
}
},
+ // 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串)
+ getMyReferralCode() {
+ const user = this.globalData.userInfo
+ if (!user) return ''
+ if (user.referralCode) return user.referralCode
+ if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6)
+ return ''
+ },
+
// 获取系统信息
getSystemInfo() {
try {
diff --git a/miniprogram/app.json b/miniprogram/app.json
index b3df2c75..a8bb2bec 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -13,7 +13,7 @@
"pages/settings/settings",
"pages/search/search",
"pages/addresses/addresses",
- "pages/addresses/edit","pages/withdraw-records/withdraw-records"
+ "pages/addresses/edit","pages/withdraw-records/withdraw-records","pages/scan/scan"
],
"window": {
"backgroundTextStyle": "light",
diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js
index 8f19cc60..c4f6b68e 100644
--- a/miniprogram/pages/about/about.js
+++ b/miniprogram/pages/about/about.js
@@ -77,5 +77,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 关于作者',
+ path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
+ }
}
})
diff --git a/miniprogram/pages/addresses/addresses.js b/miniprogram/pages/addresses/addresses.js
index 685528cf..cd13fe90 100644
--- a/miniprogram/pages/addresses/addresses.js
+++ b/miniprogram/pages/addresses/addresses.js
@@ -119,5 +119,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 收货地址',
+ path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
+ }
}
})
diff --git a/miniprogram/pages/addresses/edit.js b/miniprogram/pages/addresses/edit.js
index 9542c1dc..4f45893c 100644
--- a/miniprogram/pages/addresses/edit.js
+++ b/miniprogram/pages/addresses/edit.js
@@ -197,5 +197,13 @@ Page({
// 返回
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 编辑地址',
+ path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
+ }
}
})
diff --git a/miniprogram/pages/agreement/agreement.js b/miniprogram/pages/agreement/agreement.js
index aedd4c68..cff31e3b 100644
--- a/miniprogram/pages/agreement/agreement.js
+++ b/miniprogram/pages/agreement/agreement.js
@@ -17,5 +17,13 @@ Page({
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 用户协议',
+ path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
+ }
}
})
diff --git a/miniprogram/pages/chapters/chapters.js b/miniprogram/pages/chapters/chapters.js
index 74e323de..8584cd9d 100644
--- a/miniprogram/pages/chapters/chapters.js
+++ b/miniprogram/pages/chapters/chapters.js
@@ -257,5 +257,13 @@ Page({
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 目录',
+ path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
+ }
}
})
diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js
index bd292148..7b1c8ef8 100644
--- a/miniprogram/pages/index/index.js
+++ b/miniprogram/pages/index/index.js
@@ -56,10 +56,9 @@ Page({
navBarHeight: app.globalData.navBarHeight
})
- // 处理分享参数(推荐码绑定)
- if (options && options.ref) {
- console.log('[Index] 检测到推荐码:', options.ref)
- app.handleReferralCode({ query: options })
+ // 处理分享参数与扫码 scene(推荐码绑定)
+ if (options && (options.ref || options.scene)) {
+ app.handleReferralCode(options)
}
// 初始化数据
@@ -211,5 +210,13 @@ Page({
await this.initData()
this.updateUserStatus()
wx.stopPullDownRefresh()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 真实商业故事',
+ path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
+ }
}
})
diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js
index 104fb88c..7e87ffde 100644
--- a/miniprogram/pages/match/match.js
+++ b/miniprogram/pages/match/match.js
@@ -891,5 +891,13 @@ Page({
},
// 阻止事件冒泡
- preventBubble() {}
+ preventBubble() {},
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 找伙伴',
+ path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
+ }
+ }
})
diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js
index 6a94ddfe..123ada17 100644
--- a/miniprogram/pages/my/my.js
+++ b/miniprogram/pages/my/my.js
@@ -676,5 +676,13 @@ Page({
},
// 阻止冒泡
- stopPropagation() {}
+ stopPropagation() {},
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 我的',
+ path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
+ }
+ }
})
diff --git a/miniprogram/pages/privacy/privacy.js b/miniprogram/pages/privacy/privacy.js
index 0cd665db..0c95c06e 100644
--- a/miniprogram/pages/privacy/privacy.js
+++ b/miniprogram/pages/privacy/privacy.js
@@ -17,5 +17,13 @@ Page({
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 隐私政策',
+ path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
+ }
}
})
diff --git a/miniprogram/pages/purchases/purchases.js b/miniprogram/pages/purchases/purchases.js
index 46cbbca7..6f0f7238 100644
--- a/miniprogram/pages/purchases/purchases.js
+++ b/miniprogram/pages/purchases/purchases.js
@@ -45,5 +45,13 @@ Page({
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
- goBack() { wx.navigateBack() }
+ goBack() { wx.navigateBack() },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 我的订单',
+ path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
+ }
+ }
})
diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js
index eab1ad7d..20e59199 100644
--- a/miniprogram/pages/read/read.js
+++ b/miniprogram/pages/read/read.js
@@ -66,12 +66,18 @@ Page({
isGeneratingPoster: false,
// 免费章节
- freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
+ freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
+
+ // 分享卡片图(canvas 生成后写入,供 onShareAppMessage 使用)
+ shareImagePath: ''
},
async onLoad(options) {
- const { id, ref } = options
-
+ // 扫码进入时 id 可能在 app.globalData.initialSectionId(scene 里 id=1.1 由 app 解析)
+ let id = options.id || app.globalData.initialSectionId
+ if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
+ const ref = options.ref
+
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight,
@@ -268,10 +274,11 @@ Page({
try {
const res = await this.fetchChapterWithTimeout(id, 5000)
if (res && res.content) {
+ this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(res)
- // 成功后缓存到本地
wx.setStorageSync(cacheKey, res)
console.log('[Read] 从API加载成功:', id)
+ setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
@@ -282,10 +289,11 @@ Page({
try {
const cached = wx.getStorageSync(cacheKey)
if (cached && cached.content) {
+ this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(cached)
console.log('[Read] 从本地缓存加载成功:', id)
- // 后台静默刷新
this.silentRefresh(id)
+ setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
@@ -363,9 +371,11 @@ Page({
try {
const res = await this.fetchChapterWithTimeout(id, 8000)
if (res && res.content) {
+ this.setData({ section: this.getSectionInfo(id) })
this.setChapterContent(res)
wx.setStorageSync(`chapter_${id}`, res)
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
+ setTimeout(() => this.drawShareCard(), 600)
return
}
} catch (e) {
@@ -454,33 +464,91 @@ Page({
})
},
- // 分享到微信 - 自动带分享人ID
- onShareAppMessage() {
- const { section, sectionId } = this.data
- const userInfo = app.globalData.userInfo
- const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
-
- // 分享标题优化
- const shareTitle = section?.title
+ // 绘制分享卡片图(标题+正文摘要),生成后供 onShareAppMessage 使用
+ drawShareCard() {
+ const { section, sectionId, contentParagraphs } = this.data
+ const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
+ const raw = (contentParagraphs && contentParagraphs.length)
+ ? contentParagraphs.slice(0, 4).join(' ').replace(/\s+/g, ' ').trim()
+ : ''
+ const excerpt = raw.length > 120 ? raw.slice(0, 120) + '...' : (raw || '来自派对房的真实商业故事')
+ const ctx = wx.createCanvasContext('shareCardCanvas', this)
+ const w = 500
+ const h = 400
+ // 白底
+ ctx.setFillStyle('#ffffff')
+ ctx.fillRect(0, 0, w, h)
+ // 顶部:平台名
+ ctx.setFillStyle('#333333')
+ ctx.setFontSize(14)
+ ctx.fillText('📚 Soul 创业派对 - 真实商业故事', 24, 36)
+ // 深色内容区(模拟参考图效果)
+ const boxX = 24
+ const boxY = 52
+ const boxW = w - 48
+ const boxH = 300
+ ctx.setFillStyle('#2c2c2e')
+ ctx.fillRect(boxX, boxY, boxW, boxH)
+ // 文章标题(白字)
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(15)
+ const titleLines = this.wrapText(ctx, title.length > 50 ? title.slice(0, 50) + '...' : title, boxW - 32, 15)
+ let y = boxY + 28
+ titleLines.slice(0, 2).forEach(line => {
+ ctx.fillText(line, boxX + 16, y)
+ y += 22
+ })
+ y += 8
+ // 正文摘要(浅灰)
+ ctx.setFillStyle('rgba(255,255,255,0.88)')
+ ctx.setFontSize(12)
+ const excerptLines = this.wrapText(ctx, excerpt, boxW - 32, 12)
+ excerptLines.slice(0, 8).forEach(line => {
+ ctx.fillText(line, boxX + 16, y)
+ y += 20
+ })
+ // 底部:小程序标识
+ ctx.setFillStyle('#999999')
+ ctx.setFontSize(11)
+ ctx.fillText('小程序', 24, h - 16)
+ ctx.draw(false, () => {
+ wx.canvasToTempFilePath({
+ canvasId: 'shareCardCanvas',
+ fileType: 'png',
+ success: (res) => {
+ this.setData({ shareImagePath: res.tempFilePath })
+ }
+ }, this)
+ })
+ },
+
+ // 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用)
+ getShareConfig() {
+ const { section, sectionId, shareImagePath } = this.data
+ const ref = app.getMyReferralCode()
+ const shareTitle = section?.title
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
: '📚 Soul创业派对 - 真实商业故事'
-
+ const path = ref
+ ? `/pages/read/read?id=${sectionId}&ref=${ref}`
+ : `/pages/read/read?id=${sectionId}`
return {
title: shareTitle,
- path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
- imageUrl: '/assets/share-cover.png' // 可配置分享封面图
+ path,
+ imageUrl: shareImagePath || undefined
}
},
-
- // 分享到朋友圈
+
+ onShareAppMessage() {
+ return this.getShareConfig()
+ },
+
onShareTimeline() {
const { section, sectionId } = this.data
- const userInfo = app.globalData.userInfo
- const referralCode = userInfo?.referralCode || ''
-
+ const ref = app.getMyReferralCode()
return {
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
- query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
+ query: ref ? `id=${sectionId}&ref=${ref}` : `id=${sectionId}`
}
},
@@ -734,7 +802,8 @@ Page({
if (res.success && res.data?.payParams) {
paymentData = res.data.payParams
- console.log('[Pay] 获取支付参数成功:', paymentData)
+ paymentData._orderSn = res.data.orderSn
+ console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn)
} else {
throw new Error(res.error || res.message || '创建订单失败')
}
@@ -767,11 +836,12 @@ Page({
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
try {
+ const orderSn = paymentData._orderSn
await this.callWechatPay(paymentData)
- // 4. 【标准流程】支付成功后刷新权限并解锁内容
+ // 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机)
console.log('[Pay] 微信支付成功!')
- await this.onPaymentSuccess()
+ await this.onPaymentSuccess(orderSn)
} catch (payErr) {
console.error('[Pay] 微信支付调起失败:', payErr)
@@ -807,13 +877,34 @@ Page({
}
},
+ // 轮询订单状态,确认 paid 后刷新权限并解锁
+ async pollOrderUntilPaid(orderSn) {
+ const maxAttempts = 15
+ const interval = 800
+ for (let i = 0; i < maxAttempts; i++) {
+ try {
+ const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
+ if (r?.data?.status === 'paid') return true
+ } catch (_) {}
+ if (i < maxAttempts - 1) await this.sleep(interval)
+ }
+ return false
+ },
+
// 【新增】支付成功后的标准处理流程
- async onPaymentSuccess() {
+ async onPaymentSuccess(orderSn) {
wx.showLoading({ title: '确认购买中...', mask: true })
try {
- // 1. 等待服务端处理支付回调(1-2秒)
- await this.sleep(2000)
+ // 1. 轮询订单状态直到已支付(GET pay 会主动同步本地订单,不依赖 PayNotify)
+ if (orderSn) {
+ const paid = await this.pollOrderUntilPaid(orderSn)
+ if (!paid) {
+ console.warn('[Pay] 轮询超时,仍尝试刷新')
+ }
+ } else {
+ await this.sleep(1500)
+ }
// 2. 刷新用户购买状态
await accessManager.refreshUserPurchaseStatus()
@@ -936,134 +1027,115 @@ Page({
wx.navigateTo({ url: '/pages/referral/referral' })
},
- // 生成海报
+ // 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染)
async generatePoster() {
wx.showLoading({ title: '生成中...' })
this.setData({ showPosterModal: true, isGeneratingPoster: true })
+ const { section, contentParagraphs, sectionId } = this.data
+ const userInfo = app.globalData.userInfo
+ const userId = userInfo?.id || ''
+ const safeParagraphs = contentParagraphs || []
+
+ // 通过 GET 接口下载二维码图片,得到 tempFilePath 便于开发工具与真机统一用 drawImage 绘制
+ let qrcodeTempPath = null
try {
- const ctx = wx.createCanvasContext('posterCanvas', this)
- const { section, contentParagraphs, sectionId } = this.data
- const userInfo = app.globalData.userInfo
- const userId = userInfo?.id || ''
-
- // 获取小程序码(带推荐人参数)
- let qrcodeImage = null
- try {
- const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
- const qrRes = await app.request('/api/miniprogram/qrcode', {
- method: 'POST',
- data: { scene, page: 'pages/read/read', width: 280 }
+ const scene = userId ? `id=${sectionId}&ref=${userId.slice(0, 10)}` : `id=${sectionId}`
+ const baseUrl = app.globalData.baseUrl || ''
+ const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280`
+ qrcodeTempPath = await new Promise((resolve) => {
+ wx.downloadFile({
+ url,
+ success: (res) => resolve(res.statusCode === 200 ? res.tempFilePath : null),
+ fail: () => resolve(null)
})
- if (qrRes.success && qrRes.image) {
- qrcodeImage = qrRes.image
- }
- } catch (e) {
- console.log('[Poster] 获取小程序码失败,使用占位符')
- }
-
- // 海报尺寸 300x450
- const width = 300
- const height = 450
-
- // 背景渐变
- const grd = ctx.createLinearGradient(0, 0, 0, height)
- grd.addColorStop(0, '#1a1a2e')
- grd.addColorStop(1, '#16213e')
- ctx.setFillStyle(grd)
- ctx.fillRect(0, 0, width, height)
-
- // 顶部装饰条
- ctx.setFillStyle('#00CED1')
- ctx.fillRect(0, 0, width, 4)
-
- // 标题区域
- ctx.setFillStyle('#ffffff')
- ctx.setFontSize(14)
- ctx.fillText('📚 Soul创业派对', 20, 35)
-
- // 章节标题
- ctx.setFontSize(18)
- ctx.setFillStyle('#ffffff')
- const title = section?.title || '精彩内容'
- const titleLines = this.wrapText(ctx, title, width - 40, 18)
- let y = 70
- titleLines.forEach(line => {
- ctx.fillText(line, 20, y)
- y += 26
- })
-
- // 分隔线
- ctx.setStrokeStyle('rgba(255,255,255,0.1)')
- ctx.beginPath()
- ctx.moveTo(20, y + 10)
- ctx.lineTo(width - 20, y + 10)
- ctx.stroke()
-
- // 内容摘要
- ctx.setFontSize(12)
- ctx.setFillStyle('rgba(255,255,255,0.8)')
- y += 30
- const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
- const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
- summaryLines.slice(0, 6).forEach(line => {
- ctx.fillText(line, 20, y)
- y += 20
- })
-
- // 底部区域背景
- ctx.setFillStyle('rgba(0,206,209,0.1)')
- ctx.fillRect(0, height - 100, width, 100)
-
- // 左侧提示文字
- ctx.setFillStyle('#ffffff')
- ctx.setFontSize(13)
- ctx.fillText('长按识别小程序码', 20, height - 60)
- ctx.setFillStyle('rgba(255,255,255,0.6)')
- ctx.setFontSize(11)
- ctx.fillText('长按小程序码阅读全文', 20, height - 38)
-
- // 绘制小程序码或占位符
- const drawQRCode = () => {
- return new Promise((resolve) => {
- if (qrcodeImage) {
- // 下载base64图片并绘制
- const fs = wx.getFileSystemManager()
- const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
- const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
-
- fs.writeFile({
- filePath,
- data: base64Data,
- encoding: 'base64',
- success: () => {
- ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
- resolve()
- },
- fail: () => {
- this.drawQRPlaceholder(ctx, width, height)
- resolve()
- }
- })
- } else {
- this.drawQRPlaceholder(ctx, width, height)
- resolve()
- }
- })
- }
-
- await drawQRCode()
-
- ctx.draw(true, () => {
- wx.hideLoading()
- this.setData({ isGeneratingPoster: false })
})
} catch (e) {
- console.error('生成海报失败:', e)
- wx.hideLoading()
- wx.showToast({ title: '生成失败', icon: 'none' })
- this.setData({ showPosterModal: false, isGeneratingPoster: false })
+ console.log('[Poster] 获取小程序码失败,使用占位符')
}
+
+ const doDraw = () => {
+ try {
+ const ctx = wx.createCanvasContext('posterCanvas', this)
+ const width = 300
+ const height = 450
+
+ const grd = ctx.createLinearGradient(0, 0, 0, height)
+ grd.addColorStop(0, '#1a1a2e')
+ grd.addColorStop(1, '#16213e')
+ ctx.setFillStyle(grd)
+ ctx.fillRect(0, 0, width, height)
+
+ ctx.setFillStyle('#00CED1')
+ ctx.fillRect(0, 0, width, 4)
+
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(14)
+ ctx.fillText('📚 Soul创业派对', 20, 35)
+
+ ctx.setFontSize(18)
+ ctx.setFillStyle('#ffffff')
+ const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
+ const titleLines = this.wrapText(ctx, title, width - 40, 18)
+ let y = 70
+ titleLines.forEach(line => {
+ ctx.fillText(line, 20, y)
+ y += 26
+ })
+
+ ctx.setStrokeStyle('rgba(255,255,255,0.1)')
+ ctx.beginPath()
+ ctx.moveTo(20, y + 10)
+ ctx.lineTo(width - 20, y + 10)
+ ctx.stroke()
+
+ ctx.setFontSize(12)
+ ctx.setFillStyle('rgba(255,255,255,0.8)')
+ y += 30
+ const summary = safeParagraphs.slice(0, 3).join(' ').replace(/\s+/g, ' ').trim().slice(0, 150)
+ const summaryText = summary ? summary + (summary.length >= 150 ? '...' : '') : '来自派对房的真实商业故事'
+ const summaryLines = this.wrapText(ctx, summaryText, width - 40, 12)
+ summaryLines.slice(0, 6).forEach(line => {
+ ctx.fillText(line, 20, y)
+ y += 20
+ })
+
+ ctx.setFillStyle('rgba(0,206,209,0.1)')
+ ctx.fillRect(0, height - 100, width, 100)
+
+ ctx.setFillStyle('#ffffff')
+ ctx.setFontSize(13)
+ ctx.fillText('长按识别小程序码', 20, height - 60)
+ ctx.setFillStyle('rgba(255,255,255,0.6)')
+ ctx.setFontSize(11)
+ ctx.fillText('长按小程序码阅读全文', 20, height - 38)
+
+ const drawQRCode = () => {
+ return new Promise((resolve) => {
+ if (qrcodeTempPath) {
+ ctx.drawImage(qrcodeTempPath, width - 85, height - 85, 70, 70)
+ } else {
+ this.drawQRPlaceholder(ctx, width, height)
+ }
+ resolve()
+ })
+ }
+
+ drawQRCode().then(() => {
+ ctx.draw(true, () => {
+ wx.hideLoading()
+ this.setData({ isGeneratingPoster: false })
+ })
+ })
+ } catch (e) {
+ console.error('生成海报失败:', e)
+ wx.hideLoading()
+ wx.showToast({ title: '生成失败', icon: 'none' })
+ this.setData({ showPosterModal: false, isGeneratingPoster: false })
+ }
+ }
+
+ setTimeout(doDraw, 400)
},
// 绘制小程序码占位符
@@ -1101,11 +1173,20 @@ Page({
this.setData({ showPosterModal: false })
},
- // 保存海报到相册
+ // 保存海报到相册(与海报绘制尺寸一致,必须传 destWidth/destHeight 否则部分机型导出失败)
savePoster() {
+ const width = 300
+ const height = 450
wx.canvasToTempFilePath({
canvasId: 'posterCanvas',
+ destWidth: width,
+ destHeight: height,
+ fileType: 'png',
success: (res) => {
+ if (!res.tempFilePath) {
+ wx.showToast({ title: '生成图片失败', icon: 'none' })
+ return
+ }
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
@@ -1113,25 +1194,25 @@ Page({
this.setData({ showPosterModal: false })
},
fail: (err) => {
- if (err.errMsg.includes('auth deny')) {
+ console.error('[savePoster] saveImageToPhotosAlbum fail:', err)
+ if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
wx.showModal({
title: '提示',
content: '需要相册权限才能保存海报',
confirmText: '去设置',
- success: (res) => {
- if (res.confirm) {
- wx.openSetting()
- }
+ success: (sres) => {
+ if (sres.confirm) wx.openSetting()
}
})
} else {
- wx.showToast({ title: '保存失败', icon: 'none' })
+ wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' })
}
}
})
},
- fail: () => {
- wx.showToast({ title: '生成图片失败', icon: 'none' })
+ fail: (err) => {
+ console.error('[savePoster] canvasToTempFilePath fail:', err)
+ wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' })
}
}, this)
},
diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml
index a17cc3b3..d9928905 100644
--- a/miniprogram/pages/read/read.wxml
+++ b/miniprogram/pages/read/read.wxml
@@ -297,4 +297,7 @@
+
+
+
diff --git a/miniprogram/pages/read/read.wxss b/miniprogram/pages/read/read.wxss
index 8b0ecca2..cb924721 100644
--- a/miniprogram/pages/read/read.wxss
+++ b/miniprogram/pages/read/read.wxss
@@ -912,6 +912,14 @@
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
}
+/* 分享卡片 canvas:离屏绘制,不展示给用户 */
+.share-card-canvas {
+ position: fixed;
+ left: -600px;
+ top: 0;
+ z-index: -1;
+}
+
.poster-actions {
display: flex;
gap: 24rpx;
diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js
index 6df3905e..51edc2cb 100644
--- a/miniprogram/pages/referral/referral.js
+++ b/miniprogram/pages/referral/referral.js
@@ -796,14 +796,13 @@ Page({
})
},
- // 分享 - 带推荐码
+ // 分享 - 带自己的邀请码(与 app.getMyReferralCode 一致)
onShareAppMessage() {
- console.log('[Referral] 分享给好友,推荐码:', this.data.referralCode)
+ const app = getApp()
+ const ref = app.getMyReferralCode() || this.data.referralCode
return {
title: 'Soul创业派对 - 来自派对房的真实商业故事',
- path: `/pages/index/index?ref=${this.data.referralCode}`
- // 不设置 imageUrl,使用小程序默认截图
- // 如需自定义图片,请将图片放在 /assets/ 目录并配置路径
+ path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
}
},
diff --git a/miniprogram/pages/scan/scan.js b/miniprogram/pages/scan/scan.js
new file mode 100644
index 00000000..8b6c42e2
--- /dev/null
+++ b/miniprogram/pages/scan/scan.js
@@ -0,0 +1,157 @@
+/**
+ * Soul创业派对 - 扫码解析页
+ * 扫描二维码/条形码,展示解析内容
+ */
+const app = getApp()
+
+Page({
+ data: {
+ statusBarHeight: 44,
+ // 最近一次解析结果
+ lastResult: null,
+ scanType: '',
+ charSet: '',
+ // 小程序码解析(路径、参数)
+ parsedPath: null,
+ parsedQuery: [],
+ canNavigate: false,
+ // 历史记录
+ history: []
+ },
+
+ onLoad() {
+ this.setData({
+ statusBarHeight: app.globalData.statusBarHeight || 44
+ })
+ this.loadHistory()
+ },
+
+ loadHistory() {
+ try {
+ const history = wx.getStorageSync('scanHistory') || []
+ this.setData({ history })
+ } catch (e) {
+ console.log('加载扫码历史失败:', e)
+ }
+ },
+
+ saveToHistory(result, scanType, charSet) {
+ const item = { result, scanType, charSet, time: new Date().toLocaleString() }
+ let history = wx.getStorageSync('scanHistory') || []
+ history = [item, ...history].slice(0, 10)
+ wx.setStorageSync('scanHistory', history)
+ this.setData({ history })
+ },
+
+ // 解析小程序码内容:path?key=val 或 path
+ parseMiniProgramCode(result) {
+ if (!result || typeof result !== 'string') return { path: null, query: [], canNavigate: false }
+ const idx = result.indexOf('?')
+ let path = idx >= 0 ? result.slice(0, idx) : result
+ const qs = idx >= 0 ? result.slice(idx + 1) : ''
+ path = path.replace(/^\//, '').trim()
+ const query = []
+ if (qs) {
+ qs.split('&').forEach(pair => {
+ const eq = pair.indexOf('=')
+ const k = eq >= 0 ? pair.slice(0, eq) : pair
+ const v = eq >= 0 ? pair.slice(eq + 1) : ''
+ try {
+ if (k) query.push({ key: decodeURIComponent(k), value: decodeURIComponent(v) })
+ } catch (_) {
+ if (k) query.push({ key: k, value: v })
+ }
+ })
+ }
+ const isMiniProgramPath = /^pages\/[\w-]+\/[\w-]+$/.test(path)
+ return { path: path || null, query, canNavigate: isMiniProgramPath }
+ },
+
+ // 发起扫码(支持小程序码)
+ doScan() {
+ wx.scanCode({
+ onlyFromCamera: false,
+ scanType: ['qrCode', 'barCode'],
+ success: (res) => {
+ const { result, scanType, charSet } = res
+ const parsed = this.parseMiniProgramCode(result)
+ this.setData({
+ lastResult: result,
+ scanType: scanType || '未知',
+ charSet: charSet || '',
+ parsedPath: parsed.path,
+ parsedQuery: parsed.query,
+ canNavigate: parsed.canNavigate
+ })
+ this.saveToHistory(result, scanType, charSet)
+ },
+ fail: (err) => {
+ if (err.errMsg && err.errMsg.includes('cancel')) {
+ return
+ }
+ wx.showToast({ title: err.errMsg || '扫码失败', icon: 'none' })
+ }
+ })
+ },
+
+ // 复制内容
+ copyResult() {
+ const { lastResult } = this.data
+ if (!lastResult) {
+ wx.showToast({ title: '暂无解析内容', icon: 'none' })
+ return
+ }
+ wx.setClipboardData({
+ data: lastResult,
+ success: () => wx.showToast({ title: '已复制', icon: 'success' })
+ })
+ },
+
+ // 清空当前结果
+ clearResult() {
+ this.setData({
+ lastResult: null, scanType: '', charSet: '',
+ parsedPath: null, parsedQuery: [], canNavigate: false
+ })
+ },
+
+ // 打开解析出的小程序页面
+ openParsedPage() {
+ const { parsedPath, parsedQuery } = this.data
+ if (!parsedPath || !this.data.canNavigate) {
+ wx.showToast({ title: '非本小程序页面', icon: 'none' })
+ return
+ }
+ const queryStr = parsedQuery.length
+ ? '?' + parsedQuery.map(q => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join('&')
+ : ''
+ const url = `/${parsedPath}${queryStr}`
+ const tabPages = ['pages/index/index', 'pages/chapters/chapters', 'pages/match/match', 'pages/my/my']
+ if (tabPages.includes(parsedPath)) {
+ wx.switchTab({ url: `/${parsedPath}` })
+ } else {
+ wx.navigateTo({ url })
+ }
+ },
+
+ // 清空历史
+ clearHistory() {
+ wx.setStorageSync('scanHistory', [])
+ this.setData({ history: [], lastResult: null, scanType: '', charSet: '' })
+ wx.showToast({ title: '已清空', icon: 'success' })
+ },
+
+ // 点击历史项复制
+ onHistoryItemTap(e) {
+ const result = e.currentTarget.dataset.result
+ if (!result) return
+ wx.setClipboardData({
+ data: result,
+ success: () => wx.showToast({ title: '已复制', icon: 'success' })
+ })
+ },
+
+ goBack() {
+ wx.navigateBack()
+ }
+})
diff --git a/miniprogram/pages/scan/scan.json b/miniprogram/pages/scan/scan.json
new file mode 100644
index 00000000..88880e06
--- /dev/null
+++ b/miniprogram/pages/scan/scan.json
@@ -0,0 +1 @@
+{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"扫码解析"}
diff --git a/miniprogram/pages/scan/scan.wxml b/miniprogram/pages/scan/scan.wxml
new file mode 100644
index 00000000..a58da57c
--- /dev/null
+++ b/miniprogram/pages/scan/scan.wxml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+ ←
+
+ 扫码解析
+
+
+
+
+
+
+
+
+ 📷
+ 扫描小程序码 / 二维码
+
+
+
+
+
+
+
+
+
+ 路径
+ {{parsedPath}}
+
+
+ {{q.key}}
+ {{q.value}}
+
+
+
+ 类型: {{scanType}}
+ 字符集: {{charSet}}
+
+
+ {{lastResult}}
+
+
+
+
+
+ 📷
+ 点击上方按钮扫码
+ 支持小程序码、二维码、条形码
+
+
+
+
+
+
+
+ {{item.result}}
+ {{item.time}}
+
+
+
+
+
diff --git a/miniprogram/pages/scan/scan.wxss b/miniprogram/pages/scan/scan.wxss
new file mode 100644
index 00000000..1e2c30a4
--- /dev/null
+++ b/miniprogram/pages/scan/scan.wxss
@@ -0,0 +1,248 @@
+/* 扫码解析页样式 */
+.page {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #0a0a0a 0%, #111111 100%);
+}
+
+.nav-bar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: rgba(10, 10, 10, 0.95);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+}
+
+.nav-content {
+ display: flex;
+ align-items: center;
+ padding: 8rpx 24rpx;
+ height: 88rpx;
+}
+
+.back-btn {
+ width: 60rpx;
+ height: 60rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.back-icon {
+ font-size: 40rpx;
+ color: #00CED1;
+}
+
+.nav-title {
+ flex: 1;
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #fff;
+ text-align: center;
+ margin-right: 60rpx;
+}
+
+.main-content {
+ padding: 24rpx;
+}
+
+/* 扫码按钮 */
+.scan-action {
+ padding: 60rpx 0;
+}
+
+.scan-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.15) 100%);
+ border: 2rpx solid rgba(0, 206, 209, 0.4);
+ border-radius: 32rpx;
+ padding: 80rpx 48rpx;
+}
+
+.scan-icon {
+ font-size: 80rpx;
+ margin-bottom: 24rpx;
+}
+
+.scan-text {
+ font-size: 32rpx;
+ color: #00CED1;
+ font-weight: 500;
+}
+
+/* 解析结果卡片 */
+.result-card {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 24rpx;
+ padding: 32rpx;
+ margin-top: 32rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.08);
+}
+
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.result-label {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+ font-weight: 500;
+}
+
+.result-actions {
+ display: flex;
+ gap: 24rpx;
+}
+
+.action-btn {
+ font-size: 26rpx;
+ color: #00CED1;
+ padding: 8rpx 20rpx;
+}
+
+.action-btn.secondary {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* 小程序码解析:路径+参数 */
+.parsed-section {
+ background: rgba(0, 206, 209, 0.08);
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 24rpx;
+ border: 1rpx solid rgba(0, 206, 209, 0.2);
+}
+
+.parsed-row {
+ display: flex;
+ align-items: baseline;
+ margin-bottom: 12rpx;
+}
+
+.parsed-row:last-child {
+ margin-bottom: 0;
+}
+
+.parsed-label {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.5);
+ min-width: 120rpx;
+ flex-shrink: 0;
+}
+
+.parsed-value {
+ font-size: 26rpx;
+ color: #00CED1;
+ word-break: break-all;
+}
+
+.result-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16rpx;
+ margin-bottom: 20rpx;
+}
+
+.meta-item {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.result-content {
+ max-height: 400rpx;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 16rpx;
+ padding: 24rpx;
+}
+
+.result-text {
+ font-size: 28rpx;
+ color: #fff;
+ line-height: 1.6;
+ word-break: break-all;
+ white-space: pre-wrap;
+}
+
+/* 无结果提示 */
+.empty-tip {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 60rpx 0;
+}
+
+.empty-icon {
+ font-size: 80rpx;
+ margin-bottom: 20rpx;
+ opacity: 0.5;
+}
+
+.empty-text {
+ font-size: 30rpx;
+ color: rgba(255, 255, 255, 0.6);
+ margin-bottom: 8rpx;
+}
+
+.empty-desc {
+ font-size: 24rpx;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* 扫码历史 */
+.history-section {
+ margin-top: 48rpx;
+}
+
+.history-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.history-title {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.clear-history {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.history-item {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 16rpx;
+ padding: 24rpx;
+ border: 1rpx solid rgba(255, 255, 255, 0.06);
+}
+
+.history-content {
+ font-size: 26rpx;
+ color: #fff;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.history-time {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.4);
+ margin-top: 8rpx;
+ display: block;
+}
diff --git a/miniprogram/pages/search/search.js b/miniprogram/pages/search/search.js
index 1ac887d9..0eeb383c 100644
--- a/miniprogram/pages/search/search.js
+++ b/miniprogram/pages/search/search.js
@@ -105,5 +105,13 @@ Page({
// 返回上一页
goBack() {
wx.navigateBack()
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 搜索',
+ path: ref ? `/pages/search/search?ref=${ref}` : '/pages/search/search'
+ }
}
})
diff --git a/miniprogram/pages/settings/settings.js b/miniprogram/pages/settings/settings.js
index 157af2f0..c46ab852 100644
--- a/miniprogram/pages/settings/settings.js
+++ b/miniprogram/pages/settings/settings.js
@@ -493,5 +493,13 @@ Page({
// 跳转到地址管理页
goToAddresses() {
wx.navigateTo({ url: '/pages/addresses/addresses' })
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 设置',
+ path: ref ? `/pages/settings/settings?ref=${ref}` : '/pages/settings/settings'
+ }
}
})
diff --git a/miniprogram/pages/withdraw-records/withdraw-records.js b/miniprogram/pages/withdraw-records/withdraw-records.js
index 5ef1c2fd..9757efac 100644
--- a/miniprogram/pages/withdraw-records/withdraw-records.js
+++ b/miniprogram/pages/withdraw-records/withdraw-records.js
@@ -119,5 +119,13 @@ Page({
wx.hideLoading()
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
}
+ },
+
+ onShareAppMessage() {
+ const ref = app.getMyReferralCode()
+ return {
+ title: 'Soul创业派对 - 提现记录',
+ path: ref ? `/pages/withdraw-records/withdraw-records?ref=${ref}` : '/pages/withdraw-records/withdraw-records'
+ }
}
})
diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json
index 10365da5..fa27e553 100644
--- a/miniprogram/project.private.config.json
+++ b/miniprogram/project.private.config.json
@@ -20,73 +20,5 @@
"useIsolateContext": true
},
"libVersion": "3.13.2",
- "condition": {
- "miniprogram": {
- "list": [
- {
- "name": "找伙伴",
- "pathName": "pages/match/match",
- "query": "",
- "scene": null,
- "launchMode": "default"
- },
- {
- "name": "pages/read/read",
- "pathName": "pages/read/read",
- "query": "id=1.1",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "pages/match/match",
- "pathName": "pages/match/match",
- "query": "",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "看书",
- "pathName": "pages/read/read",
- "query": "id=1.4",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "分销中心",
- "pathName": "pages/referral/referral",
- "query": "",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "阅读",
- "pathName": "pages/read/read",
- "query": "id=1.1",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "分销中心",
- "pathName": "pages/referral/referral",
- "query": "",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "我的",
- "pathName": "pages/my/my",
- "query": "",
- "launchMode": "default",
- "scene": null
- },
- {
- "name": "新增地址",
- "pathName": "pages/addresses/edit",
- "query": "",
- "launchMode": "default",
- "scene": null
- }
- ]
- }
- }
+ "condition": {}
}
\ No newline at end of file
diff --git a/soul-admin/node_modules/.vite/deps/@radix-ui_react-dialog.js b/soul-admin/node_modules/.vite/deps/@radix-ui_react-dialog.js
index 892d8d13..fb2478cf 100644
--- a/soul-admin/node_modules/.vite/deps/@radix-ui_react-dialog.js
+++ b/soul-admin/node_modules/.vite/deps/@radix-ui_react-dialog.js
@@ -1,4 +1,7 @@
"use client";
+import {
+ Presence
+} from "./chunk-NOU7F7EJ.js";
import {
Combination_default,
DismissableLayer,
@@ -7,9 +10,6 @@ import {
hideOthers,
useFocusGuards
} from "./chunk-TQPPICSF.js";
-import {
- Presence
-} from "./chunk-NOU7F7EJ.js";
import {
useId
} from "./chunk-LAKFU2YZ.js";
diff --git a/soul-admin/node_modules/.vite/deps/@radix-ui_react-label.js b/soul-admin/node_modules/.vite/deps/@radix-ui_react-label.js
index 9e9cbb84..0192cf29 100644
--- a/soul-admin/node_modules/.vite/deps/@radix-ui_react-label.js
+++ b/soul-admin/node_modules/.vite/deps/@radix-ui_react-label.js
@@ -1,10 +1,10 @@
"use client";
-import {
- require_react_dom
-} from "./chunk-4GC24YIX.js";
import {
createSlot
} from "./chunk-GDI5LHIV.js";
+import {
+ require_react_dom
+} from "./chunk-4GC24YIX.js";
import "./chunk-H5WV2N77.js";
import {
require_jsx_runtime
diff --git a/soul-admin/node_modules/.vite/deps/@radix-ui_react-select.js b/soul-admin/node_modules/.vite/deps/@radix-ui_react-select.js
index 06f6eb02..8758677f 100644
--- a/soul-admin/node_modules/.vite/deps/@radix-ui_react-select.js
+++ b/soul-admin/node_modules/.vite/deps/@radix-ui_react-select.js
@@ -2,14 +2,14 @@
import {
clamp
} from "./chunk-QSHREGVI.js";
+import {
+ createCollection,
+ useDirection
+} from "./chunk-WRFKI3SR.js";
import {
usePrevious,
useSize
} from "./chunk-L3UKNFUA.js";
-import {
- createCollection,
- useDirection
-} from "./chunk-CEO7JH3I.js";
import {
Combination_default,
DismissableLayer,
diff --git a/soul-admin/node_modules/.vite/deps/@radix-ui_react-slider.js b/soul-admin/node_modules/.vite/deps/@radix-ui_react-slider.js
index 22537ff9..7a342310 100644
--- a/soul-admin/node_modules/.vite/deps/@radix-ui_react-slider.js
+++ b/soul-admin/node_modules/.vite/deps/@radix-ui_react-slider.js
@@ -2,14 +2,14 @@
import {
clamp
} from "./chunk-QSHREGVI.js";
+import {
+ createCollection,
+ useDirection
+} from "./chunk-WRFKI3SR.js";
import {
usePrevious,
useSize
} from "./chunk-L3UKNFUA.js";
-import {
- createCollection,
- useDirection
-} from "./chunk-CEO7JH3I.js";
import {
Primitive,
composeEventHandlers,
diff --git a/soul-admin/node_modules/.vite/deps/@radix-ui_react-tabs.js b/soul-admin/node_modules/.vite/deps/@radix-ui_react-tabs.js
index 1052eb61..7afb3b68 100644
--- a/soul-admin/node_modules/.vite/deps/@radix-ui_react-tabs.js
+++ b/soul-admin/node_modules/.vite/deps/@radix-ui_react-tabs.js
@@ -2,7 +2,7 @@
import {
createCollection,
useDirection
-} from "./chunk-CEO7JH3I.js";
+} from "./chunk-WRFKI3SR.js";
import {
Presence
} from "./chunk-NOU7F7EJ.js";
diff --git a/soul-admin/node_modules/.vite/deps/_metadata.json b/soul-admin/node_modules/.vite/deps/_metadata.json
index 07a243a8..5974a6ed 100644
--- a/soul-admin/node_modules/.vite/deps/_metadata.json
+++ b/soul-admin/node_modules/.vite/deps/_metadata.json
@@ -1,109 +1,109 @@
{
- "hash": "13546d7b",
+ "hash": "bc53cdae",
"configHash": "c74fa922",
- "lockfileHash": "816dbc6e",
- "browserHash": "392799fb",
+ "lockfileHash": "e3f3f1b6",
+ "browserHash": "7326a949",
"optimized": {
"react": {
"src": "../../.pnpm/react@18.3.1/node_modules/react/index.js",
"file": "react.js",
- "fileHash": "ee001604",
+ "fileHash": "843e764f",
"needsInterop": true
},
"react-dom": {
"src": "../../.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/index.js",
"file": "react-dom.js",
- "fileHash": "dae2d510",
+ "fileHash": "f71c92bb",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../.pnpm/react@18.3.1/node_modules/react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
- "fileHash": "c7694ff5",
+ "fileHash": "9eca8055",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
- "fileHash": "f3e00475",
+ "fileHash": "0dbe7d5b",
"needsInterop": true
},
"@radix-ui/react-dialog": {
"src": "../../.pnpm/@radix-ui+react-dialog@1.1._9cd126aa2880bf880e57e64087c058d6/node_modules/@radix-ui/react-dialog/dist/index.mjs",
"file": "@radix-ui_react-dialog.js",
- "fileHash": "0416d3c6",
+ "fileHash": "3b38be5d",
"needsInterop": false
},
"@radix-ui/react-label": {
"src": "../../.pnpm/@radix-ui+react-label@2.1.8_8915bfc4015ea7714adf8fc777d50ad8/node_modules/@radix-ui/react-label/dist/index.mjs",
"file": "@radix-ui_react-label.js",
- "fileHash": "3b5cd4b9",
+ "fileHash": "aa320a14",
"needsInterop": false
},
"@radix-ui/react-select": {
"src": "../../.pnpm/@radix-ui+react-select@2.2._f9b05e8db7247fc075715ae52301583d/node_modules/@radix-ui/react-select/dist/index.mjs",
"file": "@radix-ui_react-select.js",
- "fileHash": "eec510f7",
+ "fileHash": "490b1f6f",
"needsInterop": false
},
"@radix-ui/react-slider": {
"src": "../../.pnpm/@radix-ui+react-slider@1.3._447f5338eb4e3826348b8f99da1e7596/node_modules/@radix-ui/react-slider/dist/index.mjs",
"file": "@radix-ui_react-slider.js",
- "fileHash": "349206b0",
+ "fileHash": "980a24c0",
"needsInterop": false
},
"@radix-ui/react-slot": {
"src": "../../.pnpm/@radix-ui+react-slot@1.2.4_@types+react@18.3.28_react@18.3.1/node_modules/@radix-ui/react-slot/dist/index.mjs",
"file": "@radix-ui_react-slot.js",
- "fileHash": "a24a0b17",
+ "fileHash": "e1819b67",
"needsInterop": false
},
"@radix-ui/react-switch": {
"src": "../../.pnpm/@radix-ui+react-switch@1.2._0225fbd6ccc84d9eda18b8a03485bb75/node_modules/@radix-ui/react-switch/dist/index.mjs",
"file": "@radix-ui_react-switch.js",
- "fileHash": "b750fdf3",
+ "fileHash": "915561ff",
"needsInterop": false
},
"@radix-ui/react-tabs": {
"src": "../../.pnpm/@radix-ui+react-tabs@1.1.13_6db026cd1527317527c6849c7bd26c2f/node_modules/@radix-ui/react-tabs/dist/index.mjs",
"file": "@radix-ui_react-tabs.js",
- "fileHash": "65c9dd67",
+ "fileHash": "13909eaa",
"needsInterop": false
},
"class-variance-authority": {
"src": "../../.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/index.mjs",
"file": "class-variance-authority.js",
- "fileHash": "2f219f9c",
+ "fileHash": "c8668335",
"needsInterop": false
},
"clsx": {
"src": "../../.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs",
"file": "clsx.js",
- "fileHash": "3ac7f8dd",
+ "fileHash": "cbd98ff1",
"needsInterop": false
},
"lucide-react": {
"src": "../../.pnpm/lucide-react@0.562.0_react@18.3.1/node_modules/lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
- "fileHash": "ca2692bf",
+ "fileHash": "d7138360",
"needsInterop": false
},
"react-dom/client": {
"src": "../../.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/client.js",
"file": "react-dom_client.js",
- "fileHash": "5b781ac3",
+ "fileHash": "fa1c6ee3",
"needsInterop": true
},
"react-router-dom": {
"src": "../../.pnpm/react-router-dom@6.30.3_rea_8738f2f356869a9d467b32612b8c1bd5/node_modules/react-router-dom/dist/index.js",
"file": "react-router-dom.js",
- "fileHash": "b98b9236",
+ "fileHash": "e77ab7bf",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
- "fileHash": "a33bcba1",
+ "fileHash": "4493c29c",
"needsInterop": false
}
},
@@ -111,18 +111,21 @@
"chunk-QSHREGVI": {
"file": "chunk-QSHREGVI.js"
},
+ "chunk-WRFKI3SR": {
+ "file": "chunk-WRFKI3SR.js"
+ },
+ "chunk-GDI5LHIV": {
+ "file": "chunk-GDI5LHIV.js"
+ },
"chunk-L3UKNFUA": {
"file": "chunk-L3UKNFUA.js"
},
- "chunk-CEO7JH3I": {
- "file": "chunk-CEO7JH3I.js"
+ "chunk-NOU7F7EJ": {
+ "file": "chunk-NOU7F7EJ.js"
},
"chunk-TQPPICSF": {
"file": "chunk-TQPPICSF.js"
},
- "chunk-NOU7F7EJ": {
- "file": "chunk-NOU7F7EJ.js"
- },
"chunk-LAKFU2YZ": {
"file": "chunk-LAKFU2YZ.js"
},
@@ -132,9 +135,6 @@
"chunk-4GC24YIX": {
"file": "chunk-4GC24YIX.js"
},
- "chunk-GDI5LHIV": {
- "file": "chunk-GDI5LHIV.js"
- },
"chunk-H5WV2N77": {
"file": "chunk-H5WV2N77.js"
},
diff --git a/soul-admin/src/components/ui/slider.tsx b/soul-admin/src/components/ui/slider.tsx
index 5fe231de..f73063b6 100644
--- a/soul-admin/src/components/ui/slider.tsx
+++ b/soul-admin/src/components/ui/slider.tsx
@@ -32,8 +32,8 @@ function Slider({
)}
{...props}
>
-
-
+
+
{Array.from({ length: _values.length }, (_, index) => (
?", "created", cutoff).Find(&createdOrders).Error; err != nil {
+ c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
+ return
+ }
+
+ synced := 0
+ ctx := context.Background()
+ for _, o := range createdOrders {
+ tradeState, transactionID, totalFee, err := wechat.QueryOrderByOutTradeNo(ctx, o.OrderSN)
+ if err != nil {
+ fmt.Printf("[SyncOrders] 查询订单 %s 失败: %v\n", o.OrderSN, err)
+ continue
+ }
+ if tradeState != "SUCCESS" {
+ continue
+ }
+ // 微信已支付,本地未更新 → 补齐
+ totalAmount := float64(totalFee) / 100
+ now := time.Now()
+ if err := db.Model(&o).Updates(map[string]interface{}{
+ "status": "paid",
+ "transaction_id": transactionID,
+ "pay_time": now,
+ "updated_at": now,
+ }).Error; err != nil {
+ fmt.Printf("[SyncOrders] 更新订单 %s 失败: %v\n", o.OrderSN, err)
+ continue
+ }
+ synced++
+ fmt.Printf("[SyncOrders] 补齐漏单: %s, amount=%.2f\n", o.OrderSN, totalAmount)
+
+ // 同步后续逻辑(全书、分销等)
+ pt := "fullbook"
+ if o.ProductType != "" {
+ pt = o.ProductType
+ }
+ if pt == "fullbook" {
+ db.Model(&model.User{}).Where("id = ?", o.UserID).Update("has_full_book", true)
+ }
+ processReferralCommission(db, o.UserID, totalAmount, o.OrderSN)
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "synced": synced,
+ "total": len(createdOrders),
+ })
}
// CronUnbindExpired GET/POST /api/cron/unbind-expired
diff --git a/soul-api/internal/handler/miniprogram.go b/soul-api/internal/handler/miniprogram.go
index 9341295c..6bf0690a 100644
--- a/soul-api/internal/handler/miniprogram.go
+++ b/soul-api/internal/handler/miniprogram.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
+ "strconv"
"strings"
"time"
@@ -425,15 +426,21 @@ func MiniprogramPayNotify(c *gin.Context) {
TransactionID: &transactionID,
PayTime: &now,
}
- db.Create(&order)
+ if err := db.Create(&order).Error; err != nil {
+ fmt.Printf("[PayNotify] 补记订单失败: %s, err=%v\n", orderSn, err)
+ return fmt.Errorf("create order: %w", err)
+ }
} else if *order.Status != "paid" {
status := "paid"
now := time.Now()
- db.Model(&order).Updates(map[string]interface{}{
+ if err := db.Model(&order).Updates(map[string]interface{}{
"status": status,
"transaction_id": transactionID,
"pay_time": now,
- })
+ }).Error; err != nil {
+ fmt.Printf("[PayNotify] 更新订单状态失败: %s, err=%v\n", orderSn, err)
+ return fmt.Errorf("update order: %w", err)
+ }
fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn)
} else {
fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn)
@@ -666,6 +673,30 @@ func MiniprogramQrcode(c *gin.Context) {
})
}
+// MiniprogramQrcodeImage GET /api/miniprogram/qrcode/image?scene=xxx&page=xxx&width=280
+// 直接返回 image/png,供小程序 wx.downloadFile 使用,便于开发工具与真机统一用 tempFilePath 绘制
+func MiniprogramQrcodeImage(c *gin.Context) {
+ scene := c.Query("scene")
+ if scene == "" {
+ scene = "soul"
+ }
+ page := c.DefaultQuery("page", "pages/read/read")
+ width, _ := strconv.Atoi(c.DefaultQuery("width", "280"))
+ if width <= 0 {
+ width = 280
+ }
+ imageData, err := wechat.GenerateMiniProgramCode(scene, page, width)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "error": fmt.Sprintf("生成小程序码失败: %v", err),
+ })
+ return
+ }
+ c.Header("Content-Type", "image/png")
+ c.Data(http.StatusOK, "image/png", imageData)
+}
+
// base64 编码
func base64Encode(data []byte) string {
const base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
diff --git a/soul-api/internal/router/router.go b/soul-api/internal/router/router.go
index bde9183b..d7823a1d 100644
--- a/soul-api/internal/router/router.go
+++ b/soul-api/internal/router/router.go
@@ -215,6 +215,7 @@ func Setup(cfg *config.Config) *gin.Engine {
miniprogram.POST("/pay", handler.MiniprogramPay)
miniprogram.POST("/pay/notify", handler.MiniprogramPayNotify) // 微信支付回调,URL 需在商户平台配置
miniprogram.POST("/qrcode", handler.MiniprogramQrcode)
+ miniprogram.GET("/qrcode/image", handler.MiniprogramQrcodeImage)
miniprogram.GET("/book/all-chapters", handler.BookAllChapters)
miniprogram.GET("/book/chapter/:id", handler.BookChapterByID)
miniprogram.GET("/book/hot", handler.BookHot)
diff --git a/soul-api/internal/wechat/miniprogram.go b/soul-api/internal/wechat/miniprogram.go
index 9d7904d3..0d23fed4 100644
--- a/soul-api/internal/wechat/miniprogram.go
+++ b/soul-api/internal/wechat/miniprogram.go
@@ -210,10 +210,23 @@ func GenerateMiniProgramCode(scene, page string, width int) ([]byte, error) {
if page == "" {
page = "pages/index/index"
}
+ // 微信建议 scene 仅含英文字母、数字;& 和 = 可能导致异常,将 & 转为 _ 再传给微信
+ scene = strings.ReplaceAll(scene, "&", "_")
if len(scene) > 32 {
scene = scene[:32]
}
+ envVersion := "release"
+ if cfg != nil && cfg.WechatMiniProgramState != "" {
+ switch cfg.WechatMiniProgramState {
+ case "developer":
+ envVersion = "develop"
+ case "trial":
+ envVersion = "trial"
+ default:
+ envVersion = "release"
+ }
+ }
reqBody := map[string]interface{}{
"scene": scene,
"page": page,
@@ -221,9 +234,8 @@ func GenerateMiniProgramCode(scene, page string, width int) ([]byte, error) {
"auto_color": false,
"line_color": map[string]int{"r": 0, "g": 206, "b": 209},
"is_hyaline": false,
- "env_version": "trial", // 体验版,正式发布后改为 release
+ "env_version": envVersion,
}
-
jsonData, _ := json.Marshal(reqBody)
resp, err := http.Post(url, "application/json", bytes.NewReader(jsonData))
if err != nil {
@@ -232,22 +244,17 @@ func GenerateMiniProgramCode(scene, page string, width int) ([]byte, error) {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
-
- // 检查是否是 JSON 错误返回
- if resp.Header.Get("Content-Type") == "application/json" {
- var errResult struct {
- ErrCode int `json:"errcode"`
- ErrMsg string `json:"errmsg"`
- }
- if err := json.Unmarshal(body, &errResult); err == nil && errResult.ErrCode != 0 {
- return nil, fmt.Errorf("生成小程序码失败: %d - %s", errResult.ErrCode, errResult.ErrMsg)
- }
+ // 无论 Content-Type,先尝试按 JSON 解析:微信错误时返回小体积 JSON,否则会误报「图片数据异常(太小)」
+ var errResult struct {
+ ErrCode int `json:"errcode"`
+ ErrMsg string `json:"errmsg"`
+ }
+ if json.Unmarshal(body, &errResult) == nil && errResult.ErrCode != 0 {
+ return nil, fmt.Errorf("生成小程序码失败: %d - %s", errResult.ErrCode, errResult.ErrMsg)
}
-
if len(body) < 1000 {
- return nil, fmt.Errorf("返回的图片数据异常(太小)")
+ return nil, fmt.Errorf("返回的图片数据异常(太小),可能未发布对应版本或参数错误")
}
-
return body, nil
}
diff --git a/soul-api/scripts/sync-orders.sh b/soul-api/scripts/sync-orders.sh
new file mode 100644
index 00000000..194a64a5
--- /dev/null
+++ b/soul-api/scripts/sync-orders.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# 订单对账防漏单 - 宝塔定时任务用
+# 建议每 10 分钟执行一次
+
+URL="${SYNC_ORDERS_URL:-https://soul.quwanzhi.com/api/cron/sync-orders}"
+
+curl -s -X GET "$URL" \
+ -H "User-Agent: Baota-Cron/1.0" \
+ --connect-timeout 10 \
+ --max-time 30
+
+echo ""
diff --git a/soul-api/soul-api b/soul-api/soul-api
index 004d7bd0..918d299a 100644
Binary files a/soul-api/soul-api and b/soul-api/soul-api differ
diff --git a/tmp/build-errors.log b/tmp/build-errors.log
index b27e5257..f208b222 100644
--- a/tmp/build-errors.log
+++ b/tmp/build-errors.log
@@ -1 +1 @@
-exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
\ No newline at end of file
+exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
\ No newline at end of file