Files
youlegames/codes/agent/game/wxserver_daoqi/index.js
2026-03-15 01:27:05 +08:00

511 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在同步...</title>
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<h3 style="text-align:center; margin-top: 50px;">正在同步微信头像...</h3>
<script>
function sendAndBack() {
// 发送消息给小程序
wx.miniProgram.postMessage({
data: ${JSON.stringify(userInfo)}
});
// 跳回小程序上一页
wx.miniProgram.navigateBack();
}
if (window.WeixinJSBridge) {
sendAndBack();
} else {
document.addEventListener('WeixinJSBridgeReady', sendAndBack, false);
}
// 保底策略
setTimeout(sendAndBack, 1000);
</script>
</body>
</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');
});