mirror of
https://github.com/certd/certd.git
synced 2026-04-23 11:37:23 +08:00
chore: 优化支付提供者插件位置
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { logger } from "@certd/basic";
|
||||
import { PluginService } from "../plugin/service/plugin-service.js";
|
||||
import { registerPaymentProviders } from "../suite/payments/index.js";
|
||||
|
||||
@Autoload()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -19,6 +20,8 @@ export class AutoBLoadPlugins {
|
||||
}
|
||||
// await import("../../plugins/index.js")
|
||||
await this.pluginService.registerFromDb()
|
||||
|
||||
await registerPaymentProviders();
|
||||
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import dayjs from "dayjs";
|
||||
import { logger, utils } from "@certd/basic";
|
||||
import { AlipayAccess } from "../../../plugins/plugin-plus/alipay/access.js";
|
||||
|
||||
export class PaymentAlipay implements IPaymentProvider {
|
||||
access: AlipayAccess;
|
||||
|
||||
constructor(access: AlipayAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
|
||||
const alipaySdk = await this.createAlipaySdk();
|
||||
|
||||
const result: any = await alipaySdk.curl("POST", "/v3/alipay/trade/query", {
|
||||
body: {
|
||||
out_trade_no: tradeNo,
|
||||
},
|
||||
});
|
||||
logger.info("获取支付宝订单详情", JSON.stringify(result));
|
||||
if (result.responseHttpStatus !== 200) {
|
||||
throw new Error("请求支付宝失败:" + result.responseHttpStatus);
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
let status: string = undefined;
|
||||
let payTime: number = undefined;
|
||||
if (data.trade_status === "TRADE_SUCCESS") {
|
||||
status = "paid";
|
||||
payTime = dayjs(data.send_pay_date).valueOf();
|
||||
} else if (data.trade_status === "TRADE_CLOSED") {
|
||||
status = "closed";
|
||||
} else {
|
||||
logger.info("支付宝订单状态为:" + data.trade_status);
|
||||
}
|
||||
|
||||
return {
|
||||
tradeNo,
|
||||
status: status,
|
||||
amount: utils.amount.toCent(parseFloat(data.total_amount)),
|
||||
payNo: data.trade_no,
|
||||
payTime: payTime,
|
||||
};
|
||||
}
|
||||
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const return_url = `${opts.bindUrl}/#/certd/payment/return/alipay`;
|
||||
const notify_url = `${opts.bindUrl}/api/payment/notify/alipay`;
|
||||
|
||||
const alipaySdk = await this.createAlipaySdk();
|
||||
const url = alipaySdk.pageExec("alipay.trade.page.pay", "GET", {
|
||||
return_url,
|
||||
notify_url,
|
||||
bizContent: {
|
||||
out_trade_no: trade.tradeNo,
|
||||
total_amount: utils.amount.toYuan(trade.amount),
|
||||
subject: trade.title,
|
||||
product_code: "FAST_INSTANT_TRADE_PAY",
|
||||
// qr_pay_mode: "1",
|
||||
// qrcode_width: "100",
|
||||
// time_expire: "2016-12-31+10:05:01",
|
||||
// sub_merchant: {
|
||||
// merchant_id: "2088000603999128",
|
||||
// merchant_type: "alipay",
|
||||
// },
|
||||
// extend_params: {
|
||||
// sys_service_provider_id: "2088511833207846",
|
||||
// hb_fq_seller_percent: "100",
|
||||
// hb_fq_num: "3",
|
||||
// industry_reflux_info: '{\\"scene_code\\":\\"metro_tradeorder\\",\\"channel\\":\\"xxxx\\",\\"scene_data\\":{\\"asset_name\\":\\"ALIPAY\\"}}',
|
||||
// specified_seller_name: "XXX的跨境小铺",
|
||||
// royalty_freeze: "true",
|
||||
// card_type: "S0JP0000",
|
||||
// },
|
||||
// business_params: '{"mc_create_trade_ip":"127.0.0.1"}',
|
||||
// promo_params: '{"storeIdType":"1"}',
|
||||
// integration_type: "PCWEB",
|
||||
// request_from_url: "https://",
|
||||
// store_id: "NJ_001",
|
||||
// merchant_order_no: "20161008001",
|
||||
// ext_user_info: {
|
||||
// cert_type: "IDENTITY_CARD",
|
||||
// cert_no: "362334768769238881",
|
||||
// name: "李明",
|
||||
// mobile: "16587658765",
|
||||
// min_age: "18",
|
||||
// need_check_info: "F",
|
||||
// identity_hash: "27bfcd1dee4f22c8fe8a2374af9b660419d1361b1c207e9b41a754a113f38fcc",
|
||||
// },
|
||||
// invoice_info: {
|
||||
// key_info: {
|
||||
// tax_num: "1464888883494",
|
||||
// is_support_invoice: "true",
|
||||
// invoice_merchant_name: "ABC|003",
|
||||
// },
|
||||
// details: '[{"code":"100294400","name":"服饰","num":"2","sumPrice":"200.00","taxRate":"6%"}]',
|
||||
// },
|
||||
},
|
||||
});
|
||||
return {
|
||||
url,
|
||||
body: {},
|
||||
};
|
||||
}
|
||||
|
||||
private async createAlipaySdk() {
|
||||
const AlipaySdk = await import("alipay-sdk");
|
||||
|
||||
const alipaySdk = new AlipaySdk.AlipaySdk({
|
||||
appId: this.access.appId,
|
||||
privateKey: this.access.privateKey,
|
||||
alipayPublicKey: this.access.alipayPublicKey,
|
||||
gateway: "https://openapi.alipay.com/gateway.do",
|
||||
});
|
||||
return alipaySdk;
|
||||
}
|
||||
|
||||
async onNotify(data: any, updateTrade: UpdateTrade) {
|
||||
const alipaySdk = await this.createAlipaySdk();
|
||||
logger.info(`支付宝notify:${JSON.stringify(data)}`);
|
||||
// true | false
|
||||
let success = alipaySdk.checkNotifySign(data);
|
||||
if (!success) {
|
||||
success = alipaySdk.checkNotifySignV2(data);
|
||||
if (!success) {
|
||||
throw new Error("签名验证失败");
|
||||
}
|
||||
}
|
||||
if (data.trade_status === "TRADE_SUCCESS") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "paid",
|
||||
amount: utils.amount.toCent(parseFloat(data.total_amount)),
|
||||
payNo: data.trade_no,
|
||||
payTime: dayjs().valueOf(),
|
||||
});
|
||||
} else if (data.trade_status === "TRADE_CLOSED") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "closed",
|
||||
payNo: data.trade_no,
|
||||
});
|
||||
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {paymentProviderFactory} from "@certd/commercial-core"
|
||||
export function registerPaymentProviders() {
|
||||
|
||||
paymentProviderFactory.registerProvider("alipay", async () => (await import("./alipay.js")).PaymentAlipay);
|
||||
paymentProviderFactory.registerProvider("wxpay", async () => (await import("./wxpay.js")).PaymentWxpay);
|
||||
paymentProviderFactory.registerProvider("yizhifu", async () => (await import("./yizhifu.js")).PaymentYizhifu);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import WxPay from "wechatpay-node-v3";
|
||||
import dayjs from "dayjs";
|
||||
import { logger } from "@certd/basic"; // 支持使用require
|
||||
import { WxpayAccess } from "../../../plugins/plugin-plus/wxpay/access.js";
|
||||
export class PaymentWxpay implements IPaymentProvider {
|
||||
access: WxpayAccess;
|
||||
constructor(access: WxpayAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
|
||||
/**
|
||||
* const result = await pay.query({out_trade_no: '1609914303237'});
|
||||
* # 或者 const result = await pay.query({transaction_id: ''});
|
||||
* console.log(result);
|
||||
* {
|
||||
* status: 200,
|
||||
* appid: 'appid',
|
||||
* attach: '',
|
||||
* mchid: '商户号',
|
||||
* out_trade_no: '1609899981750',
|
||||
* payer: {},
|
||||
* promotion_detail: [],
|
||||
* trade_state: 'CLOSED',
|
||||
* trade_state_desc: '订单已关闭'
|
||||
* }
|
||||
*/
|
||||
|
||||
const pay = this.createSdk();
|
||||
|
||||
const result: any = await pay.query({ out_trade_no: tradeNo });
|
||||
logger.info(`微信支付查询订单返回:${JSON.stringify(result)}`);
|
||||
if (result.status !== 200) {
|
||||
throw new Error("查询微信支付订单失败:" + result.status);
|
||||
}
|
||||
const data = result.data;
|
||||
let status: string = undefined;
|
||||
let payTime: number = undefined;
|
||||
let amount: number = undefined;
|
||||
if (data.trade_state === "SUCCESS") {
|
||||
status = "paid";
|
||||
payTime = dayjs(data.success_time).valueOf();
|
||||
amount = data.amount.total;
|
||||
} else if (data.trade_state === "CLOSED") {
|
||||
status = "closed";
|
||||
} else {
|
||||
logger.info("微信支付订单状态为:" + data.trade_state);
|
||||
}
|
||||
|
||||
return {
|
||||
tradeNo: data.out_trade_no,
|
||||
status,
|
||||
amount,
|
||||
payNo: data.transaction_id,
|
||||
payTime,
|
||||
};
|
||||
}
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const notify_url = `${opts.bindUrl}/api/payment/notify/wxpay`;
|
||||
|
||||
const pay = this.createSdk();
|
||||
|
||||
const params = {
|
||||
description: trade.title,
|
||||
out_trade_no: trade.tradeNo,
|
||||
notify_url,
|
||||
amount: {
|
||||
total: trade.amount,
|
||||
},
|
||||
scene_info: {
|
||||
payer_client_ip: "ip",
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(`微信支付下单请求:${JSON.stringify(params)}`);
|
||||
const result: any = await pay.transactions_native(params);
|
||||
logger.info(`微信支付下单返回:${JSON.stringify(result)}`);
|
||||
if (result.status !== 200) {
|
||||
throw new Error("请求微信支付失败:" + result.status);
|
||||
}
|
||||
return {
|
||||
qrcode: result.data.code_url,
|
||||
};
|
||||
}
|
||||
|
||||
private createSdk() {
|
||||
const pay = new WxPay({
|
||||
appid: this.access.appId,
|
||||
mchid: this.access.mchid,
|
||||
publicKey: Buffer.from(this.access.publicKey), // 公钥
|
||||
privateKey: Buffer.from(this.access.privateKey), // 秘钥
|
||||
});
|
||||
return pay;
|
||||
}
|
||||
|
||||
async onNotify(notifyData: any, updateTrade: UpdateTrade) {
|
||||
const pay = this.createSdk();
|
||||
const { ciphertext, associated_data, nonce } = notifyData.resource;
|
||||
logger.info(`微信支付notify:${JSON.stringify(notifyData)}`);
|
||||
const key = this.access.key;
|
||||
const result: any = pay.decipher_gcm(ciphertext, associated_data, nonce, key);
|
||||
logger.info(`微信支付解析结果:${JSON.stringify(result)}`);
|
||||
/**
|
||||
* mchid: '商户号',
|
||||
* # appid: 'appid',
|
||||
* # out_trade_no: '1610419296553',
|
||||
* # transaction_id: '4200000848202101120290526543',
|
||||
* # trade_type: 'NATIVE',
|
||||
* # trade_state: 'SUCCESS',
|
||||
* # trade_state_desc: '支付成功',
|
||||
* # bank_type: 'OTHERS',
|
||||
* # attach: '',
|
||||
* # success_time: '2021-01-12T10:43:43+08:00',
|
||||
* # payer: { openid: '' },
|
||||
* # amount: { total: 1, payer_total: 1, currency: 'CNY', payer_currency: 'CNY' }
|
||||
*/
|
||||
const data: any = result;
|
||||
if (data.trade_state === "SUCCESS") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "paid",
|
||||
amount: data.amount.total,
|
||||
payNo: data.transaction_id,
|
||||
payTime: dayjs(data.success_time).valueOf(),
|
||||
});
|
||||
} else if (data.trade_state === "CLOSED") {
|
||||
await updateTrade({
|
||||
tradeNo: data.out_trade_no,
|
||||
status: "closed",
|
||||
payNo: data.transaction_id,
|
||||
});
|
||||
}
|
||||
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { logger, utils } from "@certd/basic";
|
||||
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
|
||||
import dayjs from "dayjs";
|
||||
import { YizhifuAccess } from "../../../plugins/plugin-plus/yizhifu/access.js";
|
||||
|
||||
export class PaymentYizhifu implements IPaymentProvider {
|
||||
access: YizhifuAccess;
|
||||
constructor(access: YizhifuAccess) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
|
||||
/**
|
||||
* http://pay.docmirror.cn/api.php?act=order&pid={商户ID}&key={商户密钥}&out_trade_no={商户订单号}
|
||||
*
|
||||
* 请求参数说明:
|
||||
*
|
||||
* 字段名 变量名 必填 类型 示例值 描述
|
||||
* 操作类型 act 是 String order 此API固定值
|
||||
* 商户ID pid 是 Int 1001
|
||||
* 商户密钥 key 是 String 89unJUB8HZ54Hj7x4nUj56HN4nUzUJ8i
|
||||
* 系统订单号 trade_no 选择 String 20160806151343312
|
||||
* 商户订单号 out_trade_no 选择 String 20160806151343349
|
||||
*/
|
||||
|
||||
const paymentReq = {
|
||||
pid: this.access.pid,
|
||||
act: "order",
|
||||
key: this.access.key,
|
||||
out_trade_no: tradeNo,
|
||||
};
|
||||
let url = this.access.url;
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
const res = await utils.http.request({
|
||||
url: url + "/api.php",
|
||||
method: "get",
|
||||
params: paymentReq,
|
||||
});
|
||||
|
||||
if (res.code !== 1) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
|
||||
if (res.status !== "1") {
|
||||
throw new Error("该订单还未支付");
|
||||
}
|
||||
/**
|
||||
* 易支付订单号 trade_no String 2016080622555342651 袖手科技聚合支付平台订单号
|
||||
* 商户订单号 out_trade_no String 20160806151343349 商户系统内部的订单号
|
||||
* 第三方订单号 api_trade_no String 20160806151343349 支付宝微信等接口方订单号
|
||||
* 支付方式 type String alipay 支付方式列表
|
||||
* 商户ID pid Int 1001 发起支付的商户ID
|
||||
* 创建订单时间 addtime String 2016-08-06 22:55:52
|
||||
* 完成交易时间 endtime String 2016-08-06 22:55:52
|
||||
* 商品名称 name String VIP会员
|
||||
* 商品金额 money String 1.00
|
||||
* 支付状态 status Int 0 1为支付成功,0为未支付
|
||||
* 业务扩展参数 param String 默认留空
|
||||
* 支付者账号 buyer String 默认留空
|
||||
*/
|
||||
|
||||
let status: string = null;
|
||||
let payTime: number = null;
|
||||
if (res.status === "1") {
|
||||
status = "paid";
|
||||
payTime = dayjs(res.endtime).valueOf();
|
||||
} else {
|
||||
throw new Error("订单未支付");
|
||||
}
|
||||
|
||||
return {
|
||||
tradeNo: res.out_trade_no,
|
||||
payNo: res.trade_no,
|
||||
remark: "支付类型:" + res.type,
|
||||
amount: utils.amount.toCent(parseFloat(res.money)),
|
||||
status: status,
|
||||
payTime: payTime,
|
||||
};
|
||||
}
|
||||
sign(paymentReq: any): any {
|
||||
const keys = Object.keys(paymentReq);
|
||||
if (paymentReq.pid && paymentReq.pid + "" !== this.access.pid + "") {
|
||||
throw new Error("pid not match");
|
||||
}
|
||||
paymentReq.pid = this.access.pid;
|
||||
const params: any[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const value = paymentReq[key];
|
||||
if (value != null && value !== "" && key !== "sign" && key !== "sign_type") {
|
||||
params.push({ name: key, value: value });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 1、将发送或接收到的所有参数按照参数名ASCII码从小到大排序(a-z),sign、sign_type、和空值不参与签名!
|
||||
* 2、将排序后的参数拼接成URL键值对的格式,例如 a=b&c=d&e=f,参数值不要进行url编码。
|
||||
* 3、再将拼接好的字符串与商户密钥KEY进行MD5加密得出sign签名参数,sign = md5 ( a=b&c=d&e=f + KEY ) (注意:+ 为各语言的拼接符,不是字符!),md5结果为小写。
|
||||
*/
|
||||
//sort
|
||||
const sortedParams = params.sort((a, b) => a.name.localeCompare(b.name));
|
||||
//join
|
||||
const signStr = sortedParams.map(p => `${p.name}=${p.value}`).join("&");
|
||||
//md5
|
||||
const signType = this.access.signType;
|
||||
|
||||
let sign = "";
|
||||
if (signType === "MD5") {
|
||||
sign = utils.hash.md5(signStr + this.access.key);
|
||||
} else if (signType === "SHA256") {
|
||||
sign = utils.hash.sha256(signStr + this.access.key);
|
||||
} else {
|
||||
throw new Error("不支持的签名方式");
|
||||
}
|
||||
|
||||
sign = sign.toLowerCase();
|
||||
|
||||
params.push({ name: "sign", value: sign });
|
||||
|
||||
params.push({ name: "sign_type", value: signType });
|
||||
|
||||
const body = {};
|
||||
params.forEach(p => {
|
||||
body[p.name] = p.value;
|
||||
});
|
||||
|
||||
return body;
|
||||
}
|
||||
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
|
||||
const { bindUrl } = opts;
|
||||
const paymentReq: any = {
|
||||
pid: this.access.pid,
|
||||
out_trade_no: trade.tradeNo,
|
||||
return_url: `${bindUrl}/#/certd/payment/return/yizhifu`,
|
||||
notify_url: `${bindUrl}/api/payment/notify/yizhifu`,
|
||||
name: trade.title,
|
||||
money: utils.amount.toYuan(trade.amount),
|
||||
};
|
||||
|
||||
if (this.access.payType) {
|
||||
paymentReq.type = this.access.payType;
|
||||
}
|
||||
|
||||
const body = this.sign(paymentReq);
|
||||
|
||||
let url = this.access.url;
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
return {
|
||||
url: url + "/submit.php",
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
checkSign(paymentRes: any) {
|
||||
// const { pid, trade_no, out_trade_no, type, name, money, trade_status, param, sign, sign_type } = paymentRes;
|
||||
const body = this.sign(paymentRes);
|
||||
const pass = body.sign === paymentRes.sign;
|
||||
if (!pass) {
|
||||
throw new Error("签名校验失败");
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
async onNotify(paymentRes: any, updateTrade: UpdateTrade) {
|
||||
logger.info(`易支付notify:${JSON.stringify(paymentRes)}`);
|
||||
this.checkSign(paymentRes);
|
||||
|
||||
const success = paymentRes.trade_status === "TRADE_SUCCESS";
|
||||
if (success) {
|
||||
await updateTrade({
|
||||
tradeNo: paymentRes.out_trade_no,
|
||||
status: "paid",
|
||||
amount: utils.amount.toCent(parseFloat(paymentRes.money)),
|
||||
remark: "支付类型:" + paymentRes.type,
|
||||
payNo: paymentRes.trade_no,
|
||||
payTime: dayjs().valueOf(),
|
||||
});
|
||||
}
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user