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' // 生产环境(待配置) // ============================================