跳转到内容

JavaScript / TypeScript 示例

此示例可直接作为 Node.js JavaScript 运行,并通过 JSDoc 类型提示兼容 TypeScript 检查。示例包含签名、POST JSONGET Query,以及代收下单、代付下单、商户信息查询三个接口。

js
// @ts-check

import crypto from 'node:crypto';

class SkynetClient {
  /**
   * @param {{ baseUrl: string; mchId: number; apiToken: string }} config
   */
  constructor(config) {
    this.baseUrl = config.baseUrl.replace(/\/$/, '');
    this.mchId = config.mchId;
    this.apiToken = config.apiToken;
  }

  async createCollectOrder() {
    return this.post('/api/v1/mch/pmt-orders', {
      trans_id: `ORDER-${Date.now()}`,
      currency: 'VND',
      amount: '100.00',
      channel: 'bank',
      callback_url: 'https://merchant.example.com/callback/collect',
      return_url: 'https://merchant.example.com/payment/result',
      remarks: 'collect demo',
    });
  }

  async createPayoutOrder() {
    return this.post('/api/v1/mch/wdl-orders', {
      trans_id: `WDL-${Date.now()}`,
      channel: 'bank',
      amount: '100.00',
      currency: 'VND',
      account_no: '2333667799212341',
      account_name: 'NGUYEN XUAN HUNG',
      account_org: 'PVCOMBANK',
      account_org_code: '970412',
      callback_url: 'https://merchant.example.com/callback/payout',
      remarks: 'payout demo',
    });
  }

  async getMerchantInfo() {
    return this.get('/api/v1/mch/info');
  }

  /**
   * @param {string} path
   * @param {Record<string, string | number | null | undefined>} params
   */
  async post(path, params) {
    const signedParams = this.withSignature(params);
    const response = await fetch(`${this.baseUrl}${path}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(signedParams),
    });

    return this.parseResponse(response);
  }

  /**
   * @param {string} path
   * @param {Record<string, string | number | null | undefined>} [params]
   */
  async get(path, params = {}) {
    const signedParams = this.withSignature(params);
    const query = new URLSearchParams();
    Object.entries(signedParams).forEach(([key, value]) => {
      query.set(key, String(value));
    });

    const response = await fetch(`${this.baseUrl}${path}?${query.toString()}`);
    return this.parseResponse(response);
  }

  /**
   * @param {Record<string, string | number | null | undefined>} params
   */
  withSignature(params) {
    const signedParams = {
      ...params,
      mch_id: this.mchId,
      nonce: crypto.randomBytes(8).toString('hex').slice(0, 12),
      timestamp: Math.floor(Date.now() / 1000),
    };

    return {
      ...signedParams,
      sign: this.sign(signedParams),
    };
  }

  /**
   * @param {Record<string, string | number | null | undefined>} params
   */
  sign(params) {
    const source = Object.keys(params)
      .filter((key) => key !== 'sign' && params[key] !== null && params[key] !== undefined && params[key] !== '')
      .sort()
      .reduce((result, key) => `${result}&${key}=${params[key]}`, this.apiToken);

    return crypto.createHash('md5').update(source, 'utf8').digest('hex');
  }

  /**
   * @param {Response} response
   */
  async parseResponse(response) {
    const body = await response.json();
    if (!response.ok) {
      throw new Error(`HTTP request failed: ${response.status}`);
    }

    if (body.code !== 200) {
      throw new Error(body?.errors?.message || body?.message || 'API request failed');
    }

    return body.payload ?? {};
  }
}

const client = new SkynetClient({
  baseUrl: 'https://api.example.com',
  mchId: 10001,
  apiToken: 'demo_key_123456',
});

const collectOrder = await client.createCollectOrder();
const payoutOrder = await client.createPayoutOrder();
const merchantInfo = await client.getMerchantInfo();

console.log({ collectOrder, payoutOrder, merchantInfo });

Released under the MIT License.