Skip to main content

设计理念

子商户信息如何注册

这实际上是一个人体工程学的问题,即:这个轮子如何好用,方便使用,而不是奇奇怪怪的能用起来。

首先分析一下场景,一个服务商下面会有至少 1 个子商户,所以我们需要按照多个子商户的方式去考虑。根据实践积累,在现实场景中可能存在一些两种情况:

  1. 我们已知一些子商户的信息,这种情况下可以将商户的信息硬编码到源码中。
  2. 在系统运行过程中动态加入的子商户信息,这些信息是提前无法预知的,何时加入也是无法预知的。通常在SasS平台上比较常见。

结合上面两种情况,为了使用起来方便,我们尽量支持两种方式:

已知商户信息

已知商户信息的情况下,我们在支付模块注册的时候就配置好即可:

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: ConfigValidation,
validationOptions: {
allowUnknown: true,
abortEarly: true
},
load: [WechatProviderPayConfigRegister]
}),
// 异步方式
PayModule.forRootAsync({
useFactory: (wechatProviderPayConfig: ConfigType<typeof WechatProviderPayConfigRegister>) => ({
wechatProvider: { // 微信服务商支付相关配置
appid: wechatProviderPayConfig.appid,
mchid: wechatProviderPayConfig.mchid,
apiKeyV3: wechatProviderPayConfig.apiKeyV3,
publicKey: readFileSync(process.cwd() + wechatProviderPayConfig.publicKeyPath),
privateKey: readFileSync(process.cwd() + wechatProviderPayConfig.privateKeyPath),
apps: [
{
app: WECHAT_PROVIDER_APP1,
appid: wechatProviderPayConfig.app1.appid,
mchid: wechatProviderPayConfig.app1.mchid
},
{
app: WECHAT_PROVIDER_APP2,
appid: wechatProviderPayConfig.app2.appid,
mchid: wechatProviderPayConfig.app2.mchid
}
]
}
}),
inject: [WechatProviderPayConfigRegister.KEY]
})
// 同步方式
PayModule.forRoot({
wechatProvider: { // 微信服务商支付相关配置
appid: '微信服务商appid',
mchid: '微信服务商商户号',
apiKeyV3: '微信服务商V3版apiKey',
publicKey: readFileSync(process.cwd() + '/certs/wechat-provider-pay/apiclient_cert.pem'),
privateKey: readFileSync(process.cwd() + '/certs/wechat-provider-pay/apiclient_key.pem'),
apps: [
{
app: WECHAT_PROVIDER_APP1,
appid: '特约商户(子商户)1的appid',
mchid: '特约商户(子商户)1的商户号'
},
{
app: WECHAT_PROVIDER_APP2,
appid: '特约商户(子商户)2的appid',
mchid: '特约商户(子商户)2的商户号'
}
]
}
})
],
})
export class AppModule {}

上面的示例中,我们已知两个子商户的信息,其中 WECHAT_PROVIDER_APP1WECHAT_PROVIDER_APP2 是两个子商户的标识,建议采用 Symbol 格式,示例:

/** 特约(子)商户1标识符 */
export const WECHAT_PROVIDER_APP1 = Symbol('WECHAT_PROVIDER_APP1');
/** 特约(子)商户2标识符 */
export const WECHAT_PROVIDER_APP2 = Symbol('WECHAT_PROVIDER_APP2');

接下来在使用的时候就可以通过标识符来直接选择相应的 Service 啦,以 下单placeOrder )方法为例:

@Controller('pay')
export class PayController {
constructor(
@InjectWechatProviderPayMpService(WECHAT_PROVIDER_APP1)
private readonly wechatProviderPayMpService: WechatProviderPayMpService
) {}

@Post('wechat')
async create(): Promise<WechatPayMpResDto> {
const { success, data, error } = await this.wechatProviderPayMpService.placeOrder({
out_trade_no: order.tradeNo,
description: 'xxx',
notify_url: this.payConfig.wechatProvider.notifyUrlPrefix + '/wechat/pay/provider',
amount: {
total: order.price
},
payer: {
sub_openid: userInfo.openId // 子应用的openid
}
});

if (!success) throw new BadRequestException(`订单支付失败:${error}`);

return {
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign
};
}
}
提示

核心就在于 constructor 构造函数的参数,采用 @InjectWechatProviderPayMpService(WECHAT_PROVIDER_APP1) 传入该 WechatProviderPayMpService 的子应用配置,接下来 placeOrder 方法就不用传入 第二个参数 了!

未知商户信息

未知商户信息就需要在使用的时候主动传入子商户的信息,使用示例如下:

@Controller('pay')
export class PayController {
constructor(
@InjectWechatProviderPayMpService()
private readonly wechatProviderPayMpService: WechatProviderPayMpService
) {}

@Post('wechat')
async create(): Promise<WechatPayMpResDto> {
const { success, data, error } = await this.wechatProviderPayMpService.placeOrder({
out_trade_no: order.tradeNo,
description: 'xxx',
notify_url: this.payConfig.wechatProvider.notifyUrlPrefix + '/wechat/pay/provider',
amount: {
total: order.price
},
payer: {
sub_openid: userInfo.openId // 子应用的openid
}
},{
appid: '子商户appid',
mchid: '子商户的商户号'
});

if (!success) throw new BadRequestException(`订单支付失败:${error}`);

return {
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign
};
}
}
提示
  1. 未知商户信息使用时需要注意constructor构造函数的参数装饰器为:@InjectWechatProviderPayMpService()不要传入参数!!!
  2. 相比于注册方式,很多方法都需要额外的传入子商户的信息,不传则会报错。
  3. 实际上完全可以采用未知商户的信息方式完成所有功能,只不过需要多些一些代码。