You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用当前checkOutRequestId查询Lipa Na MPESA交易状态报500错误

问题根源分析
  1. 全局Timestamp/Password过期:你的server.js中timestamp和password是服务器启动时生成的全局变量,但Safaricom API要求每个请求必须使用当前请求时刻的时间戳生成密码。服务器运行一段时间后,旧时间戳会失效,导致查询请求因鉴权失败返回500。
  2. 查询时机过早:STK Push返回CheckoutRequestID仅代表请求已被受理,Safaricom后台需要几秒完成交易记录落地,立即查询会因数据未同步触发500错误。

修复方案

1. 每次请求重新生成Timestamp和Password

将时间戳和密码的生成逻辑移到每个路由处理函数内部,确保每次请求都使用最新的有效鉴权信息:

// server.js 移除全局的timestamp和password定义

app.post("/stk", generateToken, async (req, res) => {
  // 每次请求生成新的timestamp和password
  const date = new Date();
  const timestamp =
    date.getFullYear() +
    ("0" + (date.getMonth() + 1)).slice(-2) +
    ("0" + date.getDate()).slice(-2) +
    ("0" + date.getHours()).slice(-2) +
    ("0" + date.getMinutes()).slice(-2) +
    ("0" + date.getSeconds()).slice(-2);

  const password = Buffer.from(
    businessShortCode + passKey + timestamp
  ).toString("base64");

  const phone = `254${req.body.phone.substring(1)}`;
  const amount = req.body.amount;

  await axios
    .post(
      "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
      {
        BusinessShortCode: businessShortCode,
        Password: password,
        Timestamp: timestamp,
        TransactionType: "CustomerPayBillOnline",
        Amount: amount,
        PartyA: phone,
        PartyB: businessShortCode,
        PhoneNumber: phone,
        CallBackURL: "https://011c-179-000-1312-63.ngrok.io/stk_callback",
        AccountReference: "Test",
        TransactionDesc: "Test",
      },
      {
        headers: {
          authorization: `Bearer ${token}`,
        },
      }
    )
    .then((data) => {
      res.status(200).json(data.data);
    })
    .catch((err) => {
      res.status(400).json(err.message);
    });
});

app.post("/transactionStatus", generateToken, async (req, res) => {
  // 查询请求同样生成新的timestamp和password
  const date = new Date();
  const timestamp =
    date.getFullYear() +
    ("0" + (date.getMonth() + 1)).slice(-2) +
    ("0" + date.getDate()).slice(-2) +
    ("0" + date.getHours()).slice(-2) +
    ("0" + date.getMinutes()).slice(-2) +
    ("0" + date.getSeconds()).slice(-2);

  const password = Buffer.from(
    businessShortCode + passKey + timestamp
  ).toString("base64");

  const checkoutReqId = req.body.checkoutReqId;
  const trnxUrl = "https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query";
  await axios
    .post(
      trnxUrl,
      {
        BusinessShortCode: businessShortCode,
        Password: password,
        Timestamp: timestamp,
        CheckoutRequestID: checkoutReqId,
      },
      {
        headers: {
          authorization: `Bearer ${token}`,
        },
      }
    )
    .then((data) => {
      res.status(200).json(data.data);
      console.log("check data response", data.data);
    })
    .catch((err) => {
      console.log("trnx attempt fail", err.message);
      res.status(400).json(err.message);
    });
});

2. 调整查询时机,添加延迟重试

修改前端index.js,不要在收到STK响应后立即查询,设置延迟后发起请求并添加重试机制:

// index.js 修改trnxStatus函数为带延迟重试的版本
function trnxStatus(checkoutReqId, retryCount = 3) {
  if (retryCount <= 0) {
    console.log("查询次数用尽,仍未获取有效响应");
    return;
  }

  // 延迟3秒后查询(可根据实际情况调整)
  setTimeout(() => {
    const body = {
      checkoutReqId: checkoutReqId,
    };

    const options = {
      method: "POST",
      body: JSON.stringify(body),
      headers: { "Content-type": "application/json; charset=UTF-8" },
    };
    const trnxUrl = "http://localhost:port/transactionStatus/";
    fetch(trnxUrl, options)
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
        return res.json();
      })
      .then((data) => {
        console.log("response data", data);
      })
      .catch((err) => {
        console.log(`查询失败,剩余重试次数:${retryCount - 1}`, err.message);
        // 递归重试
        trnxStatus(checkoutReqId, retryCount - 1);
      });
  }, 3000);
}

3. 优先依赖CallBackURL通知(最佳实践)

Safaricom会在用户完成/取消支付后主动调用你设置的CallBackURL,建议在/stk_callback路由中处理支付结果,而非主动轮询:

// server.js 添加回调路由处理
app.post("/stk_callback", (req, res) => {
  const callbackData = req.body;
  console.log("支付回调数据:", callbackData);
  
  // 解析回调数据,更新本地交易状态
  const checkoutRequestID = callbackData.Body.stkCallback.CheckoutRequestID;
  const resultCode = callbackData.Body.stkCallback.ResultCode;
  const resultDesc = callbackData.Body.stkCallback.ResultDesc;

  if (resultCode === 0) {
    console.log(`支付成功:${checkoutRequestID}`);
  } else {
    console.log(`支付失败:${checkoutRequestID},原因:${resultDesc}`);
  }

  // 必须返回200响应给Safaricom,否则会重复回调
  res.status(200).json({ ResultCode: 0, ResultDesc: "Success" });
});

额外注意事项
  • 确保generateToken中间件生成的token有效且未过期(Safaricom的access token有效期为1小时)。
  • 沙箱环境响应较慢时,可适当延长延迟时间(比如5秒)。

内容的提问来源于stack exchange,提问作者The_Ogre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 23:20:37