|
|
@@ -1,12 +1,9 @@
|
|
|
-import { Context } from "koa";
|
|
|
-import {
|
|
|
- ChannelHandler,
|
|
|
- LoginResult,
|
|
|
- PaymentResult,
|
|
|
-} from "../interfaces/ChannelHandler";
|
|
|
-import { ChannelConfig } from "../../config/channelConfig";
|
|
|
-import { getServerList, formatDate } from "../../utils/common";
|
|
|
-import { Account } from "../../config/thirdParams";
|
|
|
+import {Context} from "koa";
|
|
|
+import * as crypto from 'crypto';
|
|
|
+import {ChannelHandler, LoginResult, PaymentResult,} from "../interfaces/ChannelHandler";
|
|
|
+import {ChannelConfig} from "../../config/channelConfig";
|
|
|
+import {formatDate, getServerList} from "../../utils/common";
|
|
|
+import {Account} from "../../config/thirdParams";
|
|
|
import Msg from "../../utils/msg";
|
|
|
import axios from "axios";
|
|
|
|
|
|
@@ -15,682 +12,765 @@ const CryptoJS = require("crypto-js");
|
|
|
const logger = require("../../utils/log");
|
|
|
|
|
|
export class MiniappChannelHandler implements ChannelHandler {
|
|
|
- /**
|
|
|
- * 获取HTTPS代理配置
|
|
|
- * @returns HTTPS代理配置
|
|
|
- */
|
|
|
- private getHttpsAgent() {
|
|
|
- const https = require('https');
|
|
|
- return new https.Agent({
|
|
|
- rejectUnauthorized: false,
|
|
|
- secureProtocol: 'TLSv1_2_method',
|
|
|
- timeout: 10000
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取通用请求头
|
|
|
- * @returns 请求头配置
|
|
|
- */
|
|
|
- private getCommonHeaders() {
|
|
|
- return {
|
|
|
- 'User-Agent': 'Mozilla/5.0 (compatible; WebServer/1.0)',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'Content-Type': 'application/json'
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 小程序登录鉴权
|
|
|
- * @param ctx Koa上下文
|
|
|
- * @param config 渠道配置
|
|
|
- */
|
|
|
- async handleLogin(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
- try {
|
|
|
- const data = ctx.request.body as any;
|
|
|
- const token = data.token;
|
|
|
-
|
|
|
- if (!token) {
|
|
|
+ /**
|
|
|
+ * 获取HTTPS代理配置
|
|
|
+ * @returns HTTPS代理配置
|
|
|
+ */
|
|
|
+ private getHttpsAgent() {
|
|
|
+ const https = require('https');
|
|
|
+ return new https.Agent({
|
|
|
+ rejectUnauthorized: false,
|
|
|
+ secureProtocol: 'TLSv1_2_method',
|
|
|
+ timeout: 10000
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取通用请求头
|
|
|
+ * @returns 请求头配置
|
|
|
+ */
|
|
|
+ private getCommonHeaders() {
|
|
|
return {
|
|
|
- code: -1,
|
|
|
- msg: "缺少token参数",
|
|
|
- data: null
|
|
|
+ 'User-Agent': 'Mozilla/5.0 (compatible; WebServer/1.0)',
|
|
|
+ 'Accept': 'application/json',
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
};
|
|
|
- }
|
|
|
-
|
|
|
- const loginUrl = `https://wxlogin.${config.paymentConfig.apiUrl}/wxlogin?cmd=checkUserToken&token=${token}`;
|
|
|
- logger.info("小程序登录请求", { token, loginUrl });
|
|
|
-
|
|
|
- const wxLogin = await axios.get(loginUrl, {
|
|
|
- httpsAgent: this.getHttpsAgent(),
|
|
|
- headers: this.getCommonHeaders()
|
|
|
- });
|
|
|
- logger.info("小程序登录响应", { data: wxLogin.data });
|
|
|
-
|
|
|
- return {
|
|
|
- code: 0,
|
|
|
- msg: "success",
|
|
|
- data: wxLogin.data,
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- logger.error('wxLogin error:', error);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: error.message || "登录验证失败",
|
|
|
- data: null,
|
|
|
- };
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 小程序支付回调处理
|
|
|
- * @param ctx Koa上下文
|
|
|
- * @param config 渠道配置
|
|
|
- */
|
|
|
- async handlePayment(ctx: Context, config: ChannelConfig): Promise<PaymentResult> {
|
|
|
- try {
|
|
|
- // 获取请求参数
|
|
|
- const params = ctx.request.query as any;
|
|
|
- logger.info("小程序支付回调参数:", { url: ctx.href, params: params });
|
|
|
-
|
|
|
- // 验证必要参数
|
|
|
- const requiredParams = ['uid', 'rmb', 'reqid', 'trans_id', 'product_id', 'notify_id', 'sign'];
|
|
|
- for (const param of requiredParams) {
|
|
|
- if (!params[param]) {
|
|
|
- logger.error(`缺少必要参数: ${param}`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: `缺少必要参数: ${param}`,
|
|
|
- data: null
|
|
|
- };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序登录鉴权
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async handleLogin(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
+ try {
|
|
|
+ const data = ctx.request.body as any;
|
|
|
+ const token = data.token;
|
|
|
+
|
|
|
+ if (!token) {
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "缺少token参数",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const loginUrl = `https://wxlogin.${config.paymentConfig.apiUrl}/wxlogin?cmd=checkUserToken&token=${token}`;
|
|
|
+ logger.info("小程序登录请求", {token, loginUrl});
|
|
|
+
|
|
|
+ const wxLogin = await axios.get(loginUrl, {
|
|
|
+ httpsAgent: this.getHttpsAgent(),
|
|
|
+ headers: this.getCommonHeaders()
|
|
|
+ });
|
|
|
+ logger.info("小程序登录响应", {data: wxLogin.data});
|
|
|
+ if (!wxLogin.data || Object.keys(wxLogin.data).length === 0) {
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "登录验证失败",
|
|
|
+ data: null,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "success",
|
|
|
+ data: wxLogin.data,
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('wxLogin error:', error);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: error.message || "登录验证失败",
|
|
|
+ data: null,
|
|
|
+ };
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- // 验证签名
|
|
|
- const parameters = Object.keys(params)
|
|
|
- .filter(key => key !== 'sign')
|
|
|
- .map(key => `${key}=${params[key]}`);
|
|
|
-
|
|
|
- const expectedSignature = this.generateSignature(parameters, config.paymentConfig.signKey!);
|
|
|
- if (params.sign !== expectedSignature) {
|
|
|
- logger.error("小程序支付签名验证失败", {
|
|
|
- received: params.sign,
|
|
|
- expected: expectedSignature
|
|
|
- });
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "签名验证失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- // 获取订单信息
|
|
|
- const orderId = params.reqid;
|
|
|
- const orderInfo = (await Order.getOrder(orderId))[0];
|
|
|
+ /**
|
|
|
+ * 小程序支付回调处理
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async handlePayment(ctx: Context, config: ChannelConfig): Promise<PaymentResult> {
|
|
|
+ try {
|
|
|
+ // 获取请求参数
|
|
|
+ const params = ctx.request.query as any;
|
|
|
+ logger.info("小程序支付回调参数:", {url: ctx.href, params: params});
|
|
|
+
|
|
|
+ // 验证必要参数
|
|
|
+ const requiredParams = ['uid', 'rmb', 'reqid', 'trans_id', 'product_id', 'notify_id', 'sign'];
|
|
|
+ for (const param of requiredParams) {
|
|
|
+ if (!params[param]) {
|
|
|
+ logger.error(`缺少必要参数: ${param}`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: `缺少必要参数: ${param}`,
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证签名
|
|
|
+ const parameters = Object.keys(params)
|
|
|
+ .filter(key => key !== 'sign')
|
|
|
+ .map(key => `${key}=${params[key]}`);
|
|
|
+
|
|
|
+ const expectedSignature = this.generateSignature(parameters, config.paymentConfig.signKey!);
|
|
|
+ if (params.sign !== expectedSignature) {
|
|
|
+ logger.error("小程序支付签名验证失败", {
|
|
|
+ received: params.sign,
|
|
|
+ expected: expectedSignature
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "签名验证失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取订单信息
|
|
|
+ const orderId = params.reqid;
|
|
|
+ const orderInfo = (await Order.getOrder(orderId))[0];
|
|
|
+
|
|
|
+ if (!orderInfo) {
|
|
|
+ logger.error(`订单${orderId}不存在`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "订单不存在",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (orderInfo.status == 2) {
|
|
|
+ logger.info(`订单${orderId}已经重复发货`);
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "订单已发货",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证金额
|
|
|
+ const amount = parseFloat(params.rmb);
|
|
|
+ if (orderInfo.amount != amount) {
|
|
|
+ logger.error("订单金额不一致", {
|
|
|
+ "orderInfo.amount": orderInfo.amount,
|
|
|
+ "params.amount": amount,
|
|
|
+ });
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "订单金额不一致",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 订单校验 - 验证支付是否成功
|
|
|
+ const orderCheckResult = await this.wxOrderCheck(params.notify_id, config);
|
|
|
+ if (!orderCheckResult.success) {
|
|
|
+ logger.error(`订单${orderId}校验失败: ${orderCheckResult.message}`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: `订单校验失败: ${orderCheckResult.message}`,
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取服务器URL
|
|
|
+ const url = await getServerList(orderInfo.server_id, orderInfo.channel_id);
|
|
|
+ if (!url) {
|
|
|
+ logger.error(`区服id错误: serverId ${orderInfo.server_id}`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "区服id错误",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发货处理
|
|
|
+ logger.info(`小程序支付订单${orderId}通知游戏发货开始`);
|
|
|
+ const result = await this.deliverOrder(
|
|
|
+ orderInfo,
|
|
|
+ ctx.request.ip,
|
|
|
+ url,
|
|
|
+ params.trans_id
|
|
|
+ );
|
|
|
+
|
|
|
+ if ((result as any).code === 1) {
|
|
|
+ logger.info(`小程序支付订单${orderId}发货成功`);
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "发货成功",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ logger.error(`小程序支付订单${orderId}发货失败: ${(result as any).msg}`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: `发货失败: ${(result as any).msg}`,
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- if (!orderInfo) {
|
|
|
- logger.error(`订单${orderId}不存在`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "订单不存在",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("小程序支付处理出错:", error);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "支付处理失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (orderInfo.status == 2) {
|
|
|
- logger.info(`订单${orderId}已经重复发货`);
|
|
|
- return {
|
|
|
- code: 0,
|
|
|
- msg: "订单已发货",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 验证金额
|
|
|
- const amount = parseFloat(params.rmb);
|
|
|
- if (orderInfo.amount != amount) {
|
|
|
- logger.error("订单金额不一致", {
|
|
|
- "orderInfo.amount": orderInfo.amount,
|
|
|
- "params.amount": amount,
|
|
|
- });
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "订单金额不一致",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 订单校验 - 验证支付是否成功
|
|
|
+ * @param notifyId 平台通知ID
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 校验结果
|
|
|
+ */
|
|
|
+ async wxOrderCheck(notifyId: string, config: ChannelConfig): Promise<{ success: boolean; message: string }> {
|
|
|
+ try {
|
|
|
+ // 构建验证参数
|
|
|
+ const parameters = [
|
|
|
+ `gameid=${config.paymentConfig.gameId}`,
|
|
|
+ `notify_id=${notifyId}`
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 生成签名
|
|
|
+ const signature = this.generateSignature(parameters, config.paymentConfig.signKey!);
|
|
|
+
|
|
|
+ // 构建验证URL
|
|
|
+ const verifyUrl = `https://login.${config.paymentConfig.apiUrl}/pay/paygate/verify.php?gameid=${config.paymentConfig.gameId}¬ify_id=${notifyId}&sign=${signature}`;
|
|
|
+
|
|
|
+ logger.info("订单校验请求", {notifyId, verifyUrl});
|
|
|
+
|
|
|
+ // 调用平台验证接口
|
|
|
+ const response = await axios.get(verifyUrl, {
|
|
|
+ httpsAgent: this.getHttpsAgent(),
|
|
|
+ headers: {
|
|
|
+ ...this.getCommonHeaders(),
|
|
|
+ 'Accept': 'text/plain'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.info("订单校验响应", {notifyId, response: response.data});
|
|
|
+
|
|
|
+ // 检查返回结果
|
|
|
+ if (response.data === "SUCCESS") {
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: "订单支付成功"
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "订单支付失败或无效"
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- // 订单校验 - 验证支付是否成功
|
|
|
- const orderCheckResult = await this.wxOrderCheck(params.notify_id, config);
|
|
|
- if (!orderCheckResult.success) {
|
|
|
- logger.error(`订单${orderId}校验失败: ${orderCheckResult.message}`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: `订单校验失败: ${orderCheckResult.message}`,
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("订单校验出错:", {notifyId, error: error.message});
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "订单校验失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 获取服务器URL
|
|
|
- const url = await getServerList(orderInfo.server_id, orderInfo.channel_id);
|
|
|
- if (!url) {
|
|
|
- logger.error(`区服id错误: serverId ${orderInfo.server_id}`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "区服id错误",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 发货处理
|
|
|
- logger.info(`小程序支付订单${orderId}通知游戏发货开始`);
|
|
|
- const result = await this.deliverOrder(
|
|
|
- orderInfo,
|
|
|
- ctx.request.ip,
|
|
|
- url,
|
|
|
- params.trans_id
|
|
|
- );
|
|
|
-
|
|
|
- if ((result as any).code === 1) {
|
|
|
- logger.info(`小程序支付订单${orderId}发货成功`);
|
|
|
- return {
|
|
|
- code: 0,
|
|
|
- msg: "发货成功",
|
|
|
- data: null
|
|
|
- };
|
|
|
- } else {
|
|
|
- logger.error(`小程序支付订单${orderId}发货失败: ${(result as any).msg}`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: `发货失败: ${(result as any).msg}`,
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- logger.error("小程序支付处理出错:", error);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "支付处理失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * 内容安全审核接口
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async contentSecurityCheck(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
+ try {
|
|
|
+ const data = ctx.request.body as any;
|
|
|
+ logger.info("内容安全审核请求参数:", data);
|
|
|
+
|
|
|
+ // 验证必要参数
|
|
|
+ const requiredParams = ['uid', 'scene', 'content'];
|
|
|
+ for (const param of requiredParams) {
|
|
|
+ if (!data[param]) {
|
|
|
+ logger.error(`缺少必要参数: ${param}`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: `缺少必要参数: ${param}`,
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ data['timestamp'] = Math.floor(Date.now() / 1000); // 添加时间戳参数
|
|
|
+ data['gameid'] = config.paymentConfig.gameId; // 添加游戏ID参数
|
|
|
+ data['nonce'] = Math.floor(Math.random() * 1000000); // 添加随机数参数
|
|
|
+ //生成签名
|
|
|
+ data['signature'] = this.generateContentSecuritySignature(data, config);
|
|
|
+ if (!data['signature']) {
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "签名生成失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // 构建请求参数
|
|
|
+ const requestData = {
|
|
|
+ uid: data.uid,
|
|
|
+ gameid: data.gameid,
|
|
|
+ signature: data.signature,
|
|
|
+ timestamp: data.timestamp,
|
|
|
+ nonce: data.nonce,
|
|
|
+ scene: data.scene,
|
|
|
+ content: data.content,
|
|
|
+ nickname: data.nickname || '',
|
|
|
+ title: data.title || '',
|
|
|
+ usersign: data.usersign || ''
|
|
|
+ };
|
|
|
+
|
|
|
+ // 调用内容安全API
|
|
|
+ const apiUrl = `https://api.${config.paymentConfig.apiUrl}/mpcommon/?cmd=wxaMsgSecCheck`;
|
|
|
+ logger.info("调用内容安全API:", {url: apiUrl, data: requestData});
|
|
|
+
|
|
|
+ const response = await axios.post(apiUrl, requestData, {
|
|
|
+ httpsAgent: this.getHttpsAgent(),
|
|
|
+ headers: this.getCommonHeaders()
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.info("内容安全API响应:", response.data);
|
|
|
+
|
|
|
+ // 处理响应结果
|
|
|
+ if (response.data && response.data.error === 0) {
|
|
|
+ const result = response.data.result;
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "success",
|
|
|
+ data: {
|
|
|
+ suggest: result.suggest, // pass/review/reject
|
|
|
+ label: result.label,
|
|
|
+ trace_id: response.data.trace_id,
|
|
|
+ detail: response.data.detail
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: response.data?.errmsg || "内容安全检测失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("内容安全审核出错:", error);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "内容安全审核失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 订单校验 - 验证支付是否成功
|
|
|
- * @param notifyId 平台通知ID
|
|
|
- * @param config 渠道配置
|
|
|
- * @returns 校验结果
|
|
|
- */
|
|
|
- async wxOrderCheck(notifyId: string, config: ChannelConfig): Promise<{ success: boolean; message: string }> {
|
|
|
- try {
|
|
|
- // 构建验证参数
|
|
|
- const parameters = [
|
|
|
- `gameid=${config.paymentConfig.gameId}`,
|
|
|
- `notify_id=${notifyId}`
|
|
|
- ];
|
|
|
-
|
|
|
- // 生成签名
|
|
|
- const signature = this.generateSignature(parameters, config.paymentConfig.signKey!);
|
|
|
-
|
|
|
- // 构建验证URL
|
|
|
- const verifyUrl = `https://login.${config.paymentConfig.apiUrl}/pay/paygate/verify.php?gameid=${config.paymentConfig.gameId}¬ify_id=${notifyId}&sign=${signature}`;
|
|
|
-
|
|
|
- logger.info("订单校验请求", { notifyId, verifyUrl });
|
|
|
-
|
|
|
- // 调用平台验证接口
|
|
|
- const response = await axios.get(verifyUrl, {
|
|
|
- httpsAgent: this.getHttpsAgent(),
|
|
|
- headers: {
|
|
|
- ...this.getCommonHeaders(),
|
|
|
- 'Accept': 'text/plain'
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 角色名称修改上报
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async editUserRoleInfo(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
+ try {
|
|
|
+ const data = ctx.request.body as any;
|
|
|
+ logger.info("角色名称修改上报请求参数:", data);
|
|
|
+ // 验证必要参数
|
|
|
+ const requiredParams = ['openId', 'serverid', 'playerName'];
|
|
|
+ for (const param of requiredParams) {
|
|
|
+ if (!data[param]) {
|
|
|
+ logger.error(`缺少必要参数: ${param}`);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: `缺少必要参数: ${param}`,
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+ data['time'] = Math.floor(Date.now() / 1000); // 添加时间戳参数
|
|
|
+ data['gameid'] = config.paymentConfig.gameId; // 添加游戏ID参数
|
|
|
+
|
|
|
+ // 处理playerName的各种编码格式
|
|
|
+ let decodedPlayerName = data.playerName;
|
|
|
+ try {
|
|
|
+ // 检查是否是URL编码格式
|
|
|
+ if (data.playerName.includes('%')) {
|
|
|
+ decodedPlayerName = decodeURIComponent(data.playerName);
|
|
|
+ console.log("playerName URL decoded from:", data.playerName);
|
|
|
+ console.log("playerName URL decoded to:", decodedPlayerName);
|
|
|
+ }
|
|
|
+ // 检查是否是Unicode转义序列格式
|
|
|
+ else if (data.playerName.includes('\\u')) {
|
|
|
+ decodedPlayerName = JSON.parse('"' + data.playerName + '"');
|
|
|
+ console.log("playerName Unicode decoded from:", data.playerName);
|
|
|
+ console.log("playerName Unicode decoded to:", decodedPlayerName);
|
|
|
+ } else {
|
|
|
+ console.log("playerName is not encoded:", data.playerName);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log("playerName decode error, using original:", error.message);
|
|
|
+ decodedPlayerName = data.playerName;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新data中的playerName为解码后的值
|
|
|
+ data.playerName = decodedPlayerName;
|
|
|
+ console.log("playerName for signature:", data.playerName);
|
|
|
+
|
|
|
+ //生成签名
|
|
|
+ data['sign'] = this.generateRoleInfoSignature(data, config);
|
|
|
+
|
|
|
+ // 构建请求参数 - 对playerName进行URL编码
|
|
|
+ const encodedPlayerName = encodeURIComponent(data.playerName);
|
|
|
+ console.log("playerName for request:", encodedPlayerName);
|
|
|
+
|
|
|
+ // 手动构建URL查询字符串,避免对已编码参数进行二次编码
|
|
|
+ const queryString = [
|
|
|
+ `openId=${encodeURIComponent(data.openId)}`,
|
|
|
+ `time=${encodeURIComponent(data.time)}`,
|
|
|
+ `gameid=${encodeURIComponent(data.gameid)}`,
|
|
|
+ `serverid=${encodeURIComponent(data.serverid)}`,
|
|
|
+ `playerName=${encodedPlayerName}`, // 已经编码过的playerName,不再编码
|
|
|
+ `sign=${encodeURIComponent(data.sign)}`
|
|
|
+ ].join('&');
|
|
|
+
|
|
|
+ // 调用角色信息修改API
|
|
|
+ const apiUrl = `https://platform.${config.paymentConfig.apiUrl}/stat/api/?cmd=editUserRoleInfo&${queryString}`;
|
|
|
+ logger.info("调用角色信息修改API:", {url: apiUrl});
|
|
|
+
|
|
|
+ const response = await axios.get(apiUrl, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: {
|
|
|
+ 'User-Agent': 'Mozilla/5.0 (compatible; WebServer/1.0)',
|
|
|
+ 'Accept': 'application/json'
|
|
|
+ },
|
|
|
+ httpsAgent: this.getHttpsAgent()
|
|
|
+ });
|
|
|
+ logger.info("角色信息修改API响应:", response.data);
|
|
|
+
|
|
|
+ // 处理响应结果
|
|
|
+ if (response.data && response.data.error === 0) {
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "success",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: response.data?.errmsg || "角色信息修改失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("角色名称修改上报出错:", error);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "角色信息修改失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- logger.info("订单校验响应", { notifyId, response: response.data });
|
|
|
+ }
|
|
|
|
|
|
- // 检查返回结果
|
|
|
- if (response.data === "SUCCESS") {
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- message: "订单支付成功"
|
|
|
- };
|
|
|
- } else {
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- message: "订单支付失败或无效"
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- logger.error("订单校验出错:", { notifyId, error: error.message });
|
|
|
- return {
|
|
|
- success: false,
|
|
|
- message: "订单校验失败"
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * 签名算法2验证
|
|
|
+ * @param data 请求数据
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 验证结果
|
|
|
+ */
|
|
|
+ private verifySignatureAlgorithm2(data: any, config: ChannelConfig): boolean {
|
|
|
+ try {
|
|
|
+ // 固定密钥
|
|
|
+ const secret = 'qka8qKcvRcGN1u0bLA8O';
|
|
|
+
|
|
|
+ // 准备签名参数
|
|
|
+ const signParams: any = {
|
|
|
+ gameId: data.gameid || config.paymentConfig.gameId,
|
|
|
+ openId: data.openId,
|
|
|
+ playerName: encodeURIComponent(data.playerName), // 中文URL编码
|
|
|
+ secret: secret,
|
|
|
+ serverId: data.serverid,
|
|
|
+ time: data.time
|
|
|
+ };
|
|
|
+
|
|
|
+ // 按字典排序参数名
|
|
|
+ const sortedKeys = Object.keys(signParams).sort();
|
|
|
+
|
|
|
+ // 构建签名字符串(去掉[])
|
|
|
+ const signString = sortedKeys.map(key => {
|
|
|
+ return `${key}=${signParams[key]}`;
|
|
|
+ }).join('');
|
|
|
+
|
|
|
+ console.log("验证签名参数:", signParams);
|
|
|
+ console.log("验证签名字符串:", signString);
|
|
|
+
|
|
|
+ // 计算MD5签名
|
|
|
+ const expectedSignature = CryptoJS.MD5(signString).toString();
|
|
|
+ const actualSignature = data.sign;
|
|
|
+
|
|
|
+ console.log("期望签名:", expectedSignature);
|
|
|
+ console.log("实际签名:", actualSignature);
|
|
|
+
|
|
|
+ return expectedSignature === actualSignature;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("签名算法2验证出错:", error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 内容安全审核接口
|
|
|
- * @param ctx Koa上下文
|
|
|
- * @param config 渠道配置
|
|
|
- */
|
|
|
- async contentSecurityCheck(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
- try {
|
|
|
- const data = ctx.request.body as any;
|
|
|
- logger.info("内容安全审核请求参数:", data);
|
|
|
-
|
|
|
- // 验证必要参数
|
|
|
- const requiredParams = ['uid', 'gameid', 'signature', 'timestamp', 'nonce', 'scene', 'content'];
|
|
|
- for (const param of requiredParams) {
|
|
|
- if (!data[param]) {
|
|
|
- logger.error(`缺少必要参数: ${param}`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: `缺少必要参数: ${param}`,
|
|
|
- data: null
|
|
|
- };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成角色信息修改签名(签名算法2)
|
|
|
+ * @param params 签名参数
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 签名结果
|
|
|
+ */
|
|
|
+ generateRoleInfoSignature(params: any, config: ChannelConfig): string | null {
|
|
|
+ try {
|
|
|
+ // 固定密钥
|
|
|
+ const secret = 'qka8qKcvRcGN1u0bLA8O';
|
|
|
+
|
|
|
+ // 准备签名参数
|
|
|
+ const signParams: any = {
|
|
|
+ gameid: params.gameid || config.paymentConfig.gameId,
|
|
|
+ openId: params.openId,
|
|
|
+ playerName: encodeURIComponent(params.playerName), // 中文URL编码
|
|
|
+ secret: secret,
|
|
|
+ serverid: params.serverid,
|
|
|
+ time: params.time
|
|
|
+ };
|
|
|
+
|
|
|
+ // 按字典排序参数名
|
|
|
+ const sortedKeys = Object.keys(signParams).sort();
|
|
|
+
|
|
|
+ // 构建签名字符串(去掉[])
|
|
|
+ const signString = sortedKeys.map(key => {
|
|
|
+ return `${key}=${signParams[key]}`;
|
|
|
+ }).join('');
|
|
|
+
|
|
|
+ console.log("角色信息签名参数:", signParams);
|
|
|
+ console.log("角色信息签名字符串:", signString);
|
|
|
+
|
|
|
+ // 计算MD5签名
|
|
|
+ const signature = CryptoJS.MD5(signString).toString();
|
|
|
+
|
|
|
+ console.log("角色信息签名结果:", signature);
|
|
|
+
|
|
|
+ return signature;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("生成角色信息修改签名出错:", error);
|
|
|
+ return null;
|
|
|
}
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- // 验证签名算法1
|
|
|
- const isValidSignature = this.verifySignatureAlgorithm1(data, config);
|
|
|
- if (!isValidSignature) {
|
|
|
- logger.error("内容安全审核签名验证失败");
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "签名验证失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 构建请求参数
|
|
|
- const requestData = {
|
|
|
- uid: data.uid,
|
|
|
- gameid: data.gameid,
|
|
|
- signature: data.signature,
|
|
|
- timestamp: data.timestamp,
|
|
|
- nonce: data.nonce,
|
|
|
- scene: data.scene,
|
|
|
- content: data.content,
|
|
|
- nickname: data.nickname || '',
|
|
|
- title: data.title || '',
|
|
|
- usersign: data.usersign || ''
|
|
|
- };
|
|
|
-
|
|
|
- // 调用内容安全API
|
|
|
- const apiUrl = `https://api.${config.paymentConfig.apiUrl}/mpcommon/?cmd=wxaMsgSecCheck`;
|
|
|
- logger.info("调用内容安全API:", { url: apiUrl, data: requestData });
|
|
|
-
|
|
|
- const response = await axios.post(apiUrl, requestData, {
|
|
|
- httpsAgent: this.getHttpsAgent(),
|
|
|
- headers: this.getCommonHeaders()
|
|
|
- });
|
|
|
-
|
|
|
- logger.info("内容安全API响应:", response.data);
|
|
|
-
|
|
|
- // 处理响应结果
|
|
|
- if (response.data && response.data.error === 0) {
|
|
|
- const result = response.data.result;
|
|
|
- return {
|
|
|
- code: 0,
|
|
|
- msg: "success",
|
|
|
- data: {
|
|
|
- suggest: result.suggest, // pass/review/reject
|
|
|
- label: result.label,
|
|
|
- trace_id: response.data.trace_id,
|
|
|
- detail: response.data.detail
|
|
|
- }
|
|
|
- };
|
|
|
- } else {
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: response.data?.errmsg || "内容安全检测失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- logger.error("内容安全审核出错:", error);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "内容安全审核失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * 签名算法1验证
|
|
|
+ * @param data 请求数据
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 验证结果
|
|
|
+ */
|
|
|
+ private verifySignatureAlgorithm1(data: any, config: ChannelConfig): boolean {
|
|
|
+ return this.verifyCommonSignature(
|
|
|
+ data,
|
|
|
+ config.loginConfig?.apiKey || '',
|
|
|
+ 'signature',
|
|
|
+ ['signature'],
|
|
|
+ false // 内容安全签名不需要URL编码
|
|
|
+ );
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 角色名称修改上报
|
|
|
- * @param ctx Koa上下文
|
|
|
- * @param config 渠道配置
|
|
|
- */
|
|
|
- async editUserRoleInfo(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
- try {
|
|
|
- const data = ctx.request.body as any;
|
|
|
- logger.info("角色名称修改上报请求参数:", data);
|
|
|
-
|
|
|
- // 验证必要参数
|
|
|
- const requiredParams = ['openId', 'time', 'gameid', 'serverid', 'playerName', 'sign'];
|
|
|
- for (const param of requiredParams) {
|
|
|
- if (!data[param]) {
|
|
|
- logger.error(`缺少必要参数: ${param}`);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: `缺少必要参数: ${param}`,
|
|
|
- data: null
|
|
|
- };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成内容安全签名(供客户端使用)
|
|
|
+ * @param params 签名参数
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 签名结果
|
|
|
+ */
|
|
|
+ generateContentSecuritySignature(params: any, config: ChannelConfig): string | null {
|
|
|
+ try {
|
|
|
+ let arr = [config.loginConfig.apiKey, params.timestamp, params.nonce];
|
|
|
+ arr.sort();
|
|
|
+ let str = arr.join('');
|
|
|
+ return crypto.createHash('sha1').update(str).digest('hex');
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("生成内容安全签名出错:", error);
|
|
|
+ return null;
|
|
|
}
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- // 验证签名算法2
|
|
|
- const isValidSignature = this.verifySignatureAlgorithm2(data, config);
|
|
|
- if (!isValidSignature) {
|
|
|
- logger.error("角色名称修改上报签名验证失败");
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "签名验证失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // 构建请求参数
|
|
|
- const requestData = {
|
|
|
- openId: data.openId,
|
|
|
- time: data.time,
|
|
|
- gameid: data.gameid,
|
|
|
- serverid: data.serverid,
|
|
|
- playerName: encodeURIComponent(data.playerName), // 中文URL编码
|
|
|
- sign: data.sign
|
|
|
- };
|
|
|
-
|
|
|
- // 调用角色信息修改API
|
|
|
- const apiUrl = `https://platform.${config.paymentConfig.apiUrl}/stat/api/?cmd=editUserRoleInfo`;
|
|
|
- logger.info("调用角色信息修改API:", { url: apiUrl, data: requestData });
|
|
|
-
|
|
|
- const response = await axios.get(apiUrl, {
|
|
|
- params: requestData,
|
|
|
- timeout: 10000,
|
|
|
- headers: {
|
|
|
- 'User-Agent': 'Mozilla/5.0 (compatible; WebServer/1.0)',
|
|
|
- 'Accept': 'application/json'
|
|
|
- },
|
|
|
- httpsAgent: this.getHttpsAgent()
|
|
|
- });
|
|
|
- logger.info("角色信息修改API响应:", response.data);
|
|
|
-
|
|
|
- // 处理响应结果
|
|
|
- if (response.data && response.data.error === 0) {
|
|
|
- return {
|
|
|
- code: 0,
|
|
|
- msg: "success",
|
|
|
- data: null
|
|
|
- };
|
|
|
- } else {
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: response.data?.errmsg || "角色信息修改失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- logger.error("角色名称修改上报出错:", error);
|
|
|
- return {
|
|
|
- code: -1,
|
|
|
- msg: "角色信息修改失败",
|
|
|
- data: null
|
|
|
- };
|
|
|
+ /**
|
|
|
+ * 发货处理方法
|
|
|
+ * @param orderInfo 订单信息
|
|
|
+ * @param ip 客户端IP
|
|
|
+ * @param url 游戏服务器URL
|
|
|
+ * @param out_trade_no 外部交易号
|
|
|
+ * @returns 发货结果
|
|
|
+ */
|
|
|
+ private async deliverOrder(orderInfo: any, ip: string, url: string, out_trade_no: string): Promise<{
|
|
|
+ code: number;
|
|
|
+ msg: string
|
|
|
+ }> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const sendMsg = new Msg();
|
|
|
+ logger.info("通知游戏服务器url", {url: url});
|
|
|
+
|
|
|
+ sendMsg.connect(url, Account);
|
|
|
+ setTimeout(async () => {
|
|
|
+ try {
|
|
|
+ // 构建消息参数
|
|
|
+ const params = JSON.stringify({
|
|
|
+ account: orderInfo.uid,
|
|
|
+ channel_id: orderInfo.channel_id,
|
|
|
+ order: orderInfo.order_id,
|
|
|
+ id: orderInfo.product_id,
|
|
|
+ cnt: 100,
|
|
|
+ money: orderInfo.amount,
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.info("通知游戏服务器参数", {data: params});
|
|
|
+ logger.info("通知游戏服务器orderInfo", {orderInfo: orderInfo});
|
|
|
+ let send_res = sendMsg.CG_ASK_LOGIN(
|
|
|
+ Account,
|
|
|
+ 0,
|
|
|
+ "",
|
|
|
+ "cn",
|
|
|
+ "CN",
|
|
|
+ ip,
|
|
|
+ params,
|
|
|
+ orderInfo.server_id
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!send_res) {
|
|
|
+ resolve({code: 0, msg: "通知服务器失败"});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新订单状态
|
|
|
+ const update_time = formatDate(new Date());
|
|
|
+ const res = await Order.updateOrderStats(
|
|
|
+ orderInfo.order_id,
|
|
|
+ 2,
|
|
|
+ out_trade_no,
|
|
|
+ update_time,
|
|
|
+ orderInfo.uid
|
|
|
+ );
|
|
|
+
|
|
|
+ if (res.affectedRows <= 0) {
|
|
|
+ logger.info(`订单${orderInfo.order_id} 发货失败`);
|
|
|
+ resolve({code: 0, msg: "发货失败"});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ resolve({code: 1, msg: "发货成功"});
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("发货过程出错:", error);
|
|
|
+ resolve({code: 0, msg: "发货失败"});
|
|
|
+ }
|
|
|
+ }, 1500);
|
|
|
+ });
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 签名算法2验证
|
|
|
- * @param data 请求数据
|
|
|
- * @param config 渠道配置
|
|
|
- * @returns 验证结果
|
|
|
- */
|
|
|
- private verifySignatureAlgorithm2(data: any, config: ChannelConfig): boolean {
|
|
|
- return this.verifyCommonSignature(
|
|
|
- data,
|
|
|
- config.loginConfig?.statApiKey || '',
|
|
|
- 'sign',
|
|
|
- ['sign'],
|
|
|
- false // 角色信息签名不需要URL编码
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成角色信息修改签名(供客户端使用)
|
|
|
- * @param params 签名参数
|
|
|
- * @param config 渠道配置
|
|
|
- * @returns 签名结果
|
|
|
- */
|
|
|
- generateRoleInfoSignature(params: any, config: ChannelConfig): string | null {
|
|
|
- try {
|
|
|
- return this.generateCommonSignature(
|
|
|
- params,
|
|
|
- config.loginConfig?.statApiKey || '',
|
|
|
- ['sign'],
|
|
|
- false // 角色信息签名不需要URL编码
|
|
|
- );
|
|
|
- } catch (error) {
|
|
|
- logger.error("生成角色信息修改签名出错:", error);
|
|
|
- return null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通用签名生成函数
|
|
|
+ * @param params 参数对象
|
|
|
+ * @param privateKey 私钥
|
|
|
+ * @param excludeKeys 需要排除的键(如sign)
|
|
|
+ * @param needUrlEncode 是否需要URL编码
|
|
|
+ * @returns 签名结果
|
|
|
+ */
|
|
|
+ public generateCommonSignature(
|
|
|
+ params: any,
|
|
|
+ privateKey: string,
|
|
|
+ excludeKeys: string[] = ['sign'],
|
|
|
+ needUrlEncode: boolean = true
|
|
|
+ ): string {
|
|
|
+ try {
|
|
|
+ // 1. 过滤掉空值和排除的键
|
|
|
+ const filteredParams: any = {};
|
|
|
+ Object.keys(params).forEach(key => {
|
|
|
+ if (!excludeKeys.includes(key) && params[key] !== null && params[key] !== undefined && params[key] !== '') {
|
|
|
+ filteredParams[key] = params[key].toString();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 按字母顺序排序并构建查询字符串
|
|
|
+ const sortedKeys = Object.keys(filteredParams).sort();
|
|
|
+ const queryString = sortedKeys.map(key => {
|
|
|
+ const value = filteredParams[key];
|
|
|
+ const encodedValue = needUrlEncode ? encodeURIComponent(value) : value;
|
|
|
+ return `${key}=${encodedValue}`;
|
|
|
+ }).join('&');
|
|
|
+
|
|
|
+ // 3. 拼接私钥
|
|
|
+ const stringToSign = `${queryString}&key=${privateKey}`;
|
|
|
+ console.log("String to Sign:", stringToSign);
|
|
|
+ // 4. 计算MD5并转大写
|
|
|
+ const signature = CryptoJS.MD5(stringToSign).toString().toUpperCase();
|
|
|
+
|
|
|
+ logger.info("通用签名生成详情:", {
|
|
|
+ filteredParams,
|
|
|
+ queryString,
|
|
|
+ stringToSign,
|
|
|
+ signature
|
|
|
+ });
|
|
|
+
|
|
|
+ return signature;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("通用签名生成出错:", error);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 签名算法1验证
|
|
|
- * @param data 请求数据
|
|
|
- * @param config 渠道配置
|
|
|
- * @returns 验证结果
|
|
|
- */
|
|
|
- private verifySignatureAlgorithm1(data: any, config: ChannelConfig): boolean {
|
|
|
- return this.verifyCommonSignature(
|
|
|
- data,
|
|
|
- config.loginConfig?.apiKey || '',
|
|
|
- 'signature',
|
|
|
- ['signature'],
|
|
|
- false // 内容安全签名不需要URL编码
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成内容安全签名(供客户端使用)
|
|
|
- * @param params 签名参数
|
|
|
- * @param config 渠道配置
|
|
|
- * @returns 签名结果
|
|
|
- */
|
|
|
- generateContentSecuritySignature(params: any, config: ChannelConfig): string | null {
|
|
|
- try {
|
|
|
- return this.generateCommonSignature(
|
|
|
- params,
|
|
|
- config.loginConfig?.apiKey || '',
|
|
|
- ['signature'],
|
|
|
- false // 内容安全签名不需要URL编码
|
|
|
- );
|
|
|
- } catch (error) {
|
|
|
- logger.error("生成内容安全签名出错:", error);
|
|
|
- return null;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序签名函数(签名算法3)- 兼容旧接口
|
|
|
+ * @param parameters 参数数组
|
|
|
+ * @param privateKey 私钥
|
|
|
+ * @returns 签名结果
|
|
|
+ */
|
|
|
+ private generateSignature(parameters: string[], privateKey: string): string {
|
|
|
+ // 将参数数组转换为对象
|
|
|
+ const params: any = {};
|
|
|
+ parameters.forEach(param => {
|
|
|
+ const [key, value] = param.split('=');
|
|
|
+ if (key && value) {
|
|
|
+ params[key] = value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return this.generateCommonSignature(params, privateKey, ['sign'], true);
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 发货处理方法
|
|
|
- * @param orderInfo 订单信息
|
|
|
- * @param ip 客户端IP
|
|
|
- * @param url 游戏服务器URL
|
|
|
- * @param out_trade_no 外部交易号
|
|
|
- * @returns 发货结果
|
|
|
- */
|
|
|
- private async deliverOrder(orderInfo: any, ip: string, url: string, out_trade_no: string): Promise<{ code: number; msg: string }> {
|
|
|
- return new Promise((resolve) => {
|
|
|
- const sendMsg = new Msg();
|
|
|
- logger.info("通知游戏服务器url", { url: url });
|
|
|
-
|
|
|
- sendMsg.connect(url, Account);
|
|
|
- setTimeout(async () => {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通用签名验证函数
|
|
|
+ * @param data 请求数据
|
|
|
+ * @param privateKey 私钥
|
|
|
+ * @param signKey 签名字段名
|
|
|
+ * @param excludeKeys 需要排除的键
|
|
|
+ * @param needUrlEncode 是否需要URL编码
|
|
|
+ * @returns 验证结果
|
|
|
+ */
|
|
|
+ private verifyCommonSignature(
|
|
|
+ data: any,
|
|
|
+ privateKey: string,
|
|
|
+ signKey: string = 'sign',
|
|
|
+ excludeKeys: string[] = ['sign'],
|
|
|
+ needUrlEncode: boolean = true
|
|
|
+ ): boolean {
|
|
|
try {
|
|
|
- // 构建消息参数
|
|
|
- const params = JSON.stringify({
|
|
|
- account: orderInfo.uid,
|
|
|
- channel_id: orderInfo.channel_id,
|
|
|
- order: orderInfo.order_id,
|
|
|
- id: orderInfo.product_id,
|
|
|
- cnt: 100,
|
|
|
- money: orderInfo.amount,
|
|
|
- });
|
|
|
-
|
|
|
- logger.info("通知游戏服务器参数", { data: params });
|
|
|
- logger.info("通知游戏服务器orderInfo", { orderInfo: orderInfo });
|
|
|
- let send_res = sendMsg.CG_ASK_LOGIN(
|
|
|
- Account,
|
|
|
- 0,
|
|
|
- "",
|
|
|
- "cn",
|
|
|
- "CN",
|
|
|
- ip,
|
|
|
- params,
|
|
|
- orderInfo.server_id
|
|
|
- );
|
|
|
-
|
|
|
- if (!send_res) {
|
|
|
- resolve({ code: 0, msg: "通知服务器失败" });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 更新订单状态
|
|
|
- const update_time = formatDate(new Date());
|
|
|
- const res = await Order.updateOrderStats(
|
|
|
- orderInfo.order_id,
|
|
|
- 2,
|
|
|
- out_trade_no,
|
|
|
- update_time,
|
|
|
- orderInfo.uid
|
|
|
- );
|
|
|
-
|
|
|
- if (res.affectedRows <= 0) {
|
|
|
- logger.info(`订单${orderInfo.order_id} 发货失败`);
|
|
|
- resolve({ code: 0, msg: "发货失败" });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- resolve({ code: 1, msg: "发货成功" });
|
|
|
+ const receivedSignature = data[signKey];
|
|
|
+ if (!receivedSignature) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const expectedSignature = this.generateCommonSignature(data, privateKey, excludeKeys, needUrlEncode);
|
|
|
+
|
|
|
+ logger.info("通用签名验证详情:", {
|
|
|
+ receivedSignature,
|
|
|
+ expectedSignature,
|
|
|
+ isValid: receivedSignature === expectedSignature
|
|
|
+ });
|
|
|
+
|
|
|
+ return receivedSignature === expectedSignature;
|
|
|
} catch (error) {
|
|
|
- logger.error("发货过程出错:", error);
|
|
|
- resolve({ code: 0, msg: "发货失败" });
|
|
|
+ logger.error("通用签名验证出错:", error);
|
|
|
+ return false;
|
|
|
}
|
|
|
- }, 1500);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 通用签名生成函数
|
|
|
- * @param params 参数对象
|
|
|
- * @param privateKey 私钥
|
|
|
- * @param excludeKeys 需要排除的键(如sign)
|
|
|
- * @param needUrlEncode 是否需要URL编码
|
|
|
- * @returns 签名结果
|
|
|
- */
|
|
|
- public generateCommonSignature(
|
|
|
- params: any,
|
|
|
- privateKey: string,
|
|
|
- excludeKeys: string[] = ['sign'],
|
|
|
- needUrlEncode: boolean = true
|
|
|
- ): string {
|
|
|
- try {
|
|
|
- // 1. 过滤掉空值和排除的键
|
|
|
- const filteredParams: any = {};
|
|
|
- Object.keys(params).forEach(key => {
|
|
|
- if (!excludeKeys.includes(key) && params[key] !== null && params[key] !== undefined && params[key] !== '') {
|
|
|
- filteredParams[key] = params[key].toString();
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 2. 按字母顺序排序并构建查询字符串
|
|
|
- const sortedKeys = Object.keys(filteredParams).sort();
|
|
|
- const queryString = sortedKeys.map(key => {
|
|
|
- const value = filteredParams[key];
|
|
|
- const encodedValue = needUrlEncode ? encodeURIComponent(value) : value;
|
|
|
- return `${key}=${encodedValue}`;
|
|
|
- }).join('&');
|
|
|
-
|
|
|
- // 3. 拼接私钥
|
|
|
- const stringToSign = `${queryString}&key=${privateKey}`;
|
|
|
-
|
|
|
- // 4. 计算MD5并转大写
|
|
|
- const signature = CryptoJS.MD5(stringToSign).toString().toUpperCase();
|
|
|
-
|
|
|
- logger.info("通用签名生成详情:", {
|
|
|
- filteredParams,
|
|
|
- queryString,
|
|
|
- stringToSign,
|
|
|
- signature
|
|
|
- });
|
|
|
-
|
|
|
- return signature;
|
|
|
- } catch (error) {
|
|
|
- logger.error("通用签名生成出错:", error);
|
|
|
- return '';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 小程序签名函数(签名算法3)- 兼容旧接口
|
|
|
- * @param parameters 参数数组
|
|
|
- * @param privateKey 私钥
|
|
|
- * @returns 签名结果
|
|
|
- */
|
|
|
- private generateSignature(parameters: string[], privateKey: string): string {
|
|
|
- // 将参数数组转换为对象
|
|
|
- const params: any = {};
|
|
|
- parameters.forEach(param => {
|
|
|
- const [key, value] = param.split('=');
|
|
|
- if (key && value) {
|
|
|
- params[key] = value;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- return this.generateCommonSignature(params, privateKey, ['sign'], true);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 通用签名验证函数
|
|
|
- * @param data 请求数据
|
|
|
- * @param privateKey 私钥
|
|
|
- * @param signKey 签名字段名
|
|
|
- * @param excludeKeys 需要排除的键
|
|
|
- * @param needUrlEncode 是否需要URL编码
|
|
|
- * @returns 验证结果
|
|
|
- */
|
|
|
- private verifyCommonSignature(
|
|
|
- data: any,
|
|
|
- privateKey: string,
|
|
|
- signKey: string = 'sign',
|
|
|
- excludeKeys: string[] = ['sign'],
|
|
|
- needUrlEncode: boolean = true
|
|
|
- ): boolean {
|
|
|
- try {
|
|
|
- const receivedSignature = data[signKey];
|
|
|
- if (!receivedSignature) {
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- const expectedSignature = this.generateCommonSignature(data, privateKey, excludeKeys, needUrlEncode);
|
|
|
-
|
|
|
- logger.info("通用签名验证详情:", {
|
|
|
- receivedSignature,
|
|
|
- expectedSignature,
|
|
|
- isValid: receivedSignature === expectedSignature
|
|
|
- });
|
|
|
-
|
|
|
- return receivedSignature === expectedSignature;
|
|
|
- } catch (error) {
|
|
|
- logger.error("通用签名验证出错:", error);
|
|
|
- return false;
|
|
|
}
|
|
|
- }
|
|
|
}
|