Kaynağa Gözat

feat:华为sdk

pig flower 5 ay önce
ebeveyn
işleme
a8bb54862f

+ 2 - 0
webServer/src/channels/factory/ChannelFactory.ts

@@ -14,6 +14,7 @@ import { MianyouChannelHandler } from "../handlers/MianyouChannelHandler";
 import { HupuChannelHandler } from "../handlers/HupuChannelHandler";
 import { SevenTwoZeroChannelHandler } from "../handlers/SevenTwoZeroChannelHandler";
 import { MeituanChannelHandler } from "../handlers/MeituanChannelHandler";
+import { HuaweiChannelHandler } from "../handlers/HuaweiChannelHandler";
 
 
 const logger = require("../../utils/log");
@@ -51,6 +52,7 @@ class ChannelFactory {
     this.registerHandler(14, new HupuChannelHandler()); // 虎扑渠道
     this.registerHandler(15, new SevenTwoZeroChannelHandler()); // 720渠道
     this.registerHandler(16, new MeituanChannelHandler()); // 美团渠道
+    this.registerHandler(17, new HuaweiChannelHandler()); // 华为渠道
   }
 
   /**

+ 439 - 0
webServer/src/channels/handlers/HuaweiChannelHandler.ts

@@ -0,0 +1,439 @@
+import {Context} from "koa";
+import axios from "axios";
+import * as crypto from 'crypto';
+import {ChannelHandler, LoginResult, PaymentResult} from "../interfaces/ChannelHandler";
+import {ChannelConfig} from "../../config/channelConfig";
+import {PaymentHelper} from "../../utils/PaymentHelper";
+
+const logger = require("../../utils/log");
+
+/**
+ * 华为渠道处理器
+ * 负责登录验证与支付回调处理
+ */
+export class HuaweiChannelHandler implements ChannelHandler {
+    /**
+     * 获取HTTPS代理配置
+     * @returns HTTPS代理配置
+     */
+    private getHttpsAgent() {
+        const https = require('https');
+        return new https.Agent({
+            rejectUnauthorized: false,
+            secureProtocol: 'TLSv1_2_method',
+            timeout: 10000
+        });
+    }
+
+    /**
+     * 生成签名
+     * 根据文档:移除空值参数 -> 按字母顺序排序 -> 拼接参数 -> 拼接secret -> MD5
+     * @param params 请求参数对象
+     * @param secret 应用密钥
+     * @returns MD5签名字符串(32位小写)
+     */
+    private generateSign(params: any, secret: string): string {
+        try {
+            // 1. 移除空值参数
+            const filteredParams: any = {};
+            Object.keys(params).forEach(key => {
+                if (params[key] !== null && params[key] !== undefined && params[key] !== '') {
+                    filteredParams[key] = params[key].toString();
+                }
+            });
+
+            // 2. 按字母顺序排序
+            const sortedKeys = Object.keys(filteredParams).sort();
+
+            // 3. 构建签名字符串:key=value&key=value
+            const queryString = sortedKeys.map(key => {
+                return `${key}=${filteredParams[key]}`;
+            }).join('&');
+
+            // 4. 拼接secret key
+            const signStr = `${queryString}&key=${secret}`;
+
+            logger.info("华为渠道签名生成详情:", {
+                filteredParams,
+                queryString,
+                signStr
+            });
+
+            // 5. MD5哈希(32位小写)
+            const signature = crypto.createHash('md5').update(signStr).digest('hex').toLowerCase();
+
+            return signature;
+        } catch (error) {
+            logger.error("华为渠道签名生成出错:", error);
+            return '';
+        }
+    }
+
+    /**
+     * 华为渠道登录鉴权
+     * @param ctx Koa上下文
+     * @param config 渠道配置
+     * @returns 登录结果
+     */
+    async handleLogin(ctx: Context, config: ChannelConfig): Promise<LoginResult> {
+        try {
+            const data = ctx.request.body as any;
+            const { token } = data;
+
+            // 验证必要参数
+            if (!token) {
+                logger.error("华为渠道登录鉴权失败 - 缺少token参数");
+                return {
+                    code: -1,
+                    msg: "缺少必要参数: token",
+                    data: null
+                };
+            }
+
+            // 获取配置
+            const appId = config.loginConfig?.appId;
+            const appSecret = config.loginConfig?.appSecret;
+
+            if (!appId || !appSecret) {
+                logger.error("华为渠道登录鉴权失败 - 缺少配置参数", { appId, appSecret: !!appSecret });
+                return {
+                    code: -1,
+                    msg: "服务器配置错误: 缺少appId或appSecret",
+                    data: null
+                };
+            }
+
+            // 构建请求参数
+            const timestamp = Math.floor(Date.now() / 1000).toString();
+            const requestBody = {
+                appId: appId,
+                timestamp: timestamp
+            };
+
+            // 生成签名
+            const sign = this.generateSign(requestBody, appSecret);
+
+            if (!sign) {
+                logger.error("华为渠道登录鉴权失败 - 签名生成失败");
+                return {
+                    code: -1,
+                    msg: "签名生成失败",
+                    data: null
+                };
+            }
+
+            // 构建请求头
+            const headers = {
+                'Accept': 'application/json;',
+                'Content-Type': 'application/json;charset=utf-8;',
+                'Authorization': token,
+                'sign': sign,
+                'Server-Plat-From': 'minigame_server' // 可选,但建议传递
+            };
+
+            // API地址
+            const apiUrl = 'https://sdkapi.haotianhuyu.com/api/user/getLoginStatus';
+
+            logger.info("华为渠道登录鉴权请求", {
+                url: apiUrl,
+                headers: { ...headers, Authorization: token ? '***' : undefined },
+                body: requestBody
+            });
+
+            // 发送请求
+            const response = await axios.post(apiUrl, requestBody, {
+                headers: headers,
+                timeout: 10000,
+                httpsAgent: this.getHttpsAgent(),
+                validateStatus: (status) => {
+                    // 根据文档,失败时HTTP状态码是401,但我们也接受200
+                    return status === 200 || status === 401;
+                }
+            });
+
+            logger.info("华为渠道登录鉴权响应", {
+                status: response.status,
+                data: response.data
+            });
+
+            const responseData = response.data;
+
+            // 判断登录结果
+            // 根据文档:status=20000 表示成功,其他都是失败
+            if (responseData.status === 20000) {
+                // 登录成功
+                logger.info("华为渠道登录鉴权成功", {
+                    user_id: responseData.data?.user_id,
+                    token: responseData.data?.token ? '***' : undefined
+                });
+
+                return {
+                    code: 0,
+                    msg: responseData.message || "登录成功",
+                    data: {
+                        token: responseData.data?.token,
+                        user_id: responseData.data?.user_id,
+                        ali_user_id: responseData.data?.ali_user_id,
+                        channel_open_id: responseData.data?.channel_open_id
+                    }
+                };
+            } else {
+                // 登录失败
+                logger.warn("华为渠道登录鉴权失败", {
+                    status: responseData.status,
+                    errCode: responseData.errCode,
+                    message: responseData.message,
+                    msg: responseData.msg
+                });
+
+                return {
+                    code: -1,
+                    msg: responseData.msg || responseData.message || "登录验证失败",
+                    data: null
+                };
+            }
+
+        } catch (error: any) {
+            logger.error("华为渠道登录鉴权出错:", error);
+            
+            // 处理axios错误
+            if (error.response) {
+                const responseData = error.response.data;
+                logger.error("华为渠道登录鉴权API错误响应", {
+                    status: error.response.status,
+                    data: responseData
+                });
+
+                // 如果返回了错误数据,尝试解析
+                if (responseData && responseData.status !== undefined) {
+                    return {
+                        code: -1,
+                        msg: responseData.msg || responseData.message || "登录验证失败",
+                        data: null
+                    };
+                }
+            }
+
+            return {
+                code: -1,
+                msg: error.message || "登录验证失败",
+                data: null
+            };
+        }
+    }
+
+    /**
+     * 验证支付回调签名
+     * 根据文档:移除sign参数和空值 -> 按ASCII码升序排序 -> 拼接成key=value格式 -> 拼接&key={callback secret} -> MD5
+     * @param data 回调参数对象
+     * @param callbackSecret 回调密钥
+     * @returns 验证结果
+     */
+    private verifyPaymentSign(data: any, callbackSecret: string): boolean {
+        try {
+            // 1. 移除sign参数和空值字段
+            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. 按ASCII码升序排序
+            const sortedKeys = Object.keys(filteredParams).sort();
+
+            // 3. 构建签名字符串:key1=value1&key2=value2
+            const queryString = sortedKeys.map(key => {
+                return `${key}=${filteredParams[key]}`;
+            }).join('&');
+
+            // 4. 拼接回调密钥
+            const signStr = `${queryString}&key=${callbackSecret}`;
+
+            logger.info("华为渠道支付回调签名验证详情:", {
+                filteredParams,
+                queryString,
+                signStr: signStr.substring(0, 100) + '...' // 只显示前100个字符
+            });
+
+            // 5. MD5哈希(32位小写)
+            const calculatedSign = crypto.createHash('md5').update(signStr).digest('hex').toLowerCase();
+            const receivedSign = (data.sign || '').toLowerCase();
+
+            const isValid = calculatedSign === receivedSign;
+
+            logger.info("华为渠道支付回调签名验证结果:", {
+                calculatedSign,
+                receivedSign,
+                isValid
+            });
+
+            return isValid;
+        } catch (error) {
+            logger.error("华为渠道支付回调签名验证出错:", error);
+            return false;
+        }
+    }
+
+    /**
+     * 华为渠道支付回调处理
+     * POST请求,处理支付成功后的发货通知
+     * @param ctx Koa上下文
+     * @param config 渠道配置
+     * @returns 支付结果(注意:返回格式为 {errCode: 0} 或 {errCode: 1, errMsg: "..."})
+     */
+    async handlePayment(ctx: Context, config: ChannelConfig): Promise<PaymentResult> {
+        const startTime = Date.now();
+        
+        try {
+            const data = ctx.request.body as any;
+            logger.info("华为渠道支付回调参数:", { url: ctx.href, params: data });
+
+            // 1. 验证必要参数
+            const requiredParams = ['app_id', 'user_id', 'server_id', 'role_id', 'cp_order_id', 
+                                   'cs_order_no', 'cs_trade_no', 'pay_amount', 'swap_coin_num', 
+                                   'order_create_time', 'order_pay_time', 'order_status', 'sign'];
+            
+            const missingParams = requiredParams.filter(param => !data[param]);
+            if (missingParams.length > 0) {
+                logger.warn("华为渠道支付回调失败: 缺少必要参数", { missingParams });
+                return {
+                    code: 1,
+                    msg: `缺少必要参数: ${missingParams.join(', ')}`
+                };
+            }
+
+            // 2. 验证订单状态(只有SUCCESS状态才发货)
+            if (data.order_status !== 'SUCCESS') {
+                logger.warn("华为渠道支付回调失败: 订单状态非成功", { order_status: data.order_status });
+                return {
+                    code: 1,
+                    msg: `订单状态非成功: ${data.order_status}`
+                };
+            }
+
+            // 3. 获取回调密钥并验证签名
+            const callbackSecret = config.paymentConfig?.callbackKey;
+            if (!callbackSecret) {
+                logger.error("华为渠道支付回调失败: 未配置回调密钥");
+                return {
+                    code: 1,
+                    msg: "服务器配置错误: 未配置回调密钥"
+                };
+            }
+
+            // 验证签名(必须验证,只有签名正确的订单才可发货)
+            if (!this.verifyPaymentSign(data, callbackSecret)) {
+                logger.error("华为渠道支付回调失败: 签名验证失败");
+                return {
+                    code: 1,
+                    msg: "签名验证失败"
+                };
+            }
+
+            // 4. 验证订单(使用cp_order_id作为订单ID)
+            const orderId = data.cp_order_id;
+            const validation = await PaymentHelper.validateOrder(orderId);
+            
+            if (!validation.valid) {
+                // 如果是重复发货,需要返回成功响应,但不重复发货
+                if (validation.message?.includes("重复发货")) {
+                    logger.info("华为渠道支付回调: 订单已发货,返回成功响应", { orderId });
+                    return {
+                        code: 0,
+                        msg: "订单已发货"
+                    };
+                }
+                
+                logger.warn("华为渠道支付回调订单验证失败", {
+                    orderId,
+                    validation
+                });
+                return {
+                    code: 1,
+                    msg: validation.message || "订单验证失败"
+                };
+            }
+
+            const orderInfo = validation.orderInfo;
+
+            // 5. 验证金额(pay_amount单位是分,需要转换为元进行比较)
+            const paymentAmount = parseInt(data.pay_amount) / 100; // 转换为元
+            if (Math.abs(Number(orderInfo.amount) - paymentAmount) > 0.01) {
+                logger.warn("华为渠道支付回调金额不匹配", {
+                    orderId,
+                    orderAmount: orderInfo.amount,
+                    paymentAmount: paymentAmount
+                });
+                return {
+                    code: 1,
+                    msg: `订单金额不一致: 订单金额${orderInfo.amount}元,支付金额${paymentAmount}元`
+                };
+            }
+
+            // 6. 发货处理
+            logger.info(`华为渠道支付订单${orderId}开始发货`, {
+                cs_order_no: data.cs_order_no,
+                cs_trade_no: data.cs_trade_no,
+                amount: paymentAmount,
+                swap_coin_num: data.swap_coin_num
+            });
+
+            const result = await PaymentHelper.deliverOrder(
+                orderInfo,
+                ctx.request.ip,
+                validation.url,
+                data.cs_order_no // 使用传盛订单号作为out_trade_no
+            );
+
+            // 检查是否超时(必须在7秒内响应)
+            const elapsedTime = Date.now() - startTime;
+            if (elapsedTime > 7000) {
+                logger.warn("华为渠道支付回调响应超时", {
+                    orderId,
+                    elapsedTime: `${elapsedTime}ms`
+                });
+            }
+
+            // 7. 返回响应(注意格式:成功返回 {errCode: 0},失败返回 {errCode: 1, errMsg: "..."})
+            // PaymentHelper.deliverOrder 成功返回 code=1,失败返回 code=0
+            // 华为渠道要求:成功返回 errCode=0,失败返回 errCode=1
+            if (result.code === 1) {
+                // 发货成功
+                logger.info(`华为渠道支付订单${orderId}发货成功`, {
+                    elapsedTime: `${elapsedTime}ms`
+                });
+                // 返回 code=0 表示成功,ApiController会转换为 {errCode: 0}
+                return {
+                    code: 0,
+                    msg: "发货成功"
+                };
+            } else {
+                // 发货失败
+                logger.error(`华为渠道支付订单${orderId}发货失败`, {
+                    result,
+                    elapsedTime: `${elapsedTime}ms`
+                });
+                // 返回 code=1 表示失败,ApiController会转换为 {errCode: 1, errMsg: "..."}
+                return {
+                    code: 1,
+                    msg: `发放失败,原因:${result.msg || "未知错误"}`
+                };
+            }
+
+        } catch (error: any) {
+            const elapsedTime = Date.now() - startTime;
+            logger.error("华为渠道支付回调异常", { 
+                error: error.message, 
+                stack: error.stack,
+                elapsedTime: `${elapsedTime}ms`
+            });
+            
+            return {
+                code: 1,
+                msg: `发放失败,原因:${error.message || "未知错误"}`
+            };
+        }
+    }
+}
+

+ 8 - 5
webServer/src/channels/handlers/MiniappChannelHandler.ts

@@ -684,20 +684,23 @@ export class MiniappChannelHandler implements ChannelHandler {
 
             logger.info("抖音内容安全API响应:", response.data);
 
-            // 处理响应结果
+            // 处理抖音响应结果(抖音返回结构:{error: 0, msg: "ok", data: {}})
+            // error: 0 代表通过,其他错误码代表检测不通过
             if (response.data && response.data.error === 0) {
+                // 抖音API返回通过,返回统一的格式
                 return {
                     success: true,
                     error: 0,
-                    result: response.data.result,
-                    trace_id: response.data.trace_id,
-                    detail: response.data.detail
+                    result: {
+                        suggest: 'pass' // 抖音返回error=0表示通过
+                    }
                 };
             } else {
+                // 抖音API返回不通过
                 return {
                     success: false,
                     error: response.data?.error || -1,
-                    errmsg: response.data?.errmsg || "内容安全检测失败"
+                    errmsg: response.data?.msg || response.data?.errmsg || "内容安全检测失败"
                 };
             }
         } catch (error) {

+ 17 - 1
webServer/src/config/channelConfig.ts

@@ -39,7 +39,10 @@ import {
     MINI_APP_DOMAIN,
     MINI_APP_API_KEY,
     MEITUAN_APP_ID,
-    MEITUAN_APP_SECRET
+    MEITUAN_APP_SECRET,
+    HUAWEI_APP_ID,
+    HUAWEI_APP_SECRET,
+    HUAWEI_CALLBACK_SECRET
 } from "./thirdParams";
 
 // 渠道配置接口定义
@@ -286,4 +289,17 @@ export const channelConfigs: Record<number, ChannelConfig> = {
             appSecret: MEITUAN_APP_SECRET,
         },
     },
+    17: {
+        // 华为渠道
+        channelId: 17,
+        name: "华为",
+        platform: "hwminiapp",
+        paymentConfig: {
+            callbackKey: HUAWEI_CALLBACK_SECRET,
+        },
+        loginConfig: {
+            appId: HUAWEI_APP_ID,
+            appSecret: HUAWEI_APP_SECRET,
+        },
+    },
 };

+ 8 - 3
webServer/src/config/thirdParams.ts

@@ -128,7 +128,12 @@ export const SEVEN_TWO_ZERO_QUICK_PRODUCT_CODE = "920332684063151776920581716758
 export const SEVEN_TWO_ZERO_QUICK_PRODUCT_KEY = "02845520";
 
 // 美团渠道
-export const MEITUAN_APP_ID = ""; // 需要配置实际的应用ID
-export const MEITUAN_APP_SECRET = ""; // 需要配置实际的应用密钥
-
+export const MEITUAN_APP_ID = "251208633431"; // 需要配置实际的应用ID
+export const MEITUAN_APP_SECRET = "7B04DC515110A88336FD3DEDBF650F3F"; // 需要配置实际的应用密钥
+export const MEITUAN_APP_CALLBACK_SECRET = "55B5E96B295F07BB858FE7D90018AEAB"; // 需要配置实际的回调密钥
+
+// 华为渠道
+export const HUAWEI_APP_ID = "251208633431"; // 需要配置实际的传盛应用ID
+export const HUAWEI_APP_SECRET = "7B04DC515110A88336FD3DEDBF650F3F"; // 需要配置实际的应用密钥
+export const HUAWEI_CALLBACK_SECRET = "55B5E96B295F07BB858FE7D90018AEAB"; // 需要配置实际的回调密钥(用于支付回调签名验证)
 

+ 10 - 1
webServer/src/controller/ApiController.ts

@@ -558,6 +558,16 @@ class ApiController {
                 //{"status":0,"msg":"status 非 0 时,传入失败信息"}
                 ctx.body = {status: result.code === 1 ? 0 : -1, msg: result.msg};
                 break;
+            case 17:
+                // 华为渠道:成功返回 {"errCode": 0},失败返回 {"errCode": 1, "errMsg": "..."}
+                if (result.code === 0 || result.code === 1) {
+                    // 发货成功
+                    ctx.body = {errCode: 0};
+                } else {
+                    // 发货失败
+                    ctx.body = {errCode: 1, errMsg: result.msg || "发放失败"};
+                }
+                break;
             case 14:
                 ctx.body = result.code === 1 ? "SUCCESS" : "Fail";
                 break;
@@ -733,7 +743,6 @@ class ApiController {
                         logger.error(`查询服务器${element.server_id}角色信息失败:`, error);
                     }
                 }
-
                 data.push({
                     channel: channelConfig.name, //渠道固定
                     minSid: 1, //最小服务器