四端上传
This commit is contained in:
1 parent
25bbb5f9c8
commit
c6e2cb4d5b
276 files changed
+17812
-3
No files matched your search
@@ -0,0 +1,109 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { createHmac } from 'node:crypto';
|
||||
import { APP_CONFIG, type AppConfig } from '../../config/config';
|
||||
import { AppError } from '../../common/errors';
|
||||
|
||||
const XPAY_BASE = 'https://api.weixin.qq.com';
|
||||
|
||||
/**
|
||||
* 微信小程序虚拟支付客户端。
|
||||
*
|
||||
* 签名规则(两套,缺一不可):
|
||||
* paySig 服务端身份,密钥是后台的「现网 AppKey」:HMAC_SHA256(appkey, uri + '&' + postBody)
|
||||
* signature 用户态身份,密钥是该用户的 sessionKey:HMAC_SHA256(sessionKey, postBody)
|
||||
*
|
||||
* 注意:postBody 必须与实际发出的字符串逐字节一致,不能格式化、不能改键顺序。
|
||||
*/
|
||||
@Injectable()
|
||||
export class WxPayService {
|
||||
private readonly logger = new Logger(WxPayService.name);
|
||||
|
||||
constructor(@Inject(APP_CONFIG) private readonly cfg: AppConfig) {}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.cfg.WX_OFFER_ID !== '' && this.cfg.WX_PAY_APPKEY !== '';
|
||||
}
|
||||
|
||||
get offerId(): string {
|
||||
return this.cfg.WX_OFFER_ID;
|
||||
}
|
||||
|
||||
/** 服务端身份签名。uri 不带问号参数。 */
|
||||
calcPaySig(uri: string, postBody: string): string {
|
||||
return createHmac('sha256', this.cfg.WX_PAY_APPKEY).update(`${uri}&${postBody}`).digest('hex');
|
||||
}
|
||||
|
||||
/** 用户态签名。 */
|
||||
calcSignature(postBody: string, sessionKey: string): string {
|
||||
return createHmac('sha256', sessionKey).update(postBody).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查单。用于发货推送丢失时兜底补发。
|
||||
* 参数名是 order_id(传自己的 outTradeNo),不是 out_trade_no。
|
||||
*/
|
||||
async queryOrder(outTradeNo: string, openid: string): Promise<XpayQueryResult> {
|
||||
const uri = '/xpay/query_order';
|
||||
const body = JSON.stringify({ openid, env: 0, order_id: outTradeNo });
|
||||
const paySig = this.calcPaySig(uri, body);
|
||||
|
||||
const res = await fetch(`${XPAY_BASE}${uri}?pay_sig=${paySig}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body,
|
||||
});
|
||||
const json = (await res.json()) as XpayQueryResult;
|
||||
if (json.errcode && json.errcode !== 0) {
|
||||
this.logger.warn(`查单失败 ${outTradeNo}: ${json.errcode} ${json.errmsg ?? ''}`);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动退款(仅 Android 等非 iOS 终端可用;iOS 需用户向 App Store 申请,开发者无法发起)。
|
||||
*
|
||||
* 注意:参数集合需以官方《虚拟支付 - 退款》文档核对后再上线启用,
|
||||
* 当前按已知字段拼装,失败时抛错而非静默。
|
||||
*/
|
||||
async refundOrder(input: {
|
||||
outTradeNo: string;
|
||||
openid: string;
|
||||
refundOrderId: string;
|
||||
refundAmountFen: number;
|
||||
reason: string;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
if (!this.enabled) throw new AppError(4002, '虚拟支付未配置', 503);
|
||||
|
||||
const uri = '/xpay/refund_order';
|
||||
const body = JSON.stringify({
|
||||
openid: input.openid,
|
||||
env: 0,
|
||||
order_id: input.outTradeNo,
|
||||
refund_order_id: input.refundOrderId,
|
||||
refund_amount: input.refundAmountFen,
|
||||
refund_reason: input.reason,
|
||||
});
|
||||
const paySig = this.calcPaySig(uri, body);
|
||||
|
||||
const res = await fetch(`${XPAY_BASE}${uri}?pay_sig=${paySig}`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body,
|
||||
});
|
||||
return (await res.json()) as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface XpayQueryResult {
|
||||
errcode?: number;
|
||||
errmsg?: string;
|
||||
order?: {
|
||||
order_id?: string;
|
||||
wx_order_id?: string;
|
||||
status?: number;
|
||||
paid_time?: number;
|
||||
};
|
||||
status?: number;
|
||||
wx_order_id?: string;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user