|
|
@@ -0,0 +1,498 @@
|
|
|
+import { Context } from "koa";
|
|
|
+import { ChannelHandler, LoginResult, PaymentResult } from "../interfaces/ChannelHandler";
|
|
|
+import { ChannelConfig } from "../../config/channelConfig";
|
|
|
+import axios from "axios";
|
|
|
+
|
|
|
+const logger = require("../../utils/log");
|
|
|
+
|
|
|
+/**
|
|
|
+ * 三一iOS渠道处理器
|
|
|
+ * 渠道ID: 12
|
|
|
+ * 处理iOS平台的登录鉴权和支付回调
|
|
|
+ */
|
|
|
+export class SYIOSChannelHandler 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'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * iOS登录鉴权
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async handleLogin(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
|
|
|
+ try {
|
|
|
+ const { userToken, gameid } = ctx.request.body || ctx.request.query;
|
|
|
+
|
|
|
+ // 验证必要参数
|
|
|
+ if (!userToken) {
|
|
|
+ logger.error("iOS登录鉴权失败 - 缺少userToken参数");
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "缺少必要参数: userToken",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!gameid) {
|
|
|
+ logger.error("iOS登录鉴权失败 - 缺少gameid参数");
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "缺少必要参数: gameid",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("iOS登录鉴权请求参数:", { userToken, gameid });
|
|
|
+
|
|
|
+ // 调用第三方鉴权API
|
|
|
+ const apiUrl = `https://api.11h5.com/login?cmd=checkUserToken&userToken=${encodeURIComponent(userToken)}&gameid=${gameid}`;
|
|
|
+
|
|
|
+ logger.info("调用iOS鉴权API:", { url: apiUrl });
|
|
|
+
|
|
|
+ const response = await axios.get(apiUrl, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: this.getCommonHeaders(),
|
|
|
+ httpsAgent: this.getHttpsAgent()
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.info("iOS鉴权API响应:", response.data);
|
|
|
+
|
|
|
+ // 根据错误码判断成功与否
|
|
|
+ const responseData = response.data;
|
|
|
+
|
|
|
+ if (responseData.error === 0) {
|
|
|
+ // 成功:error = 0
|
|
|
+ logger.info("iOS登录鉴权成功:", {
|
|
|
+ uid: responseData.uid,
|
|
|
+ nickname: responseData.nickname,
|
|
|
+ usertype: responseData.usertype
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ code: 1,
|
|
|
+ msg: "登录成功",
|
|
|
+ data: {
|
|
|
+ uid: responseData.uid,
|
|
|
+ nickname: responseData.nickname,
|
|
|
+ headimgurl: responseData.headimgurl,
|
|
|
+ sex: responseData.sex,
|
|
|
+ focus: responseData.focus,
|
|
|
+ usertype: responseData.usertype
|
|
|
+ }
|
|
|
+ };
|
|
|
+ } else if (responseData.error === 401) {
|
|
|
+ // 参数有误
|
|
|
+ logger.error("iOS登录鉴权失败 - 参数有误:", responseData);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "参数有误,请检查接口所需参数是否都已经填写正确",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ } else if (responseData.error === 403) {
|
|
|
+ // token验证失败
|
|
|
+ logger.error("iOS登录鉴权失败 - token验证失败:", responseData);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "token验证失败,同一个userToken只能验证一次,可通过刷新游戏重新获取",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ // 其他错误
|
|
|
+ logger.error("iOS登录鉴权失败 - 未知错误:", responseData);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "鉴权失败,请稍后重试",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("iOS登录鉴权出错:", error);
|
|
|
+ return {
|
|
|
+ code: -1,
|
|
|
+ msg: "登录鉴权失败",
|
|
|
+ data: null
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * iOS支付回调处理
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async handlePayment(ctx: Context, config: ChannelConfig): Promise<PaymentResult> {
|
|
|
+ try {
|
|
|
+ const data = ctx.request.query || ctx.request.body;
|
|
|
+ logger.info("iOS支付回调参数:", data);
|
|
|
+
|
|
|
+ // 验证必要参数(根据API文档)
|
|
|
+ const requiredParams = ['openid', 'rmb', 'reqid', 'trans_id', 'product_id', 'notify_id', 'sign'];
|
|
|
+ for (const param of requiredParams) {
|
|
|
+ if (!data[param]) {
|
|
|
+ logger.error(`iOS支付回调缺少必要参数: ${param}`);
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: `缺少必要参数: ${param}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证签名(使用签名算法3)
|
|
|
+ if (!this.verifyPaymentSignature(data, config)) {
|
|
|
+ logger.error("iOS支付回调签名验证失败");
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "签名验证失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取notify_id进行订单校验
|
|
|
+ const notify_id = data.notify_id;
|
|
|
+ const gameid = data.product_id || config.paymentConfig?.gameId || '1540';
|
|
|
+
|
|
|
+ logger.info(`开始订单校验 - notify_id: ${notify_id}, gameid: ${gameid}`);
|
|
|
+
|
|
|
+ // 调用订单校验
|
|
|
+ const verifyResult = await this.verifyOrderInternal(gameid, notify_id, config);
|
|
|
+
|
|
|
+ if (!verifyResult.success) {
|
|
|
+ logger.error("订单校验失败:", verifyResult.message);
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: `订单校验失败: ${verifyResult.message}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("订单校验成功,开始处理支付发货逻辑");
|
|
|
+
|
|
|
+ // 处理支付成功逻辑
|
|
|
+ logger.info(`iOS支付成功 - 用户: ${data.openid}, 金额: ${data.rmb}, 订单: ${data.reqid}`);
|
|
|
+
|
|
|
+ // 返回SUCCESS表示处理成功(根据API文档要求)
|
|
|
+ ctx.body = "SUCCESS";
|
|
|
+
|
|
|
+ return {
|
|
|
+ code: 1,
|
|
|
+ msg: "支付处理成功",
|
|
|
+ data: {
|
|
|
+ openid: data.openid,
|
|
|
+ rmb: data.rmb,
|
|
|
+ reqid: data.reqid,
|
|
|
+ trans_id: data.trans_id,
|
|
|
+ product_id: data.product_id,
|
|
|
+ notify_id: data.notify_id,
|
|
|
+ userdata: data.userdata,
|
|
|
+ txid: data.txid,
|
|
|
+ product_count: data.product_count
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("iOS支付回调处理出错:", error);
|
|
|
+ return {
|
|
|
+ code: 0,
|
|
|
+ msg: "支付处理失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证支付签名(签名算法3)
|
|
|
+ * @param data 支付数据
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 验证结果
|
|
|
+ */
|
|
|
+ private verifyPaymentSignature(data: any, config: ChannelConfig): boolean {
|
|
|
+ try {
|
|
|
+ const privateKey = config.paymentConfig?.signKey || '';
|
|
|
+ if (!privateKey) {
|
|
|
+ logger.error("iOS支付签名验证失败 - 缺少私钥");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 除去为空的参数
|
|
|
+ const filteredParams: any = {};
|
|
|
+ Object.keys(data).forEach(key => {
|
|
|
+ if (key !== 'sign' && data[key] !== null && data[key] !== undefined && data[key] !== '') {
|
|
|
+ filteredParams[key] = data[key].toString();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 对参数名按字母顺序排序
|
|
|
+ const sortedKeys = Object.keys(filteredParams).sort();
|
|
|
+
|
|
|
+ // 3. 构建待签名字符串
|
|
|
+ const queryString = sortedKeys.map(key => {
|
|
|
+ return `${key}=${filteredParams[key]}`;
|
|
|
+ }).join('&');
|
|
|
+
|
|
|
+ // 4. 拼接私钥
|
|
|
+ const stringToSign = `${queryString}&key=${privateKey}`;
|
|
|
+
|
|
|
+ // 5. 计算MD5签名并转大写
|
|
|
+ const crypto = require('crypto');
|
|
|
+ const expectedSignature = crypto.createHash('md5').update(stringToSign).digest('hex').toUpperCase();
|
|
|
+ const actualSignature = data.sign;
|
|
|
+
|
|
|
+ logger.info("iOS支付签名验证详情:", {
|
|
|
+ filteredParams,
|
|
|
+ queryString,
|
|
|
+ stringToSign,
|
|
|
+ expectedSignature,
|
|
|
+ actualSignature,
|
|
|
+ isValid: expectedSignature === actualSignature
|
|
|
+ });
|
|
|
+
|
|
|
+ return expectedSignature === actualSignature;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("iOS支付签名验证出错:", error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成支付签名(签名算法3)
|
|
|
+ * @param params 签名参数
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 签名字符串
|
|
|
+ */
|
|
|
+ private generatePaymentSignature(params: any, config: ChannelConfig): string {
|
|
|
+ try {
|
|
|
+ const privateKey = config.paymentConfig?.signKey || '';
|
|
|
+ if (!privateKey) {
|
|
|
+ logger.error("iOS支付签名生成失败 - 缺少私钥");
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 除去为空的参数
|
|
|
+ const filteredParams: any = {};
|
|
|
+ Object.keys(params).forEach(key => {
|
|
|
+ if (key !== 'sign' && params[key] !== null && params[key] !== undefined && params[key] !== '') {
|
|
|
+ filteredParams[key] = params[key].toString();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 对参数名按字母顺序排序
|
|
|
+ const sortedKeys = Object.keys(filteredParams).sort();
|
|
|
+
|
|
|
+ // 3. 构建待签名字符串
|
|
|
+ const queryString = sortedKeys.map(key => {
|
|
|
+ return `${key}=${filteredParams[key]}`;
|
|
|
+ }).join('&');
|
|
|
+
|
|
|
+ // 4. 拼接私钥
|
|
|
+ const stringToSign = `${queryString}&key=${privateKey}`;
|
|
|
+
|
|
|
+ // 5. 计算MD5签名并转大写
|
|
|
+ const crypto = require('crypto');
|
|
|
+ const signature = crypto.createHash('md5').update(stringToSign).digest('hex').toUpperCase();
|
|
|
+
|
|
|
+ logger.info("iOS支付签名生成详情:", {
|
|
|
+ filteredParams,
|
|
|
+ queryString,
|
|
|
+ stringToSign,
|
|
|
+ signature
|
|
|
+ });
|
|
|
+
|
|
|
+ return signature;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("iOS支付签名生成出错:", error);
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 内部订单校验方法(不依赖Koa上下文)
|
|
|
+ * @param gameid 游戏ID
|
|
|
+ * @param notify_id 平台通知ID
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ private async verifyOrderInternal(gameid: string, notify_id: string, config: ChannelConfig): Promise<{ success: boolean; message: string }> {
|
|
|
+ try {
|
|
|
+ // 生成订单校验签名
|
|
|
+ const sign = this.generateOrderVerifySignature(gameid, notify_id, config);
|
|
|
+
|
|
|
+ if (!sign) {
|
|
|
+ logger.error("订单校验签名生成失败");
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "签名生成失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("订单校验请求参数:", { gameid, notify_id, sign });
|
|
|
+
|
|
|
+ // 调用第三方订单校验API
|
|
|
+ const apiUrl = `https://login.11h5.com/pay/paygate/verify.php?gameid=${gameid}¬ify_id=${encodeURIComponent(notify_id)}&sign=${encodeURIComponent(sign)}`;
|
|
|
+
|
|
|
+ logger.info("调用订单校验API:", { url: apiUrl });
|
|
|
+
|
|
|
+ const response = await axios.get(apiUrl, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: this.getCommonHeaders(),
|
|
|
+ httpsAgent: this.getHttpsAgent()
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.info("订单校验API响应:", response.data);
|
|
|
+
|
|
|
+ // 检查响应结果
|
|
|
+ const responseText = response.data.toString().trim();
|
|
|
+ if (responseText === "SUCCESS") {
|
|
|
+ logger.info("订单校验成功:", { notify_id, gameid });
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: "订单校验成功"
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ logger.warn("订单校验失败:", { notify_id, gameid, response: responseText });
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `订单校验失败: ${responseText}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("订单校验出错:", error);
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "订单校验失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 订单校验(公开接口)
|
|
|
+ * @param ctx Koa上下文
|
|
|
+ * @param config 渠道配置
|
|
|
+ */
|
|
|
+ async verifyOrder(ctx: Context, config: ChannelConfig): Promise<{ success: boolean; message: string }> {
|
|
|
+ try {
|
|
|
+ const { gameid, notify_id, sign } = ctx.request.query || ctx.request.body;
|
|
|
+
|
|
|
+ // 验证必要参数
|
|
|
+ if (!gameid) {
|
|
|
+ logger.error("订单校验失败 - 缺少gameid参数");
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "缺少必要参数: gameid"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!notify_id) {
|
|
|
+ logger.error("订单校验失败 - 缺少notify_id参数");
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "缺少必要参数: notify_id"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!sign) {
|
|
|
+ logger.error("订单校验失败 - 缺少sign参数");
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "缺少必要参数: sign"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("订单校验请求参数:", { gameid, notify_id, sign });
|
|
|
+
|
|
|
+ // 验证签名
|
|
|
+ const verifyData = { gameid, notify_id, sign };
|
|
|
+ if (!this.verifyPaymentSignature(verifyData, config)) {
|
|
|
+ logger.error("订单校验签名验证失败");
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "签名验证失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用第三方订单校验API
|
|
|
+ const apiUrl = `https://login.11h5.com/pay/paygate/verify.php?gameid=${gameid}¬ify_id=${encodeURIComponent(notify_id)}&sign=${encodeURIComponent(sign)}`;
|
|
|
+
|
|
|
+ logger.info("调用订单校验API:", { url: apiUrl });
|
|
|
+
|
|
|
+ const response = await axios.get(apiUrl, {
|
|
|
+ timeout: 10000,
|
|
|
+ headers: this.getCommonHeaders(),
|
|
|
+ httpsAgent: this.getHttpsAgent()
|
|
|
+ });
|
|
|
+
|
|
|
+ logger.info("订单校验API响应:", response.data);
|
|
|
+
|
|
|
+ // 检查响应结果
|
|
|
+ const responseText = response.data.toString().trim();
|
|
|
+ if (responseText === "SUCCESS") {
|
|
|
+ logger.info("订单校验成功:", { notify_id, gameid });
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ message: "订单校验成功"
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ logger.warn("订单校验失败:", { notify_id, gameid, response: responseText });
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: `订单校验失败: ${responseText}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("订单校验出错:", error);
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ message: "订单校验失败"
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成订单校验签名
|
|
|
+ * @param gameid 游戏ID
|
|
|
+ * @param notify_id 平台通知ID
|
|
|
+ * @param config 渠道配置
|
|
|
+ * @returns 签名字符串
|
|
|
+ */
|
|
|
+ private generateOrderVerifySignature(gameid: string, notify_id: string, config: ChannelConfig): string {
|
|
|
+ try {
|
|
|
+ const privateKey = config.paymentConfig?.signKey || '';
|
|
|
+ if (!privateKey) {
|
|
|
+ logger.error("订单校验签名生成失败 - 缺少私钥");
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建参数对象
|
|
|
+ const params = {
|
|
|
+ gameid: gameid,
|
|
|
+ notify_id: notify_id
|
|
|
+ };
|
|
|
+
|
|
|
+ // 使用签名算法3生成签名
|
|
|
+ return this.generatePaymentSignature(params, config);
|
|
|
+ } catch (error) {
|
|
|
+ logger.error("订单校验签名生成出错:", error);
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|