From d17150154c12753ad2cca9a92da7eebdb305719a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Thu, 29 Jan 2026 11:58:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E9=9D=A2=E4=BC=98=E5=8C=96=EF=BC=9A?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=20+=20=E5=90=8E=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 小程序优化 1. 我的页面: - 头像点击获取微信头像(button open-type="chooseAvatar") - 昵称点击直接修改 - 去掉"创业伙伴"图标 - ID优先显示微信号 2. 设置页面: - 一键获取微信手机号 - 微信号/支付宝绑定优化 ## 后台管理优化 1. 内容管理: - 章节编辑增加"免费章节"开关 - 保存时自动去重标题(如#1.2重复) 2. 用户管理: - 修复绑定关系显示(优先查referral_bindings表) --- app/admin/content/page.tsx | 37 +++++- app/api/db/users/referrals/route.ts | 90 +++++++++---- miniprogram/pages/my/my.js | 155 +++++------------------ miniprogram/pages/my/my.wxml | 19 ++- miniprogram/pages/my/my.wxss | 26 ++++ miniprogram/pages/settings/settings.js | 28 +++- miniprogram/pages/settings/settings.wxml | 14 +- miniprogram/pages/settings/settings.wxss | 12 ++ 8 files changed, 199 insertions(+), 182 deletions(-) diff --git a/app/admin/content/page.tsx b/app/admin/content/page.tsx index d7a1f7a..220fcf2 100644 --- a/app/admin/content/page.tsx +++ b/app/admin/content/page.tsx @@ -41,6 +41,7 @@ interface EditingSection { isNew?: boolean partId?: string chapterId?: string + isFree?: boolean // 是否免费章节 } export default function ContentPage() { @@ -129,14 +130,21 @@ export default function ContentPage() { setIsSaving(true) try { + // 自动去掉内容中的重复标题(如# 1.2 xxx) + let content = editingSection.content || '' + // 匹配 # 数字.数字 开头的标题行并去掉 + const titlePattern = new RegExp(`^#\\s*${editingSection.id}\\s+.*$`, 'm') + content = content.replace(titlePattern, '').trim() + const res = await fetch('/api/db/book', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: editingSection.id, title: editingSection.title, - price: editingSection.price, - content: editingSection.content, + price: editingSection.isFree ? 0 : editingSection.price, + content: content, + isFree: editingSection.isFree || editingSection.price === 0, saveToFile: true, // 同时保存到文件系统 }) }) @@ -760,7 +768,7 @@ export default function ContentPage() { {editingSection && (
-
+
setEditingSection({ ...editingSection, price: Number(e.target.value) })} + value={editingSection.isFree ? 0 : editingSection.price} + onChange={(e) => setEditingSection({ ...editingSection, price: Number(e.target.value), isFree: Number(e.target.value) === 0 })} + disabled={editingSection.isFree} />
+
+ +
+ +
+
diff --git a/app/api/db/users/referrals/route.ts b/app/api/db/users/referrals/route.ts index 59364c6..7731441 100644 --- a/app/api/db/users/referrals/route.ts +++ b/app/api/db/users/referrals/route.ts @@ -1,6 +1,8 @@ /** * 用户绑定关系API * 获取指定用户的所有绑定用户列表 + * + * 优先从referral_bindings表查询,同时兼容users表的referred_by字段 */ import { NextResponse } from 'next/server' import { query } from '@/lib/db' @@ -31,55 +33,87 @@ export async function GET(request: Request) { code = userRows[0].referral_code } - if (!code) { - return NextResponse.json({ - success: true, - referrals: [], - stats: { - total: 0, - purchased: 0, - pendingEarnings: 0, - totalEarnings: 0 - } - }) + let referrals: any[] = [] + + // 1. 首先从referral_bindings表查询绑定关系 + try { + const bindingsReferrals = await query(` + SELECT + rb.id as binding_id, + rb.referee_id, + rb.status as binding_status, + rb.binding_date, + rb.expiry_date, + rb.commission_amount, + u.id, u.nickname, u.avatar, u.phone, u.open_id, + u.has_full_book, u.purchased_sections, + u.created_at, u.updated_at, + DATEDIFF(rb.expiry_date, NOW()) as days_remaining + FROM referral_bindings rb + JOIN users u ON rb.referee_id = u.id + WHERE rb.referrer_id = ? + ORDER BY rb.binding_date DESC + `, [userId]) as any[] + + if (bindingsReferrals.length > 0) { + referrals = bindingsReferrals + } + } catch (e) { + console.log('[Referrals] referral_bindings表查询失败,使用users表') } - // 查询通过该推广码绑定的所有用户 - const referrals = await query(` - SELECT - id, nickname, avatar, phone, open_id, - has_full_book, purchased_sections, - created_at, updated_at - FROM users - WHERE referred_by = ? - ORDER BY created_at DESC - `, [code]) as any[] + // 2. 如果referral_bindings表没有数据,再从users表查询 + if (referrals.length === 0 && code) { + referrals = await query(` + SELECT + id, nickname, avatar, phone, open_id, + has_full_book, purchased_sections, + created_at, updated_at, + NULL as binding_status, + NULL as binding_date, + NULL as expiry_date, + NULL as days_remaining, + NULL as commission_amount + FROM users + WHERE referred_by = ? + ORDER BY created_at DESC + `, [code]) as any[] + } // 统计信息 - const purchasedCount = referrals.filter(r => r.has_full_book || (r.purchased_sections && r.purchased_sections !== '[]')).length + const purchasedCount = referrals.filter(r => + r.has_full_book || + r.binding_status === 'converted' || + (r.purchased_sections && r.purchased_sections !== '[]') + ).length // 查询该用户的收益信息 const earningsRows = await query(` SELECT earnings, pending_earnings, withdrawn_earnings - FROM users WHERE ${userId ? 'id = ?' : 'referral_code = ?'} - `, [userId || code]) as any[] + FROM users WHERE id = ? + `, [userId]) as any[] const earnings = earningsRows[0] || { earnings: 0, pending_earnings: 0, withdrawn_earnings: 0 } // 格式化返回数据 const formattedReferrals = referrals.map(r => ({ - id: r.id, + id: r.referee_id || r.id, nickname: r.nickname || '微信用户', avatar: r.avatar, phone: r.phone ? r.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : null, hasOpenId: !!r.open_id, - hasPurchased: r.has_full_book || (r.purchased_sections && r.purchased_sections !== '[]'), + hasPurchased: r.has_full_book || r.binding_status === 'converted' || (r.purchased_sections && r.purchased_sections !== '[]'), hasFullBook: !!r.has_full_book, purchasedSections: typeof r.purchased_sections === 'string' ? JSON.parse(r.purchased_sections || '[]').length : 0, - createdAt: r.created_at, - status: r.has_full_book ? 'vip' : (r.purchased_sections && r.purchased_sections !== '[]' ? 'paid' : 'free') + createdAt: r.binding_date || r.created_at, + bindingStatus: r.binding_status || 'active', + daysRemaining: r.days_remaining, + commission: parseFloat(r.commission_amount) || 0, + status: r.binding_status === 'converted' ? 'converted' + : r.has_full_book ? 'vip' + : (r.purchased_sections && r.purchased_sections !== '[]' ? 'paid' : 'active') })) return NextResponse.json({ diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index b32adfd..a78eeaf 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -102,138 +102,41 @@ Page({ } }, - // 编辑用户资料(头像和昵称) - editProfile() { - wx.showActionSheet({ - itemList: ['获取微信头像', '获取微信昵称', '从相册选择头像', '手动输入昵称'], - success: (res) => { - if (res.tapIndex === 0) { - this.getWechatAvatar() - } else if (res.tapIndex === 1) { - this.getWechatNickname() - } else if (res.tapIndex === 2) { - this.chooseAvatar() - } else if (res.tapIndex === 3) { - this.editNickname() - } - } - }) + // 微信头像选择回调(button open-type="chooseAvatar") + async onChooseAvatar(e) { + const avatarUrl = e.detail.avatarUrl + if (!avatarUrl) return + + wx.showLoading({ title: '更新中...', mask: true }) + + try { + // 更新本地显示 + const userInfo = this.data.userInfo + userInfo.avatar = avatarUrl + this.setData({ userInfo }) + app.globalData.userInfo = userInfo + wx.setStorageSync('userInfo', userInfo) + + // 同步到服务器 + await app.request('/api/user/update', { + method: 'POST', + data: { userId: userInfo.id, avatar: avatarUrl } + }) + + wx.hideLoading() + wx.showToast({ title: '头像已更新', icon: 'success' }) + } catch (e) { + wx.hideLoading() + wx.showToast({ title: '更新失败', icon: 'none' }) + } }, - // 获取微信头像(原生能力) - getWechatAvatar() { - // 使用chooseAvatar API(微信原生头像选择) - wx.chooseAvatar({ - success: async (res) => { - const avatarUrl = res.avatarUrl - wx.showLoading({ title: '更新中...', mask: true }) - - try { - // 更新本地显示 - const userInfo = this.data.userInfo - userInfo.avatar = avatarUrl - this.setData({ userInfo }) - app.globalData.userInfo = userInfo - wx.setStorageSync('userInfo', userInfo) - - // 同步到服务器 - await app.request('/api/user/update', { - method: 'POST', - data: { userId: userInfo.id, avatar: avatarUrl } - }) - - wx.hideLoading() - wx.showToast({ title: '头像已更新', icon: 'success' }) - } catch (e) { - wx.hideLoading() - wx.showToast({ title: '更新失败', icon: 'none' }) - } - }, - fail: () => { - wx.showToast({ title: '取消选择', icon: 'none' }) - } - }) - }, - - // 获取微信昵称(原生能力) - getWechatNickname() { - // 引导用户在弹窗中输入微信昵称 - wx.showModal({ - title: '获取微信昵称', - content: '请在下方输入您的微信昵称', - editable: true, - placeholderText: '请输入微信昵称', - success: async (res) => { - if (res.confirm && res.content) { - const nickname = res.content.trim() - if (nickname.length < 1 || nickname.length > 20) { - wx.showToast({ title: '昵称1-20个字符', icon: 'none' }) - return - } - - // 更新本地 - const userInfo = this.data.userInfo - userInfo.nickname = nickname - this.setData({ userInfo }) - app.globalData.userInfo = userInfo - wx.setStorageSync('userInfo', userInfo) - - // 同步到服务器 - try { - await app.request('/api/user/update', { - method: 'POST', - data: { userId: userInfo.id, nickname } - }) - } catch (e) { - console.log('同步昵称到服务器失败', e) - } - - wx.showToast({ title: '昵称已更新', icon: 'success' }) - } - } - }) - }, - - // 从相册选择头像 - chooseAvatar() { - wx.chooseMedia({ - count: 1, - mediaType: ['image'], - sourceType: ['album', 'camera'], - success: async (res) => { - const tempFilePath = res.tempFiles[0].tempFilePath - wx.showLoading({ title: '上传中...', mask: true }) - - try { - // 更新本地显示 - const userInfo = this.data.userInfo - userInfo.avatar = tempFilePath - this.setData({ userInfo }) - app.globalData.userInfo = userInfo - wx.setStorageSync('userInfo', userInfo) - - // 同步到服务器 - await app.request('/api/user/update', { - method: 'POST', - data: { userId: userInfo.id, avatar: tempFilePath } - }) - - wx.hideLoading() - wx.showToast({ title: '头像已更新', icon: 'success' }) - } catch (e) { - wx.hideLoading() - wx.showToast({ title: '上传失败', icon: 'none' }) - } - } - }) - }, - - // 手动输入昵称 + // 点击昵称修改 editNickname() { wx.showModal({ title: '修改昵称', editable: true, - placeholderText: '请输入新昵称', + placeholderText: '请输入昵称', success: async (res) => { if (res.confirm && res.content) { const newNickname = res.content.trim() diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index 65de111..38452bd 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -27,8 +27,8 @@ - - + + - + diff --git a/miniprogram/pages/my/my.wxss b/miniprogram/pages/my/my.wxss index 27910f8..bee8c07 100644 --- a/miniprogram/pages/my/my.wxss +++ b/miniprogram/pages/my/my.wxss @@ -69,6 +69,20 @@ margin-bottom: 36rpx; } +/* 头像按钮样式 */ +.avatar-btn { + position: relative; + flex-shrink: 0; + padding: 0; + margin: 0; + background: transparent; + border: none; + line-height: normal; +} +.avatar-btn::after { + border: none; +} + .avatar-wrapper { position: relative; flex-shrink: 0; @@ -146,6 +160,18 @@ color: #ffffff; } +.edit-name-icon { + font-size: 24rpx; + color: rgba(255,255,255,0.4); + margin-left: 8rpx; +} + +.user-wechat { + font-size: 24rpx; + color: #00CED1; + margin-top: 4rpx; +} + .user-badge-small { display: inline-flex; align-items: center; diff --git a/miniprogram/pages/settings/settings.js b/miniprogram/pages/settings/settings.js index f8563ff..962797f 100644 --- a/miniprogram/pages/settings/settings.js +++ b/miniprogram/pages/settings/settings.js @@ -206,8 +206,10 @@ Page({ } }, - // 获取微信手机号(需要button组件配合) - async getPhoneNumber(e) { + // 一键获取微信手机号(button组件回调) + async onGetPhoneNumber(e) { + console.log('[Settings] 获取手机号回调:', e.detail) + if (e.detail.errMsg !== 'getPhoneNumber:ok') { wx.showToast({ title: '授权失败', icon: 'none' }) return @@ -217,30 +219,44 @@ Page({ // 需要将code发送到服务器解密获取手机号 const code = e.detail.code if (!code) { - wx.showToast({ title: '获取失败,请手动输入', icon: 'none' }) + // 如果没有code,弹出手动输入 + this.bindPhone() return } + wx.showLoading({ title: '获取中...', mask: true }) + // 调用服务器解密手机号 - const res = await app.request('/api/wechat/phone', { + const res = await app.request('/api/miniprogram/phone', { method: 'POST', data: { code } }) + wx.hideLoading() + if (res.success && res.phoneNumber) { wx.setStorageSync('user_phone', res.phoneNumber) this.setData({ phoneNumber: res.phoneNumber }) + // 更新用户信息 + if (app.globalData.userInfo) { + app.globalData.userInfo.phone = res.phoneNumber + wx.setStorageSync('userInfo', app.globalData.userInfo) + } + // 同步到服务器 this.syncProfileToServer() wx.showToast({ title: '手机号绑定成功', icon: 'success' }) } else { - wx.showToast({ title: '获取失败,请手动输入', icon: 'none' }) + // 获取失败,弹出手动输入 + this.bindPhone() } } catch (e) { + wx.hideLoading() console.log('[Settings] 获取手机号失败:', e) - wx.showToast({ title: '获取失败,请手动输入', icon: 'none' }) + // 获取失败,弹出手动输入 + this.bindPhone() } }, diff --git a/miniprogram/pages/settings/settings.wxml b/miniprogram/pages/settings/settings.wxml index c4e9acd..b857958 100644 --- a/miniprogram/pages/settings/settings.wxml +++ b/miniprogram/pages/settings/settings.wxml @@ -21,7 +21,7 @@ - + 📱 @@ -32,12 +32,14 @@ - 去绑定 + - + 💬 @@ -47,12 +49,12 @@ - 去绑定 + 去绑定 - + 💳 @@ -62,7 +64,7 @@ - 去绑定 + 去绑定 diff --git a/miniprogram/pages/settings/settings.wxss b/miniprogram/pages/settings/settings.wxss index 3add11a..9374f7b 100644 --- a/miniprogram/pages/settings/settings.wxss +++ b/miniprogram/pages/settings/settings.wxss @@ -32,6 +32,18 @@ .bind-check { color: #00CED1; font-size: 32rpx; } .bind-btn { color: #00CED1; font-size: 26rpx; } +/* 一键获取手机号按钮 */ +.get-phone-btn { + padding: 12rpx 24rpx; + background: rgba(0,206,209,0.2); + border: 2rpx solid rgba(0,206,209,0.3); + border-radius: 16rpx; + font-size: 24rpx; + color: #00CED1; + line-height: normal; +} +.get-phone-btn::after { border: none; } + /* 提现提示 */ .tip-banner { background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 24rpx; } .tip-text { font-size: 24rpx; color: #FFA500; line-height: 1.5; }