const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const axios = require('axios'); const path = require('path'); const fs = require('fs'); const multer = require('multer'); // const { S3Client } = require('@aws-sdk/client-s3'); // Removed S3 // const { Upload } = require('@aws-sdk/lib-storage'); // Removed S3 const config = require('./config/index'); const remoteConfig = require('./services/remoteConfig'); const logger = require('./utils/logger'); const app = express(); const port = config.port; app.use(cors()); app.use(bodyParser.json()); // 请求日志中间件 app.use((req, res, next) => { // 忽略静态资源请求日志,避免刷屏 if (!req.url.startsWith('/uploads') && !req.url.startsWith('/public')) { logger.info('HTTP', `${req.method} ${req.url}`); } next(); }); // 配置静态文件服务,用于微信域名校验文件 // 请将下载的 MP_verify_xxx.txt 文件放入 server/public 目录 app.use(express.static(path.join(__dirname, 'public'))); // 配置上传文件的静态服务 app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // ================================================================== // 公众号配置区域 (用于获取永久头像) // ================================================================== // 已移至 config.js // Step 1: 小程序跳转到这里,服务器重定向到微信授权页 app.get('/auth/oa/login', (req, res) => { // 1. 尝试从远程配置获取动态域名 const { agentid, gameid, channelid, marketid } = config.remoteConfig; const key = config.officialAccount.redirectDomainKey || 'auth_redirect_domain'; let currentDomain = remoteConfig.getParaValue(key, agentid, gameid, channelid, marketid); if (!currentDomain) { return res.status(500).send('Configuration Error: No redirect domain available in remote config.'); } // 3. 格式化域名 (确保有 https且无末尾斜杠) currentDomain = currentDomain.trim().replace(/\/$/, ''); if (!currentDomain.startsWith('http')) { currentDomain = `https://${currentDomain}`; } // 这里的 redirect_uri 必须是 encodeURIComponent 后的完整 URL const redirectUri = encodeURIComponent(`${currentDomain}/auth/oa/callback`); const scope = 'snsapi_userinfo'; // 获取头像必须用 userinfo 作用域 const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${config.officialAccount.appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=STATE#wechat_redirect`; // console.log('Redirecting to WeChat Auth:', url); // 打印跳转链接,方便调试 logger.info('Auth', 'Redirecting to WeChat Auth:', url); res.redirect(url); }); // Step 2: 微信回调,获取 code -> access_token -> userinfo -> 返回 HTML 给 web-view app.get('/auth/oa/callback', async (req, res) => { const code = req.query.code; // console.log('WeChat Callback Code:', code); // 打印回调 Code logger.info('Auth', 'WeChat Callback Code:', code); if (!code) return res.send('Auth failed, no code'); try { // 1. 获取 access_token const tokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${config.officialAccount.appId}&secret=${config.officialAccount.appSecret}&code=${code}&grant_type=authorization_code`; // console.log('Requesting Token URL:', tokenUrl); // 打印 Token 请求链接 (注意:生产环境不要打印 Secret) logger.debug('Auth', 'Requesting Token URL (Secret Hidden)'); const tokenRes = await axios.get(tokenUrl); // console.log('Token Response:', tokenRes.data); // 打印 Token 响应结果 logger.debug('Auth', 'Token Response:', tokenRes.data); if (tokenRes.data.errcode) { return res.send('Token Error: ' + JSON.stringify(tokenRes.data)); } const { access_token, openid } = tokenRes.data; // 2. 获取用户信息 (包含永久头像 headimgurl) const infoUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`; const infoRes = await axios.get(infoUrl); // console.log('User Info Response:', infoRes.data); // 打印用户信息响应 logger.success('Auth', 'User Info Retrieved:', infoRes.data.nickname); const userInfo = infoRes.data; // { headimgurl, nickname, unionid, ... } // 3. 返回一个 HTML,利用 JSSDK 把数据传回小程序 // 注意:wx.miniProgram.postMessage 只有在页面后退、销毁、分享时才会触发小程序的 bindmessage // 所以这里我们发送数据后,立即调用 navigateBack const html = ` 正在同步...

正在同步微信头像...

`; res.send(html); } catch (error) { console.error(error); res.send('Auth error: ' + error.message); } }); // 获取 AccessToken (简单实现,实际生产环境需要缓存 AccessToken) async function getAccessToken() { const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.miniProgram.appId}&secret=${config.miniProgram.appSecret}`; try { const response = await axios.get(url); if (response.data.errcode) { throw new Error(`获取 AccessToken 失败: ${response.data.errmsg}`); } return response.data.access_token; } catch (error) { console.error('Get AccessToken Error:', error.message); throw error; } } // 1. 登录接口:换取 OpenID 和 UnionID app.post('/api/login', async (req, res) => { const { code } = req.body; if (!code) { return res.status(400).json({ error: 'Code is required' }); } // 如果用户没有配置 AppID,返回模拟数据 if (config.miniProgram.appId === 'YOUR_APP_ID') { // console.log('未配置 AppID,返回模拟登录数据'); return res.json({ openid: 'mock_openid_' + Date.now(), unionid: 'mock_unionid_' + Date.now(), session_key: 'mock_session_key' }); } const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.miniProgram.appId}&secret=${config.miniProgram.appSecret}&js_code=${code}&grant_type=authorization_code`; try { const response = await axios.get(url); if (response.data.errcode) { return res.status(500).json({ error: response.data.errmsg }); } // 返回 openid, unionid (如果有), session_key res.json(response.data); } catch (error) { res.status(500).json({ error: error.message }); } }); // 2. 获取手机号接口 app.post('/api/getPhoneNumber', async (req, res) => { const { code } = req.body; if (!code) { return res.status(400).json({ error: 'Code is required' }); } // 如果用户没有配置 AppID,返回模拟数据 if (config.miniProgram.appId === 'YOUR_APP_ID') { // console.log('未配置 AppID,返回模拟手机号'); return res.json({ phoneNumber: '13800138000', purePhoneNumber: '13800138000', countryCode: '86', watermark: { timestamp: Date.now(), appid: config.miniProgram.appId } }); } try { const accessToken = await getAccessToken(); const url = `https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${accessToken}`; const response = await axios.post(url, { code }); if (response.data.errcode === 0) { // 成功获取 res.json(response.data.phone_info); } else { res.status(500).json({ error: response.data.errmsg, errcode: response.data.errcode }); } } catch (error) { console.error('Get Phone Number Error:', error); res.status(500).json({ error: error.message }); } }); // 缓存 game_server_http let cachedGameServerHttp = null; // 监听远程配置更新 remoteConfig.onUpdate(() => { const { agentid, gameid, channelid, marketid } = config.remoteConfig; const newValue = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid); if (newValue) { cachedGameServerHttp = newValue; // logger.info('Config', 'Updated cached game_server_http:', cachedGameServerHttp); } else { logger.warn('Config', 'game_server_http not found in new config'); } }); // 3. 文件上传接口 (本地存储) // 确保存储目录存在 const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, uploadDir) }, filename: function (req, file, cb) { const fileExtension = path.extname(file.originalname) || '.jpg'; const fileName = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}${fileExtension}`; cb(null, fileName) } }); const upload = multer({ storage: storage, fileFilter: (req, file, cb) => { if (config.enableAvatarUpload === false) { // 如果配置关闭了上传,拒绝文件 return cb(null, false); } cb(null, true); } }); app.post('/api/upload', upload.single('file'), (req, res) => { if (config.enableAvatarUpload === false) { return res.status(403).json({ error: 'Avatar upload is disabled' }); } if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); } try { // 构造访问链接 const protocol = req.protocol; const host = req.get('host'); const fileUrl = `${protocol}://${host}/uploads/${req.file.filename}`; logger.success('Upload', 'File uploaded successfully:', fileUrl); res.json({ url: fileUrl }); } catch (error) { logger.error('Upload', 'Upload Error:', error); res.status(500).json({ error: 'Upload failed: ' + error.message }); } }); // 4. 提交用户信息接口 (真实保存到游戏服务器) app.post('/api/saveUserInfo', async (req, res) => { const userInfo = req.body; logger.divider('Save User Info'); logger.info('API', 'Received User Info:', userInfo.nickName); try { // 1. 使用缓存的 game_server_http if (!cachedGameServerHttp) { // 如果缓存为空(例如启动时首次获取尚未完成),尝试手动获取一次 const { agentid, gameid, channelid, marketid } = config.remoteConfig; cachedGameServerHttp = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid); } if (!cachedGameServerHttp) { logger.error('Config', 'game_server_http not found'); return res.status(500).json({ error: 'Configuration error: game_server_http not found' }); } // 确保 URL 格式正确 const targetUrl = cachedGameServerHttp.startsWith('http') ? cachedGameServerHttp : `http://${cachedGameServerHttp}`; // 2. 构造请求数据 const { agentid, gameid, channelid, marketid } = config.remoteConfig; const payload = { "app": "youle", "route": "agent", "rpc": "bind_player_wechat", "data": { "agentid": agentid, "channelid": channelid, "marketid": marketid, "gameid": gameid, "openid": userInfo.openid, "unionid": userInfo.unionid, "nickname": userInfo.nickName, "avatar": userInfo.avatarUrl, "sex": userInfo.gender, "province": userInfo.province, "city": userInfo.city, "telphone": userInfo.phoneNumber } }; // --- Debug: 生成可粘贴到浏览器的 GET 链接 --- // try { // const debugQuery = `data=${encodeURIComponent(JSON.stringify(payload))}`; // const debugUrl = `${targetUrl}?${debugQuery}`; // console.log('\n[Debug] Request URL for Browser:'); // console.log(debugUrl); // console.log(''); // } catch (e) { // console.error('[Debug] Failed to generate debug URL:', e); // } // ------------------------------------------- // 3. 发送请求 const bodyData = 'data=' + JSON.stringify(payload); logger.info('GameServer', 'Sending bind_player_wechat request...'); logger.debug('GameServer', 'Payload:', payload); const response = await axios.post(targetUrl, bodyData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); logger.info('GameServer', 'Response received'); logger.debug('GameServer', 'Response Data:', response.data); const gameResult = response.data?.data?.result; const isSuccess = gameResult !== -1; res.json({ success: isSuccess, message: response.data?.data?.msg, data: response.data }); } catch (error) { logger.error('API', 'Save User Info Error:', error.message); res.status(500).json({ success: false, message: '用户信息保存失败: ' + error.message, error: error.message }); } }); // 5. 玩家登录接口 app.post('/api/playerLogin', async (req, res) => { const userInfo = req.body; logger.divider('Player Login'); logger.info('API', 'Received Login Request:', userInfo.nickName); try { if (!cachedGameServerHttp) { const { agentid, gameid, channelid, marketid } = config.remoteConfig; cachedGameServerHttp = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid); } if (!cachedGameServerHttp) { return res.status(500).json({ error: 'Configuration error: game_server_http not found' }); } const targetUrl = cachedGameServerHttp.startsWith('http') ? cachedGameServerHttp : `http://${cachedGameServerHttp}`; const { agentid, gameid, channelid, marketid } = config.remoteConfig; const payload = { "app": "youle", "route": "agent", "rpc": "player_login", "data": { "agentid": agentid, "channelid": channelid, "marketid": marketid, "gameid": gameid, "openid": userInfo.openid, "unionid": userInfo.unionid, "nickname": userInfo.nickName, "avatar": userInfo.avatarUrl, "sex": userInfo.gender, "province": userInfo.province, "city": userInfo.city, "version": userInfo.version, "telphone": userInfo.phoneNumber, "smmcode": userInfo.verificationCode, "telphoneAuto": userInfo.telphoneAuto, "playerid": userInfo.playerid, } }; // --- Debug: 生成可粘贴到浏览器的 GET 链接 --- // try { // const debugQuery = `data=${encodeURIComponent(JSON.stringify(payload))}`; // const debugUrl = `${targetUrl}?${debugQuery}`; // console.log('\n[Debug] Request URL for Browser:'); // console.log(debugUrl); // console.log(''); // } catch (e) { // console.error('[Debug] Failed to generate debug URL:', e); // } // ------------------------------------------- const bodyData = 'data=' + JSON.stringify(payload); logger.info('GameServer', 'Sending player_login request...'); logger.debug('GameServer', 'Payload:', payload); const response = await axios.post(targetUrl, bodyData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); logger.info('GameServer', 'Login Response received'); logger.debug('GameServer', 'Response Data:', response.data); res.json(response.data); } catch (error) { logger.error('API', 'Player Login Error:', error.message); res.status(500).json({ error: '登录失败: ' + error.message }); } }); // 6. 获取验证码接口 app.post('/api/getPhoneCode', async (req, res) => { const { phonenum } = req.body; logger.divider('Get Phone Code'); logger.info('API', 'Received GetCode Request:', phonenum); try { if (!cachedGameServerHttp) { const { agentid, gameid, channelid, marketid } = config.remoteConfig; cachedGameServerHttp = remoteConfig.getParaValue('game_server_http', agentid, gameid, channelid, marketid); } if (!cachedGameServerHttp) { return res.status(500).json({ error: 'Configuration error: game_server_http not found' }); } const targetUrl = cachedGameServerHttp.startsWith('http') ? cachedGameServerHttp : `http://${cachedGameServerHttp}`; const { agentid } = config.remoteConfig; const payload = { "app": "youle", "route": "agent", "rpc": "send_phone_code_wechat", "data": { "agentid": agentid, "phonenum": phonenum } }; // --- Debug: 生成可粘贴到浏览器的 GET 链接 --- // try { // const debugQuery = `data=${encodeURIComponent(JSON.stringify(payload))}`; // const debugUrl = `${targetUrl}?${debugQuery}`; // console.log('\n[Debug] Request URL for Browser:'); // console.log(debugUrl); // console.log(''); // } catch (e) { // console.error('[Debug] Failed to generate debug URL:', e); // } // ------------------------------------------- const bodyData = 'data=' + JSON.stringify(payload); logger.info('GameServer', 'Sending get_phone_code_wechat request...'); const response = await axios.post(targetUrl, bodyData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); logger.info('GameServer', 'GetCode Response received'); logger.debug('GameServer', 'Response Data:', response.data); res.json(response.data); } catch (error) { logger.error('API', 'Get Phone Code Error:', error.message); res.status(500).json({ error: '获取随机数失败: ' + error.message }); } }); // 启动远程配置更新 remoteConfig.start(); app.listen(port, () => { logger.divider(); logger.success('System', `Server running at http://localhost:${port}`); logger.info('System', 'Press Ctrl+C to stop'); });