diff --git a/src/AlipayFormStream.ts b/src/AlipayFormStream.ts new file mode 100644 index 0000000..675846c --- /dev/null +++ b/src/AlipayFormStream.ts @@ -0,0 +1,11 @@ +// import { statSync } from 'node:fs'; +import FormStream from 'formstream'; + +export class AlipayFormStream extends FormStream { + // 覆盖 file 方法,由于 OpenAPI 文件上传需要强制设置 content-length,所以需要增加一次同步文件 io 来实现此功能 + // https://github.com/node-modules/formstream/blob/master/lib/formstream.js#L119 + // file(name: string, filepath: string, filename: string) { + // const size = statSync(filepath).size; + // return super.file(name, filepath, filename, size); + // } +} diff --git a/src/alipay.ts b/src/alipay.ts index 3298b6c..24719e9 100644 --- a/src/alipay.ts +++ b/src/alipay.ts @@ -1,13 +1,14 @@ import { debuglog } from 'node:util'; import { createVerify, randomUUID, createSign } from 'node:crypto'; +import { Readable } from 'node:stream'; import urllib, { Agent } from 'urllib'; import type { HttpClientResponse, HttpMethod, RequestOptions, RawResponseWithMeta, } from 'urllib'; import camelcaseKeys from 'camelcase-keys'; import snakeCaseKeys from 'snakecase-keys'; -import FormStream from 'formstream'; import { Stream as SSEStream } from 'sse-decoder'; +import { AlipayFormStream } from './AlipayFormStream.js'; import type { AlipaySdkConfig } from './types.js'; import { AlipayFormData } from './form.js'; import { @@ -17,8 +18,6 @@ import { } from './util.js'; import { getSNFromPath, getSN, loadPublicKey, loadPublicKeyFromPath } from './antcertutil.js'; -export const AlipayFormStream = FormStream; - const debug = debuglog('alipay-sdk'); const http2Agent = new Agent({ allowH2: true, @@ -142,7 +141,7 @@ export interface AlipayCURLOptions { /** 参数需在请求 JSON 传参 */ body?: Record; /** 表单方式提交数据 */ - form?: AlipayFormData | FormStream; + form?: AlipayFormData | AlipayFormStream; /** 调用方的 requestId,用于定位一次请求,需要每次请求保持唯一 */ requestId?: string; /** @@ -357,9 +356,9 @@ export class AlipaySdk { throw new TypeError('提交 form 数据不支持内容加密'); } // 文件上传,走 multipart/form-data - let form: FormStream; + let form: AlipayFormStream; if (options.form instanceof AlipayFormData) { - form = new FormStream(); + form = new AlipayFormStream(); const dataFieldValue = {} as Record; for (const item of options.form.fields) { dataFieldValue[item.name] = item.value; @@ -372,7 +371,7 @@ export class AlipaySdk { form.field('data', httpRequestBody, 'application/json'); // 文件上传 https://opendocs.alipay.com/open-v3/054oog#%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0 for (const item of options.form.files) { - form.file(item.name, item.path, item.fieldName); + form.file(item.fieldName, item.path, item.name); } } else if (options.form instanceof AlipayFormStream) { form = options.form; @@ -384,7 +383,7 @@ export class AlipaySdk { } else { throw new TypeError('options.form 必须是 AlipayFormData 或者 AlipayFormStream 类型'); } - requestOptions.content = form as any; + requestOptions.content = new Readable().wrap(form as any); Object.assign(requestOptions.headers, form.headers()); } else { // 普通请求 diff --git a/src/form.ts b/src/form.ts index a7f6bb8..51bbc32 100644 --- a/src/form.ts +++ b/src/form.ts @@ -18,8 +18,11 @@ function isJSONString(value: any) { } export interface IFile { + /** 文件名 */ name: string; + /** 文件路径 */ path: string; + /** 表单字段名 */ fieldName: string; } diff --git a/src/index.ts b/src/index.ts index 3a337ab..826e027 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './types.js'; export * from './alipay.js'; export { AlipayFormData } from './form.js'; +export { AlipayFormStream } from './AlipayFormStream.js'; diff --git a/test/alipay.test.ts b/test/alipay.test.ts index 220f00c..81c6be5 100755 --- a/test/alipay.test.ts +++ b/test/alipay.test.ts @@ -125,7 +125,7 @@ describe('test/alipay.test.ts', () => { }); }); - it.skip('POST 文件上传,使用 AlipayFormData', async () => { + it('POST 文件上传,使用 AlipayFormData', async () => { // https://opendocs.alipay.com/open-v3/5aa91070_alipay.open.file.upload?scene=common&pathHash=c8e11ccc const filePath = getFixturesFile('demo.jpg'); const form = new AlipayFormData(); @@ -146,11 +146,11 @@ describe('test/alipay.test.ts', () => { assert(uploadResult.traceId); }); - it.skip('POST 文件上传,使用 AlipayFormData with body', async () => { + it('POST 文件上传,使用 AlipayFormData with body', async () => { // https://opendocs.alipay.com/open-v3/5aa91070_alipay.open.file.upload?scene=common&pathHash=c8e11ccc const filePath = getFixturesFile('demo.jpg'); const form = new AlipayFormData(); - form.addFile('file_content', '图片.jpg', filePath); + form.addFile('file_content', 'demo.jpg', filePath); const uploadResult = await sdkStable.curl<{ file_id: string; @@ -466,7 +466,7 @@ describe('test/alipay.test.ts', () => { aesDecryptText(readFixturesFile('test_alipayCertPublicKey_RSA2_aesEncryptText.crt'), encryptKey), }); - it.skip('POST 文件上传,使用 AlipayFormStream with body', async () => { + it('POST 文件上传,使用 AlipayFormStream with body', async () => { // https://opendocs.alipay.com/open-v3/5aa91070_alipay.open.file.upload?scene=common&pathHash=c8e11ccc const filePath = getFixturesFile('demo.jpg'); const form = new AlipayFormStream(); @@ -486,6 +486,26 @@ describe('test/alipay.test.ts', () => { assert(uploadResult.traceId); }); + it('POST 文件上传,使用 AlipayFormData with body', async () => { + // https://opendocs.alipay.com/open-v3/5aa91070_alipay.open.file.upload?scene=common&pathHash=c8e11ccc + const filePath = getFixturesFile('demo.jpg'); + const form = new AlipayFormData(); + form.addFile('file_content', '图片.jpg', filePath); + + const uploadResult = await sdk.curl<{ + file_id: string; + }>('POST', '/v3/alipay/open/file/upload', { + form, + body: { + biz_code: 'openpt_appstore', + }, + }); + console.log(uploadResult); + assert(uploadResult.data.file_id); + assert.equal(uploadResult.responseHttpStatus, 200); + assert(uploadResult.traceId); + }); + it('POST /v3/alipay/user/deloauth/detail/query', async () => { // https://opendocs.alipay.com/open-v3/668cd27c_alipay.user.deloauth.detail.query?pathHash=3ab93168 const result = await sdk.curl('POST', '/v3/alipay/user/deloauth/detail/query', {