feat: sync upstream and keep local certification/withdraw

This commit is contained in:
zhiyun 2026-02-06 00:01:03 +08:00
parent 7473405242
commit 854e07d3f0
17 changed files with 909 additions and 48 deletions

21
app.js
View File

@ -488,7 +488,15 @@ App({
console.log('从query获取推荐码(ref):', referralCode) console.log('从query获取推荐码(ref):', referralCode)
} }
if (!referralCode && options.scene) { const queryScene = options && options.query ? options.query.scene : null
if (!referralCode && queryScene) {
referralCode = this.parseSceneReferralCode(queryScene)
if (referralCode) {
console.log('从query.scene获取推荐码:', referralCode)
}
}
if (!referralCode && typeof options.scene === 'string') {
referralCode = this.parseSceneReferralCode(options.scene) referralCode = this.parseSceneReferralCode(options.scene)
if (referralCode) { if (referralCode) {
console.log('从scene获取推荐码:', referralCode) console.log('从scene获取推荐码:', referralCode)
@ -521,7 +529,7 @@ App({
try { try {
const decoded = decodeURIComponent(scene) const decoded = decodeURIComponent(scene)
const match = decoded.match(/r=([A-Z0-9]+)/) const match = decoded.match(/r=([A-Za-z0-9]+)/)
return match ? match[1] : null return match ? match[1] : null
} catch (error) { } catch (error) {
console.error('解析scene失败:', error) console.error('解析scene失败:', error)
@ -534,10 +542,12 @@ App({
* 用户转发小程序时的默认配置 * 用户转发小程序时的默认配置
*/ */
onShareAppMessage() { onShareAppMessage() {
const referralCode = wx.getStorageSync('referralCode') || ''
const path = referralCode ? `/pages/index/index?referralCode=${referralCode}` : '/pages/index/index'
return { return {
title: '欢迎来到心伴俱乐部', title: '欢迎来到心伴俱乐部',
desc: '随时可聊 一直陪伴', desc: '随时可聊 一直陪伴',
path: '/pages/index/index', path,
imageUrl: '/images/icon-heart-new.png' imageUrl: '/images/icon-heart-new.png'
} }
}, },
@ -546,9 +556,12 @@ App({
* 全局分享到朋友圈配置 * 全局分享到朋友圈配置
*/ */
onShareTimeline() { onShareTimeline() {
const referralCode = wx.getStorageSync('referralCode') || ''
const query = referralCode ? `referralCode=${referralCode}` : ''
return { return {
title: '欢迎来到心伴俱乐部 - 随时可聊 一直陪伴', title: '欢迎来到心伴俱乐部 - 随时可聊 一直陪伴',
imageUrl: '/images/icon-heart-new.png' imageUrl: '/images/icon-heart-new.png',
query
} }
} }
}) })

View File

@ -27,6 +27,7 @@
"pages/edit-profile/edit-profile", "pages/edit-profile/edit-profile",
"pages/customer-management/customer-management", "pages/customer-management/customer-management",
"pages/withdraw/withdraw", "pages/withdraw/withdraw",
"pages/certification/certification",
"pages/commission/commission", "pages/commission/commission",
"pages/promote/promote", "pages/promote/promote",
"pages/backpack/backpack", "pages/backpack/backpack",

View File

@ -24,7 +24,8 @@ const ENV = {
API_BASE_URL: 'https://ai-c.maimanji.com/api', API_BASE_URL: 'https://ai-c.maimanji.com/api',
WS_URL: 'wss://ai-c.maimanji.com', WS_URL: 'wss://ai-c.maimanji.com',
DEBUG: false, DEBUG: false,
TEST_MODE: false // 关闭测试模式,使用真实微信支付(后端测试支付接口有数据库错误) TEST_MODE: false, // 关闭测试模式,使用真实微信支付(后端测试支付接口有数据库错误)
PAYMENT_CHANNEL: 'huifu' // 支付渠道huifu=汇付天下, wechat=官方微信支付
} }
} }

View File

@ -0,0 +1,3 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.33 13.33H12C9.79086 13.33 8 15.1209 8 17.33V52C8 54.2091 9.79086 56 12 56H52C54.2091 56 56 54.2091 56 52V17.33C56 15.1209 54.2091 13.33 52 13.33H42.67M21.33 13.33L24 8H40L42.67 13.33M21.33 13.33H42.67M32 45.33C37.8912 45.33 42.6667 40.5545 42.6667 34.6667C42.6667 28.7789 37.8912 24 32 24C26.1088 24 21.3333 28.7789 21.3333 34.6667C21.3333 40.5545 26.1088 45.33 32 45.33Z" stroke="#B06AB3" stroke-width="2.67" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 575 B

View File

@ -0,0 +1,151 @@
const { request, uploadFile, getBaseUrl } = require('../../utils_new/request');
Page({
data: {
statusBarHeight: 20,
navBarHeight: 44,
totalNavHeight: 64,
realName: '',
phone: '',
idCardNumber: '',
idCardFront: '',
submitting: false
},
onLoad() {
const sys = wx.getSystemInfoSync();
const menu = wx.getMenuButtonBoundingClientRect();
const statusBarHeight = sys.statusBarHeight || 20;
const navBarHeight = menu.height + (menu.top - statusBarHeight) * 2;
this.setData({
statusBarHeight,
navBarHeight,
totalNavHeight: statusBarHeight + navBarHeight
});
this.loadProfile();
},
async loadProfile() {
try {
const res = await request({ url: '/api/user/certification', method: 'GET' });
if (res.data && res.data.success && res.data.data) {
const { realName, phone, idCardFront, idCardNumber } = res.data.data;
this.setData({
realName: realName || '',
phone: phone || '',
idCardNumber: idCardNumber || '',
idCardFront: idCardFront || ''
});
}
} catch (err) {
console.error('Load certification info failed', err);
}
},
onBack() {
wx.navigateBack();
},
onInput(e) {
const field = e.currentTarget.dataset.field;
this.setData({
[field]: e.detail.value
});
},
chooseImage() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: async (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
this.setData({ idCardFront: tempFilePath });
try {
wx.showLoading({ title: '上传中...' });
const uploadRes = await uploadFile({
url: '/api/upload',
filePath: tempFilePath,
name: 'file',
formData: { type: 'certification' }
});
if (uploadRes && uploadRes.data) {
let data = uploadRes.data;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Parse upload response failed:', e);
}
}
if (data.code === 0 || data.success) {
const url = data.data.url;
// 如果返回的是相对路径,需要拼接域名
const fullUrl = url.startsWith('http') ? url : getBaseUrl() + url;
this.setData({ idCardFront: fullUrl });
} else {
wx.showToast({ title: '上传失败: ' + (data.message || data.error || '未知错误'), icon: 'none' });
}
}
} catch (err) {
console.error('Upload failed', err);
wx.showToast({ title: '上传失败,请重试', icon: 'none' });
} finally {
wx.hideLoading();
}
}
});
},
async submit() {
const { realName, phone, idCardNumber, idCardFront } = this.data;
if (!realName.trim()) {
wx.showToast({ title: '请输入真实姓名', icon: 'none' });
return;
}
if (!phone.trim()) {
wx.showToast({ title: '请输入手机号码', icon: 'none' });
return;
}
if (!idCardNumber.trim()) {
wx.showToast({ title: '请输入身份证号', icon: 'none' });
return;
}
if (!idCardFront) {
wx.showToast({ title: '请上传身份证正面', icon: 'none' });
return;
}
this.setData({ submitting: true });
try {
const res = await request({
url: '/api/user/certification',
method: 'POST',
data: {
realName,
phone,
idCardNumber,
idCardFront
}
});
if (res.data && res.data.success) {
wx.showToast({ title: '提交成功', icon: 'success' });
setTimeout(() => {
wx.navigateBack();
}, 1500);
} else {
throw new Error(res.data?.error || '提交失败');
}
} catch (err) {
wx.showToast({ title: err.message || '提交失败', icon: 'none' });
} finally {
this.setData({ submitting: false });
}
}
});

View File

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "实名认证",
"usingComponents": {
"app-icon": "/components/icon/icon"
}
}

View File

@ -0,0 +1,91 @@
<view class="page safe-bottom">
<!-- 顶部导航栏 -->
<view class="unified-header" style="height: {{totalNavHeight}}px; padding-top: {{statusBarHeight}}px;">
<view class="unified-header-left" style="height: {{navBarHeight}}px;" bindtap="onBack">
<image src="/images/icon-back.png" class="unified-back-icon" mode="aspectFit"></image>
<text class="unified-back-text">返回</text>
</view>
<view class="unified-header-title" style="line-height: {{navBarHeight}}px;">实名认证</view>
<view class="unified-header-right"></view>
</view>
<view class="wrap" style="padding-top: {{totalNavHeight}}px">
<view class="container">
<!-- 真实姓名 -->
<view class="form-group">
<text class="label">真实姓名</text>
<view class="input-wrapper">
<input
class="input"
placeholder="请输入您的真实姓名"
placeholder-class="placeholder"
value="{{realName}}"
bindinput="onInput"
data-field="realName"
/>
</view>
</view>
<!-- 手机号码 -->
<view class="form-group">
<text class="label">手机号码</text>
<view class="input-wrapper">
<input
class="input"
type="number"
maxlength="11"
placeholder="请输入您的手机号码"
placeholder-class="placeholder"
value="{{phone}}"
bindinput="onInput"
data-field="phone"
/>
</view>
</view>
<!-- 身份证号 -->
<view class="form-group">
<text class="label">身份证号</text>
<view class="input-wrapper">
<input
class="input"
type="idcard"
maxlength="18"
placeholder="请输入您的身份证号"
placeholder-class="placeholder"
value="{{idCardNumber}}"
bindinput="onInput"
data-field="idCardNumber"
/>
</view>
</view>
<!-- 身份证正面 -->
<view class="form-group">
<text class="label">身份证正面</text>
<view class="upload-box" bindtap="chooseImage">
<image wx:if="{{idCardFront}}" src="{{idCardFront}}" mode="aspectFill" class="preview-image" />
<view wx:else class="upload-placeholder">
<view class="icon-plus">
<image src="/images/certification/camera.svg" class="camera-icon" />
</view>
<text class="upload-text">点击上传身份证人像面</text>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-btn-wrapper">
<button class="submit-btn" bindtap="submit" disabled="{{submitting}}">
{{submitting ? '提交中...' : '提交认证'}}
</button>
</view>
<!-- 隐私声明 -->
<view class="privacy-note">
<app-icon name="shield-check" size="32" color="#00C950" />
<text class="privacy-text">我们严格保护您的隐私安全,实名信息仅用于提现身份核验,不会泄露给任何第三方。</text>
</view>
</view>
</view>
</view>

View File

@ -0,0 +1,170 @@
.page {
min-height: 100vh;
background: linear-gradient(180deg, #F8F5FF 0%, #FFFFFF 100%);
padding-bottom: env(safe-area-inset-bottom);
}
.unified-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0 32rpx;
background-color: transparent;
z-index: 100;
box-sizing: border-box;
}
.unified-header-left {
display: flex;
align-items: center;
position: absolute;
left: 32rpx;
bottom: 0;
z-index: 101;
}
.unified-header-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
}
.container {
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.form-group {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.label {
font-family: Arial;
font-weight: 700;
font-size: 36rpx;
color: #364153;
}
.input-wrapper {
background-color: #F5F7FA;
border-radius: 28rpx;
padding: 24rpx 32rpx;
display: flex;
align-items: center;
}
.input {
flex: 1;
font-size: 36rpx;
color: #333;
height: 48rpx;
line-height: 48rpx;
}
.placeholder {
color: rgba(10, 10, 10, 0.5);
font-size: 36rpx;
}
.upload-box {
background-color: #F5F7FA;
border: 2rpx dashed #D1D5DC;
border-radius: 28rpx;
height: 340rpx;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.icon-plus {
width: 128rpx;
height: 128rpx;
background-color: #FFFFFF;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.1);
}
.camera-icon {
width: 64rpx;
height: 64rpx;
}
.upload-text {
font-size: 32rpx;
color: #6A7282;
}
.preview-image {
width: 100%;
height: 100%;
}
.submit-btn-wrapper {
margin-top: 32rpx;
}
.submit-btn {
width: 100%;
height: 104rpx;
border-radius: 52rpx;
background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%);
color: #ffffff;
font-size: 36rpx;
font-weight: 900;
box-shadow: 0 20rpx 40rpx rgba(176, 106, 179, 0.3);
display: flex;
align-items: center;
justify-content: center;
margin-top: 64rpx;
letter-spacing: 2rpx;
transition: opacity 0.3s;
}
.submit-btn[disabled] {
opacity: 0.6;
box-shadow: none;
background: linear-gradient(135deg, #B06AB3 0%, #9B4D9E 100%);
}
.privacy-note {
background-color: #F9FAFB;
border-radius: 28rpx;
padding: 32rpx;
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 16rpx;
}
.privacy-text {
flex: 1;
font-size: 28rpx;
color: #99A1AF;
line-height: 1.5;
}

View File

@ -838,7 +838,7 @@ Page({
// 根据消息类型添加额外字段 // 根据消息类型添加额外字段
if (msg.message_type === 'image' && msg.image_url) { if (msg.message_type === 'image' && msg.image_url) {
baseMessage.imageUrl = msg.image_url baseMessage.imageUrl = imageUrl.getFullImageUrl(msg.image_url)
} else if (msg.message_type === 'voice' && msg.voice_url) { } else if (msg.message_type === 'voice' && msg.voice_url) {
baseMessage.audioUrl = msg.voice_url baseMessage.audioUrl = msg.voice_url
baseMessage.duration = msg.voice_duration baseMessage.duration = msg.voice_duration
@ -1826,13 +1826,15 @@ Page({
throw new Error('图片上传失败') throw new Error('图片上传失败')
} }
const imageUrl = uploadRes.data.url const uploadedUrl = uploadRes.data.url
console.log('[chat-detail] 图片上传成功:', imageUrl) // 转换图片URL为完整地址
const fullImageUrl = imageUrl.getFullImageUrl(uploadedUrl)
console.log('[chat-detail] 图片上传成功:', uploadedUrl, '完整地址:', fullImageUrl)
// 2. 更新本地消息,移除上传中状态 // 2. 更新本地消息,移除上传中状态
const messages = this.data.messages.map(msg => { const messages = this.data.messages.map(msg => {
if (msg.id === newId) { if (msg.id === newId) {
return { ...msg, imageUrl: imageUrl, uploading: false } return { ...msg, imageUrl: fullImageUrl, uploading: false }
} }
return msg return msg
}) })
@ -1843,7 +1845,7 @@ Page({
await api.chat.sendImage({ await api.chat.sendImage({
character_id: this.data.characterId, character_id: this.data.characterId,
conversation_id: this.data.conversationId, conversation_id: this.data.conversationId,
image_url: imageUrl image_url: fullImageUrl
}) })
console.log('[chat-detail] 图片消息已保存到数据库') console.log('[chat-detail] 图片消息已保存到数据库')
} catch (err) { } catch (err) {

View File

@ -116,7 +116,7 @@ Page({
isMe: msg.senderType === 'user', isMe: msg.senderType === 'user',
text: msg.type === 'text' ? msg.content : (msg.type === 'image' ? '[图片]' : (msg.type === 'voice' ? '[语音]' : msg.content)), text: msg.type === 'text' ? msg.content : (msg.type === 'image' ? '[图片]' : (msg.type === 'voice' ? '[语音]' : msg.content)),
type: msg.type || 'text', type: msg.type || 'text',
imageUrl: msg.type === 'image' ? msg.content : '', imageUrl: msg.type === 'image' ? imageUrl.getFullImageUrl(msg.content) : '',
audioUrl: msg.type === 'voice' ? msg.content : '', audioUrl: msg.type === 'voice' ? msg.content : '',
duration: msg.duration || 0, duration: msg.duration || 0,
time: util.formatTime(new Date(msg.createdAt), 'HH:mm'), time: util.formatTime(new Date(msg.createdAt), 'HH:mm'),
@ -451,7 +451,8 @@ Page({
try { try {
const uploadRes = await api.uploadFile(filePath, 'uploads') const uploadRes = await api.uploadFile(filePath, 'uploads')
if (uploadRes.success && uploadRes.data && uploadRes.data.url) { if (uploadRes.success && uploadRes.data && uploadRes.data.url) {
await this.sendMessage(uploadRes.data.url, 'image') const fullUrl = imageUrl.getFullImageUrl(uploadRes.data.url)
await this.sendMessage(fullUrl, 'image')
} else { } else {
throw new Error('Upload failed') throw new Error('Upload failed')
} }

View File

@ -7,14 +7,22 @@ Page({
totalNavHeight: 64, totalNavHeight: 64,
balance: '0.00', balance: '0.00',
amount: '', amount: '',
withdrawType: 'wechat', withdrawType: 'bank',
withdrawTypeText: '微信', withdrawTypeText: '银行卡',
cardHolder: '',
bankName: '',
cardNumber: '',
submitting: false, submitting: false,
records: [], records: [],
withdrawConfig: { withdrawConfig: {
minWithdrawAmount: 1 minWithdrawAmount: 1
}, },
showRulesModal: false showRulesModal: false,
isVerified: false,
showBankModal: false,
tempCardHolder: '',
tempBankName: '',
tempCardNumber: ''
}, },
onOpenRules() { onOpenRules() {
this.setData({ showRulesModal: true }); this.setData({ showRulesModal: true });
@ -38,10 +46,142 @@ Page({
navBarHeight, navBarHeight,
totalNavHeight: statusBarHeight + navBarHeight totalNavHeight: statusBarHeight + navBarHeight
}); });
this.load();
this.fetchConfig(); this.fetchConfig();
this.fetchRecords(); this.fetchRecords();
}, },
onShow() {
this.load();
this.checkCertification();
this.fetchBankCards(); // 尝试获取已绑定的银行卡
// Load cached bank info
const cachedInfo = wx.getStorageSync('last_withdraw_bank_info');
if (cachedInfo) {
this.setData({
cardHolder: cachedInfo.name || cachedInfo.cardHolder || '',
bankName: cachedInfo.bank || cachedInfo.bankName || '',
cardNumber: cachedInfo.account || cachedInfo.cardNumber || ''
});
}
},
// 获取用户已绑定的银行卡列表
async fetchBankCards() {
try {
const res = await request({ url: '/api/user/bank-cards', method: 'GET' });
if (res.data && res.data.success) {
const cards = res.data.data || [];
// 筛选出非默认卡或未被禁用的卡 (这里假设后端返回 active 状态)
// 如果后端支持多张卡,这里取第一张有效卡,或者根据 isDefault 排序
if (cards.length > 0) {
const defaultCard = cards.find(c => c.isDefault) || cards[0];
// 更新页面状态,视为已加载到缓存
this.setData({
cardHolder: defaultCard.cardHolder,
bankName: defaultCard.bankName,
// 注意:如果后端返回的是脱敏卡号,这里直接展示。提现时可能需要完整卡号
// 如果后端返回完整卡号,则正常使用。
// 假设后端返回完整卡号用于回显
cardNumber: defaultCard.cardNumber
});
// 同时更新本地缓存,保持一致
wx.setStorageSync('last_withdraw_bank_info', {
name: defaultCard.cardHolder,
bank: defaultCard.bankName,
account: defaultCard.cardNumber
});
} else {
// 如果后端返回空列表,说明用户没有绑定银行卡
// 此时应清除本地缓存的脏数据,确保 UI 显示“点击绑定”
this.setData({
cardHolder: '',
bankName: '',
cardNumber: ''
});
wx.removeStorageSync('last_withdraw_bank_info');
}
}
} catch (err) {
console.log('Fetch bank cards failed', err);
}
},
onCardHolder(e) { this.setData({ cardHolder: e.detail.value }); },
onBankName(e) { this.setData({ bankName: e.detail.value }); },
onCardNumber(e) { this.setData({ cardNumber: e.detail.value }); },
// Bank Info Modal
onEditBankCard() {
// Check certification status before binding card
if (!this.data.isVerified) {
wx.showModal({
title: '提示',
content: '绑定银行卡前请先完成实名认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
wx.navigateTo({ url: '/pages/certification/certification' });
}
}
});
return;
}
this.setData({
showBankModal: true,
tempCardHolder: this.data.cardHolder,
tempBankName: this.data.bankName,
tempCardNumber: this.data.cardNumber
});
},
onCloseBankModal() {
this.setData({ showBankModal: false });
},
onTempCardHolder(e) { this.setData({ tempCardHolder: e.detail.value }); },
onTempBankName(e) { this.setData({ tempBankName: e.detail.value }); },
onTempCardNumber(e) { this.setData({ tempCardNumber: e.detail.value }); },
confirmBankInfo() {
const { tempCardHolder, tempBankName, tempCardNumber } = this.data;
if (!tempCardHolder.trim()) {
wx.showToast({ title: '请输入持卡人姓名', icon: 'none' });
return;
}
if (!tempBankName.trim()) {
wx.showToast({ title: '请输入银行名称', icon: 'none' });
return;
}
if (!tempCardNumber.trim()) {
wx.showToast({ title: '请输入银行卡号', icon: 'none' });
return;
}
this.setData({
cardHolder: tempCardHolder,
bankName: tempBankName,
cardNumber: tempCardNumber,
showBankModal: false
});
// Auto save to cache when confirmed
wx.setStorageSync('last_withdraw_bank_info', {
name: tempCardHolder,
bank: tempBankName,
account: tempCardNumber
});
},
async checkCertification() {
try {
const res = await request({ url: '/api/user/certification', method: 'GET' });
if (res.data && res.data.success && res.data.data) {
this.setData({ isVerified: res.data.data.status === 'approved' });
}
} catch (err) {
console.error('Check certification status failed', err);
}
},
onBack() { onBack() {
wx.navigateBack({ delta: 1 }); wx.navigateBack({ delta: 1 });
}, },
@ -49,8 +189,13 @@ Page({
try { try {
const res = await request({ url: '/api/withdraw/config', method: 'GET' }); const res = await request({ url: '/api/withdraw/config', method: 'GET' });
if (res.data && res.data.code === 0 && res.data.data) { if (res.data && res.data.code === 0 && res.data.data) {
// 优先读取后端返回的配置
// 确保 minWithdrawAmount 被正确解析为数字
const minAmount = Number(res.data.data.minWithdrawAmount);
this.setData({ this.setData({
withdrawConfig: res.data.data withdrawConfig: {
minWithdrawAmount: !isNaN(minAmount) ? minAmount : 1
}
}); });
} }
} catch (err) { } catch (err) {
@ -75,6 +220,15 @@ Page({
'rejected': '已拒绝' 'rejected': '已拒绝'
}; };
item.statusText = statusMap[item.status] || '未知'; item.statusText = statusMap[item.status] || '未知';
// 类型映射
const typeMap = {
'wechat': '微信提现',
'alipay': '支付宝提现',
'bank': '银行卡提现'
};
item.typeText = typeMap[item.withdrawType] || '余额提现';
return item; return item;
}); });
this.setData({ records }); this.setData({ records });
@ -104,6 +258,22 @@ Page({
}, },
async submit() { async submit() {
if (this.data.submitting) return; if (this.data.submitting) return;
// Check certification status
if (!this.data.isVerified) {
wx.showModal({
title: '提示',
content: '提现前请先完成实名认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
wx.navigateTo({ url: '/pages/certification/certification' });
}
}
});
return;
}
const amountNum = Number(this.data.amount || 0); const amountNum = Number(this.data.amount || 0);
const minAmount = this.data.withdrawConfig.minWithdrawAmount || 0; const minAmount = this.data.withdrawConfig.minWithdrawAmount || 0;
@ -117,6 +287,20 @@ Page({
return; return;
} }
let accountInfo = {};
if (this.data.withdrawType === 'bank') {
const { cardHolder, bankName, cardNumber } = this.data;
if (!cardHolder.trim() || !bankName.trim() || !cardNumber.trim()) {
this.onEditBankCard(); // Open modal if info missing
return;
}
accountInfo = {
name: cardHolder,
bank: bankName,
account: cardNumber
};
}
this.setData({ submitting: true }); this.setData({ submitting: true });
try { try {
const res = await request({ const res = await request({
@ -126,11 +310,19 @@ Page({
action: 'withdraw', action: 'withdraw',
amount: amountNum, amount: amountNum,
withdrawType: this.data.withdrawType, withdrawType: this.data.withdrawType,
accountInfo: {} accountInfo: accountInfo,
// 如果后续支持选择已绑定银行卡,可在此传递 bankCardId
// bankCardId: this.data.selectedCardId
} }
}); });
const body = res.data || {}; const body = res.data || {};
if (!body.success) throw new Error(body.error || '提交失败'); if (!body.success) throw new Error(body.error || '提交失败');
// Cache successful bank info
if (this.data.withdrawType === 'bank') {
wx.setStorageSync('last_withdraw_bank_info', accountInfo);
}
wx.showToast({ title: '提交申请成功', icon: 'success' }); wx.showToast({ title: '提交申请成功', icon: 'success' });
this.setData({ amount: '' }); this.setData({ amount: '' });
this.load(); this.load();

View File

@ -39,17 +39,33 @@
<view class="field-group"> <view class="field-group">
<text class="f-label">提现方式</text> <text class="f-label">提现方式</text>
<view class="select-wrapper"> <!-- 移除原有的固定银行卡选择栏,直接展示已录入的卡片或录入提示 -->
<view class="select-left"> <!-- 如果没有录入银行卡信息,显示点击录入的样式 -->
<view class="wechat-icon-box"> <view class="bank-card-empty" wx:if="{{!cardNumber}}" bindtap="onEditBankCard" style="margin-top: 0;">
<app-icon name="wechat" size="40" color="#FFFFFF" /> <view class="bank-icon-box" style="margin-right: 24rpx; background: #E5E7EB;">
</view> <app-icon name="credit-card" size="40" color="#9CA3AF" />
<text class="select-text">微信零钱</text> </view>
</view> <text style="color: #6B7280; font-weight: 600;">银行卡</text>
<app-icon name="check-circle-fill" size="40" color="#B06AB3" /> <view style="flex: 1;"></view>
<text style="color: #9CA3AF; font-size: 26rpx;">点击绑定</text>
<app-icon name="chevron-right" size="32" color="#9CA3AF" />
</view>
<!-- 如果已录入银行卡信息,显示银行卡详情卡片 -->
<view class="bank-card-preview" wx:else>
<view class="bank-card-icon">
<app-icon name="credit-card" size="32" color="#FFFFFF" />
</view>
<view class="bank-card-details">
<text class="bank-name">{{bankName}}</text>
<text class="bank-number">{{cardNumber}}</text>
</view>
<view class="bank-card-edit" bindtap="onEditBankCard">
<text>修改</text>
</view>
</view> </view>
</view> </view>
<button class="btn-reset submit-btn" bindtap="submit" disabled="{{submitting}}"> <button class="btn-reset submit-btn" bindtap="submit" disabled="{{submitting}}">
{{submitting ? '处理中...' : '立即提现'}} {{submitting ? '处理中...' : '立即提现'}}
</button> </button>
@ -60,6 +76,38 @@
</view> </view>
</view> </view>
<!-- 银行卡信息输入弹窗 -->
<view class="modal-mask" wx:if="{{showBankModal}}" bindtap="onCloseBankModal"></view>
<view class="modal-container bank-modal" wx:if="{{showBankModal}}">
<view class="modal-header">
<text class="modal-title">填写银行卡信息</text>
<view class="modal-close" bindtap="onCloseBankModal">
<app-icon name="close" size="40" color="#9CA3AF" />
</view>
</view>
<view class="bank-form">
<view class="field-group">
<text class="f-label">持卡人姓名</text>
<view class="input-wrapper small-input">
<input class="input-text" placeholder="请输入持卡人姓名" placeholder-class="placeholder" value="{{tempCardHolder}}" bindinput="onTempCardHolder" />
</view>
</view>
<view class="field-group">
<text class="f-label">银行名称</text>
<view class="input-wrapper small-input">
<input class="input-text" placeholder="例如:招商银行" placeholder-class="placeholder" value="{{tempBankName}}" bindinput="onTempBankName" />
</view>
</view>
<view class="field-group">
<text class="f-label">银行卡号</text>
<view class="input-wrapper small-input">
<input class="input-text" type="number" placeholder="请输入银行卡号" placeholder-class="placeholder" value="{{tempCardNumber}}" bindinput="onTempCardNumber" />
</view>
</view>
<button class="btn-reset confirm-btn" bindtap="confirmBankInfo">确认保存</button>
</view>
</view>
<!-- Records Section --> <!-- Records Section -->
<view class="records-section" wx:if="{{records.length > 0}}"> <view class="records-section" wx:if="{{records.length > 0}}">
<view class="section-header"> <view class="section-header">
@ -68,7 +116,7 @@
<view class="record-list"> <view class="record-list">
<view class="record-item" wx:for="{{records}}" wx:key="id"> <view class="record-item" wx:for="{{records}}" wx:key="id">
<view class="record-info"> <view class="record-info">
<view class="record-type">微信提现</view> <view class="record-type">{{item.typeText}}</view>
<view class="record-time">{{item.timeStr}}</view> <view class="record-time">{{item.timeStr}}</view>
</view> </view>
<view class="record-amount"> <view class="record-amount">
@ -102,10 +150,10 @@
<text class="rule-title">提现规则说明</text> <text class="rule-title">提现规则说明</text>
</view> </view>
<view class="rule-content"> <view class="rule-content">
<view class="rule-item">1. <text class="rule-label">最低提现金额:</text>单笔提现金额不低于 ¥100.00。</view> <view class="rule-item">1. <text class="rule-label">最低提现金额:</text>单笔提现金额不低于 ¥{{withdrawConfig.minWithdrawAmount || 100}}。</view>
<view class="rule-item">2. <text class="rule-label">每日提现次数:</text>每日最多可发起 3 次提现申请。</view> <view class="rule-item">2. <text class="rule-label">每日提现次数:</text>每日最多可发起 3 次提现申请。</view>
<view class="rule-item">3. <text class="rule-label">可提现余额:</text>仅限“已结算”状态的收入可提现,待结算金额暂不支持。</view> <view class="rule-item">3. <text class="rule-label">可提现余额:</text>仅限“已结算”状态的收入可提现,待结算金额暂不支持。</view>
<view class="rule-item">4. <text class="rule-label">提现服务费:</text>每笔提现收取 1% 服务费(最低 ¥1.00),由第三方支付平台收取。</view> <view class="rule-item">4. <text class="rule-label">提现服务费:</text>目前平台提现免收服务费,全额到账。</view>
<view class="rule-item">5. <text class="rule-label">审核时间:</text>提现申请将在 24 小时 内完成审核并安排打款。</view> <view class="rule-item">5. <text class="rule-label">审核时间:</text>提现申请将在 24 小时 内完成审核并安排打款。</view>
<view class="rule-item">6. <text class="rule-label">到账时间:</text>审核通过后,预计 24 小时内到账,具体以银行或微信到账通知为准。</view> <view class="rule-item">6. <text class="rule-label">到账时间:</text>审核通过后,预计 24 小时内到账,具体以银行或微信到账通知为准。</view>
<view class="rule-item">7. <text class="rule-label">账户要求:</text>提现账户需为 本人实名认证 的微信或银行卡。</view> <view class="rule-item">7. <text class="rule-label">账户要求:</text>提现账户需为 本人实名认证 的微信或银行卡。</view>
@ -125,7 +173,7 @@
</view> </view>
<view class="qa-item"> <view class="qa-item">
<view class="qa-q">· 提现服务费如何计算?</view> <view class="qa-q">· 提现服务费如何计算?</view>
<view class="qa-a">每笔提现收取 5% 作为服务费,在提现金额中自动扣除。</view> <view class="qa-a">目前平台处于推广期,提现免收服务费。</view>
</view> </view>
<view class="qa-item"> <view class="qa-item">
<view class="qa-q">· 提现未到账怎么办?</view> <view class="qa-q">· 提现未到账怎么办?</view>

View File

@ -97,6 +97,118 @@
transition: all 0.3s; transition: all 0.3s;
} }
/* 银行卡展示区域 */
.bank-info-display {
margin-top: -24rpx;
margin-bottom: 24rpx;
padding-top: 32rpx;
border-top: 2rpx dashed #F3F4F6;
}
.bank-card-preview {
display: flex;
align-items: center;
background: #F9FAFB;
border-radius: 24rpx;
padding: 24rpx;
border: 2rpx solid #F3F4F6;
}
.bank-card-icon {
width: 64rpx;
height: 64rpx;
background: #3B82F6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.bank-card-details {
flex: 1;
display: flex;
flex-direction: column;
}
.bank-name {
font-size: 30rpx;
font-weight: 700;
color: #111827;
margin-bottom: 4rpx;
}
.bank-number {
font-size: 26rpx;
color: #6B7280;
}
.bank-card-edit {
padding: 12rpx 24rpx;
background: #FFFFFF;
border-radius: 999rpx;
border: 2rpx solid #E5E7EB;
}
.bank-card-edit text {
font-size: 24rpx;
color: #6B7280;
font-weight: 600;
}
.bank-card-empty {
height: 112rpx; /* Match input-wrapper height */
display: flex;
align-items: center;
padding: 0 32rpx;
background: #F9FAFB;
border: 2rpx dashed #D1D5DB;
border-radius: 32rpx;
}
.bank-card-empty text {
font-size: 28rpx;
color: #6B7280;
font-weight: 600;
}
/* 银行卡弹窗样式 */
.bank-modal {
z-index: 1002;
}
.bank-form {
padding-bottom: 40rpx;
}
.confirm-btn {
margin-top: 48rpx;
width: 100%;
height: 96rpx;
border-radius: 48rpx;
background: #3B82F6;
color: #ffffff;
font-size: 34rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.input-wrapper.small-input {
height: 96rpx;
background: #F9FAFB;
border-radius: 24rpx;
}
.input-text {
flex: 1;
height: 100%;
font-size: 32rpx;
color: #111827;
font-weight: 500;
}
.input-wrapper:focus-within { .input-wrapper:focus-within {
background: #FFFFFF; background: #FFFFFF;
border-color: #B06AB3; border-color: #B06AB3;
@ -156,6 +268,16 @@
justify-content: center; justify-content: center;
} }
.bank-icon-box {
width: 64rpx;
height: 64rpx;
background: #3B82F6; /* Bank Blue */
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.select-text { .select-text {
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;

View File

@ -153,9 +153,23 @@ function saveUserInfo(user, token, expiresAt = null) {
// 登录成功后,检查并绑定推荐码 // 登录成功后,检查并绑定推荐码
if (user && user.id && token) { if (user && user.id && token) {
checkAndBindReferralCode(user.id, token) checkAndBindReferralCode(user.id, token)
syncReferralCode()
} }
} }
function syncReferralCode() {
const existing = wx.getStorageSync('referralCode')
if (existing) return Promise.resolve(existing)
return api.commission.getStats().then(res => {
const code = res && res.data ? (res.data.referralCode || '') : ''
if (res && res.success && code) {
wx.setStorageSync('referralCode', code)
}
return code
}).catch(() => '')
}
/** /**
* 检查并绑定推荐码佣金系统 * 检查并绑定推荐码佣金系统
* 在用户登录成功后自动调用 * 在用户登录成功后自动调用

View File

@ -38,14 +38,16 @@ function getFullImageUrl(url, defaultImage = '') {
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) { if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('data:')) {
try { try {
const parsed = new URL(url) const parsed = new URL(url)
if (parsed.pathname.startsWith('/uploads/')) { // 移除对 /uploads/ 路径的特殊处理,直接使用原始路径
parsed.pathname = `/api/uploads/${parsed.pathname.slice('/uploads/'.length)}` // if (parsed.pathname.startsWith('/uploads/')) {
return parsed.toString() // parsed.pathname = `/api/uploads/${parsed.pathname.slice('/uploads/'.length)}`
} // return parsed.toString()
if (/^\/(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(parsed.pathname)) { // }
parsed.pathname = `/api/uploads${parsed.pathname}` // 移除统一加 /api/uploads 前缀的逻辑,直接使用静态资源路径
return parsed.toString() // if (/^\/(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(parsed.pathname)) {
} // parsed.pathname = `/api/uploads${parsed.pathname}`
// return parsed.toString()
// }
return url return url
} catch (_) { } catch (_) {
return url return url
@ -58,13 +60,16 @@ function getFullImageUrl(url, defaultImage = '') {
} }
let processedUrl = url let processedUrl = url
if (processedUrl.startsWith('/uploads/')) { // 移除对 /uploads/ 路径的特殊处理,直接使用原始路径
processedUrl = `/api/uploads/${processedUrl.slice('/uploads/'.length)}` // if (processedUrl.startsWith('/uploads/')) {
} else if (processedUrl.startsWith('uploads/')) { // processedUrl = `/api/uploads/${processedUrl.slice('/uploads/'.length)}`
processedUrl = `/api/uploads/${processedUrl.slice('uploads/'.length)}` // } else if (processedUrl.startsWith('uploads/')) {
} else if (/^(\/)?(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(processedUrl)) { // processedUrl = `/api/uploads/${processedUrl.slice('uploads/'.length)}`
processedUrl = processedUrl.startsWith('/') ? `/api/uploads${processedUrl}` : `/api/uploads/${processedUrl}` // } else
} // 移除统一加 /api/uploads 前缀的逻辑
// if (/^(\/)?(avatars|characters|audio|documents|assets|interest-partners|exchange|products|temp)\//.test(processedUrl)) {
// processedUrl = processedUrl.startsWith('/') ? `/api/uploads${processedUrl}` : `/api/uploads/${processedUrl}`
// }
// 其他相对路径,拼接服务器地址 // 其他相对路径,拼接服务器地址
// 确保URL以/开头 // 确保URL以/开头

View File

@ -1,10 +1,22 @@
const { request } = require('./request'); const { request } = require('./request');
const config = require('../config/index');
const getPaymentChannel = () => {
return config.PAYMENT_CHANNEL || 'wechat';
};
const createVipOrder = async ({ planId, duration }) => { const createVipOrder = async ({ planId, duration }) => {
const channel = getPaymentChannel();
const res = await request({ const res = await request({
url: '/api/payment/vip', url: '/api/payment/vip',
method: 'POST', method: 'POST',
data: { planId, duration, paymentMethod: 'wechat' } data: {
planId,
duration,
paymentMethod: channel,
paymentChannel: channel,
platform: 'miniapp' // [关键] 必须传这个,后端据此调用汇付的小程序支付接口
}
}); });
const body = res.data || {}; const body = res.data || {};
if (!body.success) throw new Error(body.error || '创建VIP订单失败'); if (!body.success) throw new Error(body.error || '创建VIP订单失败');
@ -12,10 +24,16 @@ const createVipOrder = async ({ planId, duration }) => {
}; };
const createProductOrder = async ({ productId, referralCode }) => { const createProductOrder = async ({ productId, referralCode }) => {
const channel = getPaymentChannel();
const res = await request({ const res = await request({
url: `/api/products/${productId}/purchase`, url: `/api/products/${productId}/purchase`,
method: 'POST', method: 'POST',
data: { payment_method: 'wechat', ...(referralCode ? { referral_code: referralCode } : {}) } data: {
payment_method: channel,
payment_channel: channel,
platform: 'miniapp', // [关键] 必须传这个,后端据此调用汇付的小程序支付接口
...(referralCode ? { referral_code: referralCode } : {})
}
}); });
const body = res.data || {}; const body = res.data || {};
if (!body.success) throw new Error(body.error || '创建订单失败'); if (!body.success) throw new Error(body.error || '创建订单失败');

View File

@ -51,7 +51,30 @@ const request = ({ url, method = 'GET', data, header = {} }) => {
}); });
}; };
const uploadFile = ({ url, filePath, name = 'file', formData = {}, header = {} }) => {
return new Promise((resolve, reject) => {
const baseUrl = getBaseUrl();
wx.uploadFile({
url: `${baseUrl}${url}`,
filePath,
name,
formData,
header: {
...(getToken() ? { Authorization: `Bearer ${getToken()}` } : {}),
...header
},
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
};
module.exports = { module.exports = {
request, request,
uploadFile,
getBaseUrl getBaseUrl
}; };