diff --git a/admin/src/views/config/index.vue b/admin/src/views/config/index.vue
index 0af8416..8ae07ac 100644
--- a/admin/src/views/config/index.vue
+++ b/admin/src/views/config/index.vue
@@ -80,6 +80,40 @@
+
+
+
+
+
+
+
+
建议尺寸: 200x200px
+
支持格式: PNG, JPG
+
文件大小: 最大5MB
+
新用户注册时将使用此头像
+
+ 清除头像
+
+
+
+
+
@@ -359,7 +393,8 @@ const originalConfigs = reactive({
const appearanceConfig = ref({
app_logo: '',
- about_us_image: ''
+ about_us_image: '',
+ default_avatar: ''
})
const generalConfig = ref({
@@ -553,6 +588,13 @@ const saveAppearanceConfig = async () => {
description: 'About us section image URL'
})
+ await api.put('/api/v1/admin/config/default_avatar', {
+ value: appearanceConfig.value.default_avatar,
+ type: 'image',
+ category: 'appearance',
+ description: '用户默认头像'
+ })
+
// Update original configs
originalConfigs.appearance = { ...appearanceConfig.value }
@@ -803,6 +845,30 @@ const handleAboutSuccess = (response) => {
}
}
+// Handle default avatar upload success
+const handleDefaultAvatarSuccess = (response) => {
+ if (response.code === 0) {
+ appearanceConfig.value.default_avatar = response.data.url
+ ElMessage.success('默认头像上传成功')
+ } else {
+ ElMessage.error(response.message || '默认头像上传失败')
+ }
+}
+
+// Clear default avatar
+const clearDefaultAvatar = async () => {
+ try {
+ await ElMessageBox.confirm('确定要清除默认头像吗?', '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning'
+ })
+ appearanceConfig.value.default_avatar = ''
+ } catch {
+ // User cancelled
+ }
+}
+
// Handle QR code upload success
const handleQrSuccess = (response) => {
if (response.code === 0) {
@@ -996,6 +1062,42 @@ onMounted(() => {
display: block;
}
+.avatar-uploader {
+ border: 2px dashed #d9d9d9;
+ border-radius: 8px;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s;
+ background-color: #fafafa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 200px;
+ height: 200px;
+}
+
+.avatar-uploader:hover {
+ border-color: #409eff;
+ background-color: #f0f7ff;
+}
+
+.avatar-uploader :deep(.el-upload) {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.avatar-preview {
+ width: 200px;
+ height: 200px;
+ object-fit: cover;
+ display: block;
+ border-radius: 50%;
+}
+
.upload-info {
display: flex;
flex-direction: column;
diff --git a/backend/run-migration.js b/backend/run-migration.js
new file mode 100644
index 0000000..2a83b39
--- /dev/null
+++ b/backend/run-migration.js
@@ -0,0 +1,122 @@
+/**
+ * Safe migration script - checks if columns exist before renaming
+ */
+const { sequelize } = require('./src/config/database');
+
+async function runMigration() {
+ try {
+ console.log('Connecting to database...');
+ await sequelize.authenticate();
+ console.log('Connected successfully!\n');
+
+ // Check and rename hot_services.name_pt to name_es
+ console.log('Checking hot_services table...');
+ const [hotServiceCols] = await sequelize.query(
+ "SHOW COLUMNS FROM `hot_services` LIKE 'name_pt'"
+ );
+
+ if (hotServiceCols.length > 0) {
+ console.log('Renaming hot_services.name_pt to name_es...');
+ await sequelize.query(
+ "ALTER TABLE `hot_services` CHANGE COLUMN `name_pt` `name_es` VARCHAR(100) NOT NULL"
+ );
+ console.log('✓ hot_services.name_pt renamed to name_es');
+ } else {
+ console.log('✓ hot_services.name_es already exists, skipping...');
+ }
+
+ // Check and rename category.name_pt to name_es
+ console.log('\nChecking category table...');
+ const [categoryCols] = await sequelize.query(
+ "SHOW COLUMNS FROM `category` LIKE 'name_pt'"
+ );
+
+ if (categoryCols.length > 0) {
+ console.log('Renaming category.name_pt to name_es...');
+ await sequelize.query(
+ "ALTER TABLE `category` CHANGE COLUMN `name_pt` `name_es` VARCHAR(100) NOT NULL"
+ );
+ console.log('✓ category.name_pt renamed to name_es');
+ } else {
+ console.log('✓ category.name_es already exists, skipping...');
+ }
+
+ // Check and rename service.title_pt and description_pt
+ console.log('\nChecking service table...');
+ const [serviceTitleCols] = await sequelize.query(
+ "SHOW COLUMNS FROM `service` LIKE 'title_pt'"
+ );
+
+ if (serviceTitleCols.length > 0) {
+ console.log('Renaming service.title_pt to title_es...');
+ await sequelize.query(
+ "ALTER TABLE `service` CHANGE COLUMN `title_pt` `title_es` VARCHAR(200) NOT NULL"
+ );
+ console.log('✓ service.title_pt renamed to title_es');
+ } else {
+ console.log('✓ service.title_es already exists, skipping...');
+ }
+
+ const [serviceDescCols] = await sequelize.query(
+ "SHOW COLUMNS FROM `service` LIKE 'description_pt'"
+ );
+
+ if (serviceDescCols.length > 0) {
+ console.log('Renaming service.description_pt to description_es...');
+ await sequelize.query(
+ "ALTER TABLE `service` CHANGE COLUMN `description_pt` `description_es` TEXT"
+ );
+ console.log('✓ service.description_pt renamed to description_es');
+ } else {
+ console.log('✓ service.description_es already exists, skipping...');
+ }
+
+ // Check and rename notification fields
+ console.log('\nChecking notification table...');
+ const [notifTitleCols] = await sequelize.query(
+ "SHOW COLUMNS FROM `notification` LIKE 'title_pt'"
+ );
+
+ if (notifTitleCols.length > 0) {
+ console.log('Renaming notification.title_pt to title_es...');
+ await sequelize.query(
+ "ALTER TABLE `notification` CHANGE COLUMN `title_pt` `title_es` VARCHAR(200) NOT NULL"
+ );
+ console.log('✓ notification.title_pt renamed to title_es');
+ } else {
+ console.log('✓ notification.title_es already exists, skipping...');
+ }
+
+ const [notifContentCols] = await sequelize.query(
+ "SHOW COLUMNS FROM `notification` LIKE 'content_pt'"
+ );
+
+ if (notifContentCols.length > 0) {
+ console.log('Renaming notification.content_pt to content_es...');
+ await sequelize.query(
+ "ALTER TABLE `notification` CHANGE COLUMN `content_pt` `content_es` TEXT"
+ );
+ console.log('✓ notification.content_pt renamed to content_es');
+ } else {
+ console.log('✓ notification.content_es already exists, skipping...');
+ }
+
+ // Update user language preferences
+ console.log('\nUpdating user language preferences...');
+ const [result] = await sequelize.query(
+ "UPDATE `user` SET language = 'es' WHERE language = 'pt'"
+ );
+ console.log(`✓ Updated ${result.affectedRows || 0} users from 'pt' to 'es'`);
+
+ console.log('\n========================================');
+ console.log('Migration completed successfully!');
+ console.log('========================================');
+
+ process.exit(0);
+ } catch (error) {
+ console.error('\nMigration failed:', error.message);
+ process.exit(1);
+ }
+}
+
+runMigration();
diff --git a/backend/src/services/authService.js b/backend/src/services/authService.js
index 95f1f72..ea20d39 100644
--- a/backend/src/services/authService.js
+++ b/backend/src/services/authService.js
@@ -4,6 +4,7 @@ const LoginHistory = require('../models/LoginHistory');
const Invitation = require('../models/Invitation');
const { generateToken, generateRefreshToken } = require('../utils/jwt');
const env = require('../config/env');
+const configService = require('./configService');
/**
* Authentication Service
@@ -64,11 +65,22 @@ const wechatLogin = async (code, userInfo = {}, invitationCode = null, deviceInf
let user = await User.findOne({ where: { wechatOpenId: openId } });
if (!user) {
+ // Get default avatar from config
+ let defaultAvatar = null;
+ try {
+ const avatarConfig = await configService.getConfig('default_avatar');
+ if (avatarConfig && avatarConfig.value) {
+ defaultAvatar = avatarConfig.value;
+ }
+ } catch (e) {
+ console.error('Failed to get default avatar config:', e);
+ }
+
// Create new user
const userData = {
wechatOpenId: openId,
nickname: userInfo.nickname || 'User',
- avatar: userInfo.avatar || null,
+ avatar: userInfo.avatar || defaultAvatar,
invitationCode: await generateUniqueInvitationCode(),
};
diff --git a/backend/src/services/configService.js b/backend/src/services/configService.js
index adb19b4..7a53f55 100644
--- a/backend/src/services/configService.js
+++ b/backend/src/services/configService.js
@@ -121,7 +121,7 @@ const getPublicConfigs = async () => {
const configs = await getAllConfigs();
// Filter only public configurations
- const publicKeys = ['app_logo', 'about_us_image', 'app_name', 'contact_phone', 'contact_email', 'contact_qr_image', 'user_agreement', 'privacy_policy', 'invite_rules'];
+ const publicKeys = ['app_logo', 'about_us_image', 'app_name', 'contact_phone', 'contact_email', 'contact_qr_image', 'user_agreement', 'privacy_policy', 'invite_rules', 'default_avatar'];
const publicConfigs = {};
configs.forEach(config => {
@@ -140,6 +140,7 @@ const initializeDefaults = async () => {
const defaults = [
{ key: 'app_logo', value: '', type: 'image', category: 'appearance', description: 'Application logo image URL' },
{ key: 'about_us_image', value: '', type: 'image', category: 'appearance', description: 'About us section image URL' },
+ { key: 'default_avatar', value: '', type: 'image', category: 'appearance', description: '用户默认头像' },
{ key: 'app_name', value: 'Overseas Appointment System', type: 'string', category: 'general', description: 'Application name' },
{ key: 'contact_phone', value: '', type: 'string', category: 'contact', description: 'Contact phone number' },
{ key: 'contact_email', value: '', type: 'string', category: 'contact', description: 'Contact email address' },
diff --git a/miniprogram/src/modules/Config.js b/miniprogram/src/modules/Config.js
index b56df4d..362a3b5 100644
--- a/miniprogram/src/modules/Config.js
+++ b/miniprogram/src/modules/Config.js
@@ -11,8 +11,8 @@ var Config = Config || {}
// API 基础地址
// 注意:微信小程序开发工具无法访问localhost,需要使用本机IP地址
-// Config.API_BASE_URL = 'https://sub.zpc-xy.com' // 本地开发环境(使用本机IP)
-Config.API_BASE_URL = 'http://localhost:3000' // 本地开发环境(浏览器可用)
+Config.API_BASE_URL = 'https://sub.zpc-xy.com' // 本地开发环境(使用本机IP)
+// Config.API_BASE_URL = 'http://localhost:3000' // 本地开发环境(浏览器可用)
// Config.API_BASE_URL = 'https://your-production-domain.com' // 生产环境(待配置)
// ============================================