diff --git a/admin/src/views/withdrawals/index.vue b/admin/src/views/withdrawals/index.vue
index 05c12a2..7ec5a44 100644
--- a/admin/src/views/withdrawals/index.vue
+++ b/admin/src/views/withdrawals/index.vue
@@ -143,7 +143,7 @@
- {{ formatPaymentDetailsLong(withdrawalDetails.paymentMethod, withdrawalDetails.paymentDetails) }}
+ {{ formatPaymentDetailsLong(withdrawalDetails.paymentMethod, withdrawalDetails.paymentDetails) }}
{{ withdrawalDetails.user.nickname || '-' }}
-
- {{ withdrawalDetails.user.realName || '-' }}
+
+ {{ withdrawalDetails.user.uid || '-' }}
-
- {{ withdrawalDetails.user.phone || '-' }}
+
+ ¥{{ withdrawalDetails.user.balance || '0.00' }}
-
- {{ withdrawalDetails.user.whatsapp || '-' }}
-
-
- {{ withdrawalDetails.user.wechatId || '-' }}
-
-
- ¥{{ withdrawalDetails.user.balance }}
+
+ ₱{{ withdrawalDetails.user.balancePeso || '0.00' }}
@@ -617,15 +611,12 @@ function formatPaymentDetailsLong(method, details) {
return `支付宝账号: ${parsed.alipayAccount || parsed.account || '-'}`
case 'bank':
// 支持两种字段名: 小程序使用 bankCardNumber/cardholderName, 旧版使用 bankAccount/accountName
- const bankName = parsed.bankName || '银行'
+ const bankName = parsed.bankName || '-'
const account = parsed.bankCardNumber || parsed.bankAccount || '-'
const name = parsed.cardholderName || parsed.accountName || '-'
- const swiftCode = parsed.swiftCode
- let result = `${bankName} | 账号: ${account} | 户名: ${name}`
- if (swiftCode) {
- result += ` | SWIFT: ${swiftCode}`
- }
- return result
+ const swiftCode = parsed.swiftCode || '-'
+ // 使用换行符分隔,与前端标题对应
+ return `银行卡号:${account}\n持卡人:${name}\n开户行:${bankName}\nSwift号:${swiftCode}`
default:
return JSON.stringify(parsed)
}
@@ -723,6 +714,11 @@ onMounted(() => {
color: #f56c6c;
}
+ .payment-details-text {
+ white-space: pre-line;
+ line-height: 1.8;
+ }
+
.image-error {
display: flex;
flex-direction: column;
diff --git a/backend/src/scripts/addUserBalanceDual.js b/backend/src/scripts/addUserBalanceDual.js
new file mode 100644
index 0000000..767bf4c
--- /dev/null
+++ b/backend/src/scripts/addUserBalanceDual.js
@@ -0,0 +1,65 @@
+/**
+ * 为指定用户添加双币种可提现余额
+ * 用法: node src/scripts/addUserBalanceDual.js
+ * 示例: node src/scripts/addUserBalanceDual.js 704963 20 20
+ */
+
+require('dotenv').config();
+const { sequelize } = require('../config/database');
+const User = require('../models/User');
+
+async function addUserBalanceDual(uid, amountRmb, amountPeso) {
+ try {
+ console.log(`正在查找用户 UID: ${uid}...`);
+
+ const user = await User.findOne({ where: { uid } });
+
+ if (!user) {
+ console.error(`错误: 未找到 UID 为 ${uid} 的用户`);
+ process.exit(1);
+ }
+
+ console.log(`找到用户: ${user.nickname || user.realName || '未设置昵称'}`);
+ console.log(`当前人民币余额: ¥${user.balance}`);
+ console.log(`当前比索余额: ₱${user.balancePeso || 0}`);
+
+ const oldBalanceRmb = parseFloat(user.balance) || 0;
+ const oldBalancePeso = parseFloat(user.balancePeso) || 0;
+ const newBalanceRmb = oldBalanceRmb + parseFloat(amountRmb);
+ const newBalancePeso = oldBalancePeso + parseFloat(amountPeso);
+
+ await user.update({
+ balance: newBalanceRmb,
+ balancePeso: newBalancePeso
+ });
+
+ console.log(`\n✅ 余额更新成功!`);
+ console.log(`人民币余额:`);
+ console.log(` 原余额: ¥${oldBalanceRmb}`);
+ console.log(` 增加: +¥${amountRmb}`);
+ console.log(` 新余额: ¥${newBalanceRmb}`);
+ console.log(`比索余额:`);
+ console.log(` 原余额: ₱${oldBalancePeso}`);
+ console.log(` 增加: +₱${amountPeso}`);
+ console.log(` 新余额: ₱${newBalancePeso}`);
+
+ } catch (error) {
+ console.error('操作失败:', error.message);
+ process.exit(1);
+ } finally {
+ await sequelize.close();
+ }
+}
+
+// 从命令行参数获取
+const uid = process.argv[2];
+const amountRmb = parseFloat(process.argv[3]) || 0;
+const amountPeso = parseFloat(process.argv[4]) || 0;
+
+if (!uid) {
+ console.log('用法: node src/scripts/addUserBalanceDual.js ');
+ console.log('示例: node src/scripts/addUserBalanceDual.js 704963 20 20');
+ process.exit(1);
+}
+
+addUserBalanceDual(uid, amountRmb, amountPeso);
diff --git a/backend/src/services/adminWithdrawalService.js b/backend/src/services/adminWithdrawalService.js
index 0aa925d..2884370 100644
--- a/backend/src/services/adminWithdrawalService.js
+++ b/backend/src/services/adminWithdrawalService.js
@@ -268,7 +268,7 @@ const getWithdrawalDetails = async (withdrawalId) => {
{
model: User,
as: 'user',
- attributes: ['id', 'nickname', 'realName', 'phone', 'whatsapp', 'wechatId', 'balance'],
+ attributes: ['id', 'uid', 'nickname', 'balance', 'balancePeso'],
},
],
});
@@ -292,12 +292,10 @@ const getWithdrawalDetails = async (withdrawalId) => {
updatedAt: withdrawal.updatedAt,
user: withdrawal.user ? {
id: withdrawal.user.id,
+ uid: withdrawal.user.uid,
nickname: withdrawal.user.nickname,
- realName: withdrawal.user.realName,
- phone: withdrawal.user.phone,
- whatsapp: withdrawal.user.whatsapp,
- wechatId: withdrawal.user.wechatId,
- balance: parseFloat(withdrawal.user.balance).toFixed(2),
+ balance: parseFloat(withdrawal.user.balance || 0).toFixed(2),
+ balancePeso: parseFloat(withdrawal.user.balancePeso || 0).toFixed(2),
} : null,
};
};
diff --git a/backend/src/services/authService.js b/backend/src/services/authService.js
index ea20d39..b7b44af 100644
--- a/backend/src/services/authService.js
+++ b/backend/src/services/authService.js
@@ -24,6 +24,19 @@ const generateInvitationCode = () => {
return code;
};
+/**
+ * Generate random suffix for default nickname
+ * @returns {string} 4-character random suffix (lowercase letters and digits)
+ */
+const generateNicknameSuffix = () => {
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
+ let suffix = '';
+ for (let i = 0; i < 4; i++) {
+ suffix += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return suffix;
+};
+
/**
* Authenticate user with WeChat
* @param {string} code - WeChat authorization code
@@ -79,7 +92,7 @@ const wechatLogin = async (code, userInfo = {}, invitationCode = null, deviceInf
// Create new user
const userData = {
wechatOpenId: openId,
- nickname: userInfo.nickname || 'User',
+ nickname: userInfo.nickname || `User_${generateNicknameSuffix()}`,
avatar: userInfo.avatar || defaultAvatar,
invitationCode: await generateUniqueInvitationCode(),
};
diff --git a/miniprogram/src/locale/en.js b/miniprogram/src/locale/en.js
index c7df088..ab62366 100644
--- a/miniprogram/src/locale/en.js
+++ b/miniprogram/src/locale/en.js
@@ -98,6 +98,23 @@ export default {
general: 'Common Features',
other: 'Other Features'
},
+ profile: {
+ title: 'Profile',
+ nickname: 'Nickname',
+ nicknamePlaceholder: 'Tap to get WeChat nickname',
+ realName: 'Real Name',
+ realNamePlaceholder: 'Enter real name',
+ phone: 'Phone',
+ phonePlaceholder: 'Enter phone number',
+ whatsapp: 'WhatsApp',
+ whatsappPlaceholder: 'Enter WhatsApp number',
+ wechatId: 'WeChat ID',
+ wechatIdPlaceholder: 'Enter WeChat ID',
+ avatar: 'Avatar',
+ avatarTip: 'Tap to get WeChat avatar',
+ saveSuccess: 'Saved successfully',
+ saveFailed: 'Save failed'
+ },
language: {
chinese: '中文',
english: 'English',
@@ -244,6 +261,7 @@ If you have privacy questions, please contact us through the application.`
statusWaitingPeso: 'PHP Pending',
statusProcessing: 'Processing',
statusCompleted: 'Completed',
+ statusRejected: 'Rejected',
ruleTitle: 'Rules',
ruleContent: '1. Invite new users to register and complete first purchase\n2. Get cash reward for each successful invitation\n3. Rewards will be credited within 24 hours after payment\n4. Minimum withdrawal amount is 100 yuan',
qrcodeGenerated: 'QR Code Generated',
diff --git a/miniprogram/src/locale/es.js b/miniprogram/src/locale/es.js
index 587d627..f52e3fc 100644
--- a/miniprogram/src/locale/es.js
+++ b/miniprogram/src/locale/es.js
@@ -98,6 +98,23 @@ export default {
general: 'Funciones Comunes',
other: 'Otras Funciones'
},
+ profile: {
+ title: 'Perfil',
+ nickname: 'Apodo',
+ nicknamePlaceholder: 'Toca para obtener apodo de WeChat',
+ realName: 'Nombre Real',
+ realNamePlaceholder: 'Ingresa nombre real',
+ phone: 'Teléfono',
+ phonePlaceholder: 'Ingresa número de teléfono',
+ whatsapp: 'WhatsApp',
+ whatsappPlaceholder: 'Ingresa número de WhatsApp',
+ wechatId: 'ID de WeChat',
+ wechatIdPlaceholder: 'Ingresa ID de WeChat',
+ avatar: 'Avatar',
+ avatarTip: 'Toca para obtener avatar de WeChat',
+ saveSuccess: 'Guardado exitosamente',
+ saveFailed: 'Error al guardar'
+ },
language: {
chinese: '中文',
english: 'English',
@@ -244,6 +261,7 @@ Si tiene preguntas sobre privacidad, contáctenos a través de la aplicación.`
statusWaitingPeso: 'Peso Pendiente',
statusProcessing: 'Procesando',
statusCompleted: 'Completado',
+ statusRejected: 'Rechazado',
ruleTitle: 'Reglas',
ruleContent: '1. Invita nuevos usuarios a registrarse y completar la primera compra\n2. Recibe recompensa en efectivo por cada invitación exitosa\n3. Las recompensas se acreditarán dentro de 24 horas después del pago\n4. El monto mínimo de retiro es de 100 yuan',
qrcodeGenerated: 'Código QR Generado',
diff --git a/miniprogram/src/locale/zh.js b/miniprogram/src/locale/zh.js
index b1726ea..e76042a 100644
--- a/miniprogram/src/locale/zh.js
+++ b/miniprogram/src/locale/zh.js
@@ -101,7 +101,7 @@ export default {
profile: {
title: '个人资料',
nickname: '昵称',
- nicknamePlaceholder: '请输入昵称',
+ nicknamePlaceholder: '点击获取微信昵称',
realName: '真实姓名',
realNamePlaceholder: '请输入真实姓名',
phone: '手机号',
@@ -111,6 +111,7 @@ export default {
wechatId: '微信号',
wechatIdPlaceholder: '请输入微信号',
avatar: '头像',
+ avatarTip: '点击获取微信头像',
saveSuccess: '保存成功',
saveFailed: '保存失败'
},
@@ -260,6 +261,7 @@ export default {
statusWaitingPeso: '比索待提现',
statusProcessing: '提现中',
statusCompleted: '已提现',
+ statusRejected: '已拒绝',
ruleTitle: '活动规则',
ruleContent: '1. 邀请新用户注册并完成首次消费\n2. 每成功邀请一位用户可获得现金奖励\n3. 奖励将在用户完成支付后24小时内到账\n4. 提现金额满100元即可申请提现',
qrcodeGenerated: '二维码已生成',
diff --git a/miniprogram/src/modules/api/AppServer.js b/miniprogram/src/modules/api/AppServer.js
index 3558335..8afd2fd 100644
--- a/miniprogram/src/modules/api/AppServer.js
+++ b/miniprogram/src/modules/api/AppServer.js
@@ -644,6 +644,11 @@ AppServer.prototype.UploadImage = async function(filePath) {
authToken = uni.getStorageSync("token") || "";
}
+ // 确保token带有Bearer前缀
+ if (authToken && !authToken.startsWith('Bearer ')) {
+ authToken = 'Bearer ' + authToken;
+ }
+
return new Promise((resolve, reject) => {
uni.uploadFile({
url: url,
diff --git a/miniprogram/src/pages/me/invite-reward-page.vue b/miniprogram/src/pages/me/invite-reward-page.vue
index a482aa3..afd3689 100644
--- a/miniprogram/src/pages/me/invite-reward-page.vue
+++ b/miniprogram/src/pages/me/invite-reward-page.vue
@@ -350,6 +350,7 @@
invitationCode: ''
},
loading: false,
+ submitting: false, // 提现申请提交中状态
inviteRules: '' // 邀请规则说明(从后台配置获取)
}
},
@@ -804,6 +805,11 @@
})
},
async submitWithdraw() {
+ // 防止重复提交
+ if (this.submitting) {
+ return
+ }
+
// 验证微信/支付宝收款码
if (this.paymentMethod !== 'bank' && !this.qrcodeImage) {
uni.showToast({
@@ -824,6 +830,13 @@
}
}
+ // 设置提交中状态
+ this.submitting = true
+ uni.showLoading({
+ title: '提交中...',
+ mask: true
+ })
+
// 构建支付详情
let paymentDetails = {}
if (this.paymentMethod === 'bank') {
@@ -843,6 +856,8 @@
qrcodeUrl: uploadRes.data.url
}
} else {
+ uni.hideLoading()
+ this.submitting = false
uni.showToast({
title: '上传收款码失败',
icon: 'none'
@@ -851,6 +866,8 @@
}
} catch (error) {
console.error('上传收款码失败:', error)
+ uni.hideLoading()
+ this.submitting = false
uni.showToast({
title: '上传收款码失败',
icon: 'none'
@@ -868,6 +885,9 @@
paymentDetails: paymentDetails
})
+ uni.hideLoading()
+ this.submitting = false
+
// 后端返回格式: { code: 0, message: "", data: {...} }
if (res && res.code === 0) {
uni.showToast({
@@ -885,6 +905,8 @@
}
} catch (error) {
console.error('提现申请失败:', error)
+ uni.hideLoading()
+ this.submitting = false
uni.showToast({
title: '提现申请失败',
icon: 'none'
diff --git a/miniprogram/src/pages/me/me-page.vue b/miniprogram/src/pages/me/me-page.vue
index d70a2b7..fbfbac3 100644
--- a/miniprogram/src/pages/me/me-page.vue
+++ b/miniprogram/src/pages/me/me-page.vue
@@ -10,7 +10,7 @@
-
@@ -25,7 +25,7 @@
-
@@ -235,6 +235,7 @@
import { updateTabBarI18n } from '@/utils/tabbar-i18n.js'
import { requireAuth, getCurrentUser, isLoggedIn, logout, saveUserInfo } from '@/utils/auth.js'
import { AppServer } from '@/modules/api/AppServer.js'
+ import Config from '@/modules/Config.js'
export default {
data() {
@@ -258,6 +259,17 @@
computed: {
currentLanguage() {
return this.$i18n.locale
+ },
+ /**
+ * 获取默认头像(从后台配置)
+ */
+ defaultAvatar() {
+ const app = getApp()
+ if (app && app.globalData && app.globalData.config && app.globalData.config.default_avatar) {
+ return Config.getImageUrl(app.globalData.config.default_avatar)
+ }
+ // 如果没有配置,返回空字符串,让背景色显示
+ return ''
}
},
onLoad() {
@@ -274,6 +286,16 @@
}
},
methods: {
+ /**
+ * 获取头像URL,如果没有头像则使用默认头像
+ */
+ getAvatarUrl(avatar) {
+ if (avatar) {
+ return Config.getImageUrl(avatar)
+ }
+ return this.defaultAvatar
+ },
+
/**
* 检查登录状态
*/
diff --git a/miniprogram/src/pages/me/profile-edit-page.vue b/miniprogram/src/pages/me/profile-edit-page.vue
index 7aaafa0..0b7c078 100644
--- a/miniprogram/src/pages/me/profile-edit-page.vue
+++ b/miniprogram/src/pages/me/profile-edit-page.vue
@@ -21,15 +21,28 @@
-
-
-
-
-
- ✏️
+
+
+
+
+
+
+
+
+
+
+ ✏️
+
+
+ {{ $t('profile.avatarTip') || '点击获取微信头像' }}
@@ -37,8 +50,16 @@
{{ $t('profile.nickname') || '昵称' }}
+
+
+
+
+
+
@@ -81,6 +102,19 @@
}
},
+ computed: {
+ /**
+ * 获取默认头像(从后台配置)
+ */
+ defaultAvatar() {
+ const app = getApp()
+ if (app && app.globalData && app.globalData.config && app.globalData.config.default_avatar) {
+ return Config.getImageUrl(app.globalData.config.default_avatar)
+ }
+ return ''
+ }
+ },
+
/**
* 页面加载时执行
*/
@@ -151,6 +185,30 @@
})
},
+ /**
+ * 微信小程序获取微信头像回调
+ * @param {Object} e - 事件对象,包含微信头像临时路径
+ */
+ onChooseWechatAvatar(e) {
+ const { avatarUrl } = e.detail
+ if (avatarUrl) {
+ // 上传微信头像到服务器
+ this.uploadAvatar(avatarUrl)
+ }
+ },
+
+ /**
+ * 微信昵称输入框失焦回调
+ * 用于获取微信昵称
+ * @param {Object} e - 事件对象
+ */
+ onNicknameBlur(e) {
+ const nickname = e.detail.value
+ if (nickname) {
+ this.form.nickname = nickname
+ }
+ },
+
/**
* 上传头像到服务器
* @param {String} filePath - 本地图片路径
@@ -316,8 +374,9 @@
/* 头像区域 */
.avatar-section {
display: flex;
- justify-content: center;
- padding: 60rpx 0;
+ flex-direction: column;
+ align-items: center;
+ padding: 60rpx 0 40rpx;
background-color: #fff;
margin-bottom: 20rpx;
@@ -328,10 +387,27 @@
height: 160rpx;
}
+ /* 微信头像按钮 */
+ .avatar-button {
+ position: relative;
+ width: 160rpx;
+ height: 160rpx;
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: none;
+ border-radius: 50%;
+ overflow: visible;
+
+ &::after {
+ border: none;
+ }
+ }
+
/* 头像图片 */
.avatar {
- width: 100%;
- height: 100%;
+ width: 160rpx;
+ height: 160rpx;
border-radius: 50%;
background-color: #e0e0e0;
}
@@ -354,6 +430,13 @@
font-size: 20rpx;
}
}
+
+ /* 头像提示文字 */
+ .avatar-tip {
+ margin-top: 16rpx;
+ font-size: 24rpx;
+ color: #999;
+ }
}
/* 表单区域 */