|
|
@@ -2,16 +2,7 @@ import { DataSource, Repository } from 'typeorm';
|
|
|
import { WechatPayConfig } from './wechat-pay-config.entity';
|
|
|
import { WechatCouponStock } from './wechat-coupon-stock.entity';
|
|
|
import { WechatCoupon } from './wechat-coupon.entity';
|
|
|
-import crypto from 'crypto';
|
|
|
-import axios from 'axios';
|
|
|
-
|
|
|
-interface WechatPayConfigData {
|
|
|
- merchantId: string;
|
|
|
- appId: string;
|
|
|
- privateKey: string;
|
|
|
- certificateSerialNo: string;
|
|
|
- apiV3Key: string;
|
|
|
-}
|
|
|
+import { WechatPayClientService } from './wechat-pay-client.service';
|
|
|
|
|
|
interface CreateStockData {
|
|
|
stockName: string;
|
|
|
@@ -52,99 +43,26 @@ export class WechatPayService {
|
|
|
return config;
|
|
|
}
|
|
|
|
|
|
- // 生成V3签名
|
|
|
- private generateV3Signature(
|
|
|
- httpMethod: string,
|
|
|
- url: string,
|
|
|
- body: string,
|
|
|
- config: WechatPayConfigData
|
|
|
- ): string {
|
|
|
- const timestamp = Math.floor(Date.now() / 1000);
|
|
|
- const nonceStr = this.generateNonceStr(32);
|
|
|
-
|
|
|
- const urlParts = new URL(url);
|
|
|
- const canonicalUrl = urlParts.pathname + (urlParts.search || '');
|
|
|
-
|
|
|
- const message = `${httpMethod}\n${canonicalUrl}\n${timestamp}\n${nonceStr}\n${body}\n`;
|
|
|
-
|
|
|
- const sign = crypto.createSign('RSA-SHA256');
|
|
|
- sign.write(message);
|
|
|
- sign.end();
|
|
|
-
|
|
|
- const privateKey = `-----BEGIN PRIVATE KEY-----\n${config.privateKey}\n-----END PRIVATE KEY-----`;
|
|
|
- const signature = sign.sign(privateKey, 'base64');
|
|
|
-
|
|
|
- const token = `WECHATPAY2-SHA256-RSA2048 mchid="${config.merchantId}",nonce_str="${nonceStr}",timestamp="${timestamp}",serial_no="${config.certificateSerialNo}",signature="${signature}"`;
|
|
|
-
|
|
|
- return token;
|
|
|
- }
|
|
|
-
|
|
|
- // 生成随机字符串
|
|
|
- private generateNonceStr(length: number = 32): string {
|
|
|
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
- let result = '';
|
|
|
- for (let i = 0; i < length; i++) {
|
|
|
- result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
|
+ // 获取微信支付客户端实例
|
|
|
+ private async getWechatPayClient(configId: number): Promise<WechatPayClientService> {
|
|
|
+ const config = await this.configRepository.findOne({ where: { id: configId } });
|
|
|
+ if (!config) {
|
|
|
+ throw new Error('微信支付配置不存在');
|
|
|
}
|
|
|
- return result;
|
|
|
- }
|
|
|
|
|
|
- // 发起微信支付API请求
|
|
|
- private async wechatPayRequest(
|
|
|
- method: string,
|
|
|
- endpoint: string,
|
|
|
- data?: any,
|
|
|
- config?: WechatPayConfig
|
|
|
- ): Promise<any> {
|
|
|
- const activeConfig = config || await this.getActiveConfig();
|
|
|
-
|
|
|
- const configData: WechatPayConfigData = {
|
|
|
- merchantId: activeConfig.merchantId,
|
|
|
- appId: activeConfig.appId,
|
|
|
- privateKey: activeConfig.privateKey,
|
|
|
- certificateSerialNo: activeConfig.certificateSerialNo,
|
|
|
- apiV3Key: activeConfig.apiV3Key
|
|
|
- };
|
|
|
-
|
|
|
- const url = `https://api.mch.weixin.qq.com${endpoint}`;
|
|
|
- const body = data ? JSON.stringify(data) : '';
|
|
|
- const signature = this.generateV3Signature(method, url, body, configData);
|
|
|
-
|
|
|
- const response = await axios({
|
|
|
- method,
|
|
|
- url,
|
|
|
- data,
|
|
|
- headers: {
|
|
|
- 'Authorization': signature,
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- 'Accept': 'application/json',
|
|
|
- 'User-Agent': 'WeChatPay/1.0'
|
|
|
- }
|
|
|
+ return new WechatPayClientService({
|
|
|
+ merchantId: config.merchantId,
|
|
|
+ appId: config.appId,
|
|
|
+ privateKey: config.privateKey,
|
|
|
+ certificateSerialNo: config.certificateSerialNo,
|
|
|
+ apiV3Key: config.apiV3Key
|
|
|
});
|
|
|
-
|
|
|
- return response.data;
|
|
|
}
|
|
|
|
|
|
// 创建代金券批次
|
|
|
async createStock(data: CreateStockData, configId: number): Promise<WechatCouponStock> {
|
|
|
- const config = await this.configRepository.findOne({ where: { id: configId } });
|
|
|
- if (!config) {
|
|
|
- throw new Error('微信支付配置不存在');
|
|
|
- }
|
|
|
-
|
|
|
- const stockData = {
|
|
|
- stock_name: data.stockName,
|
|
|
- stock_creator_mchid: data.stockCreatorMchid,
|
|
|
- coupon_use_rule: data.couponUseRule,
|
|
|
- stock_send_rule: data.stockSendRule,
|
|
|
- coupon_type: data.couponType,
|
|
|
- coupon_amount: data.couponAmount,
|
|
|
- coupon_quantity: data.couponQuantity,
|
|
|
- available_begin_time: data.startTime.toISOString(),
|
|
|
- available_end_time: data.endTime.toISOString()
|
|
|
- };
|
|
|
-
|
|
|
- const response = await this.wechatPayRequest('POST', '/v3/marketing/favor/coupon-stocks', stockData, config);
|
|
|
+ const client = await this.getWechatPayClient(configId);
|
|
|
+ const response = await client.createStock(data);
|
|
|
|
|
|
const stock = this.stockRepository.create({
|
|
|
stockId: response.stock_id,
|
|
|
@@ -173,14 +91,8 @@ export class WechatPayService {
|
|
|
throw new Error('代金券批次不存在');
|
|
|
}
|
|
|
|
|
|
- const config = await this.configRepository.findOne({ where: { id: stock.configId } });
|
|
|
- if (!config) {
|
|
|
- throw new Error('微信支付配置不存在');
|
|
|
- }
|
|
|
-
|
|
|
- await this.wechatPayRequest('POST', `/v3/marketing/favor/stocks/${stockId}/start`, {
|
|
|
- stock_creator_mchid: stock.stockCreatorMchid
|
|
|
- }, config);
|
|
|
+ const client = await this.getWechatPayClient(stock.configId);
|
|
|
+ await client.activateStock(stockId, stock.stockCreatorMchid);
|
|
|
|
|
|
stock.status = 'RUNNING';
|
|
|
await this.stockRepository.save(stock);
|
|
|
@@ -199,19 +111,8 @@ export class WechatPayService {
|
|
|
throw new Error('代金券库存不足');
|
|
|
}
|
|
|
|
|
|
- const config = await this.configRepository.findOne({ where: { id: configId } });
|
|
|
- if (!config) {
|
|
|
- throw new Error('微信支付配置不存在');
|
|
|
- }
|
|
|
-
|
|
|
- const couponData = {
|
|
|
- stock_id: data.stockId,
|
|
|
- out_request_no: data.outRequestNo,
|
|
|
- appid: config.appId,
|
|
|
- stock_creator_mchid: data.stockCreatorMchid
|
|
|
- };
|
|
|
-
|
|
|
- const response = await this.wechatPayRequest('POST', `/v3/marketing/favor/users/${data.openid}/coupons`, couponData, config);
|
|
|
+ const client = await this.getWechatPayClient(configId);
|
|
|
+ const response = await client.sendCoupon(data);
|
|
|
|
|
|
const coupon = this.couponRepository.create({
|
|
|
couponId: response.coupon_id,
|
|
|
@@ -245,12 +146,8 @@ export class WechatPayService {
|
|
|
throw new Error('代金券不存在');
|
|
|
}
|
|
|
|
|
|
- const response = await this.wechatPayRequest(
|
|
|
- 'GET',
|
|
|
- `/v3/marketing/favor/users/${openid}/coupons/${couponId}?appid=${coupon.stock.config.appId}`
|
|
|
- );
|
|
|
-
|
|
|
- return response;
|
|
|
+ const client = await this.getWechatPayClient(coupon.stock.configId);
|
|
|
+ return client.getCouponDetail(couponId, openid);
|
|
|
}
|
|
|
|
|
|
// 查询代金券批次列表
|
|
|
@@ -260,24 +157,21 @@ export class WechatPayService {
|
|
|
stockCreatorMchid: string,
|
|
|
status?: string
|
|
|
): Promise<any> {
|
|
|
- const config = await this.getActiveConfig();
|
|
|
+ const activeConfig = await this.getActiveConfig();
|
|
|
+ const client = await this.getWechatPayClient(activeConfig.id);
|
|
|
|
|
|
- let url = `/v3/marketing/favor/stocks?offset=${offset}&limit=${limit}&stock_creator_mchid=${stockCreatorMchid}`;
|
|
|
- if (status) {
|
|
|
- url += `&status=${status}`;
|
|
|
- }
|
|
|
-
|
|
|
- const response = await this.wechatPayRequest('GET', url, undefined, config);
|
|
|
- return response;
|
|
|
+ return client.getStockList(offset, limit, stockCreatorMchid, status);
|
|
|
}
|
|
|
|
|
|
// 查询代金券批次详情
|
|
|
async getStockDetail(stockId: string, stockCreatorMchid: string): Promise<any> {
|
|
|
- const response = await this.wechatPayRequest(
|
|
|
- 'GET',
|
|
|
- `/v3/marketing/favor/stocks/${stockId}?stock_creator_mchid=${stockCreatorMchid}`
|
|
|
- );
|
|
|
- return response;
|
|
|
+ const stock = await this.stockRepository.findOne({ where: { stockId } });
|
|
|
+ if (!stock) {
|
|
|
+ throw new Error('代金券批次不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ const client = await this.getWechatPayClient(stock.configId);
|
|
|
+ return client.getStockDetail(stockId, stockCreatorMchid);
|
|
|
}
|
|
|
|
|
|
// 查询本地代金券批次列表
|