增加广播栏
This commit is contained in:
1 parent
85b0e70907
commit
d8786eb28a
21 files changed
+516
-59
No files matched your search
@@ -289,7 +289,8 @@ export class PaymentsService {
|
||||
* 由定时任务每 5 分钟调用一次。
|
||||
*/
|
||||
async reconcilePendingOrders(): Promise<number> {
|
||||
if (!this.wxpay.enabled) return 0;
|
||||
const source = this.prisma.activeEnv;
|
||||
if (!this.wxpay.enabledFor(source)) return 0;
|
||||
|
||||
const pending = await this.prisma.active.order.findMany({
|
||||
where: { status: { in: [OrderStatus.CREATED, OrderStatus.PAID] } },
|
||||
@@ -301,7 +302,7 @@ export class PaymentsService {
|
||||
let delivered = 0;
|
||||
for (const order of pending) {
|
||||
try {
|
||||
const result = await this.wxpay.queryOrder(order.out_trade_no, order.user.openid);
|
||||
const result = await this.wxpay.queryOrder(order.out_trade_no, order.user.openid, source);
|
||||
const wxOrderId = result.wx_order_id ?? result.order?.wx_order_id;
|
||||
// 只认「已支付待发货 / 发货中 / 已发货」这三种状态。
|
||||
// 不能用「有没有 wx_order_id」当判据——已关闭(6)和已退款(5)的单也有单号,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { createHmac } from 'node:crypto';
|
||||
import { APP_CONFIG, type AppConfig } from '../../config/config';
|
||||
import type { DataSourceEnv } from '../../common/admin-env';
|
||||
import { AppError } from '../../common/errors';
|
||||
|
||||
const XPAY_BASE = 'https://api.weixin.qq.com';
|
||||
@@ -39,27 +40,37 @@ export class WxPayService {
|
||||
|
||||
constructor(@Inject(APP_CONFIG) private readonly cfg: AppConfig) {}
|
||||
|
||||
/** 当前支付环境:0 现网 / 1 沙箱。 */
|
||||
get env(): number {
|
||||
return this.cfg.WX_PAY_ENV;
|
||||
/**
|
||||
* 取某个数据源用的支付配置。
|
||||
*
|
||||
* 生产库走现网 AppKey(WX_PAY_ENV 决定现网/沙箱),
|
||||
* 开发库走沙箱 AppKey,OfferID 默认与生产共用——微信本来就只有一个 OfferID,
|
||||
* 沙箱与现网的区别是 AppKey 加 env 值。
|
||||
*/
|
||||
configFor(source: DataSourceEnv): WxPayConfig {
|
||||
if (source === 'develop') {
|
||||
return {
|
||||
offerId: this.cfg.WX_OFFER_ID_DEVELOP || this.cfg.WX_OFFER_ID,
|
||||
appkey: this.cfg.WX_PAY_APPKEY_DEVELOP || this.cfg.WX_PAY_APPKEY_SANDBOX,
|
||||
env: this.cfg.WX_PAY_ENV_DEVELOP,
|
||||
};
|
||||
}
|
||||
return {
|
||||
offerId: this.cfg.WX_OFFER_ID,
|
||||
appkey: this.cfg.WX_PAY_ENV === 1 ? this.cfg.WX_PAY_APPKEY_SANDBOX : this.cfg.WX_PAY_APPKEY,
|
||||
env: this.cfg.WX_PAY_ENV,
|
||||
};
|
||||
}
|
||||
|
||||
/** 当前环境对应的支付密钥。 */
|
||||
private get appkey(): string {
|
||||
return this.env === 1 ? this.cfg.WX_PAY_APPKEY_SANDBOX : this.cfg.WX_PAY_APPKEY;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.cfg.WX_OFFER_ID !== '' && this.appkey !== '';
|
||||
}
|
||||
|
||||
get offerId(): string {
|
||||
return this.cfg.WX_OFFER_ID;
|
||||
/** 该数据源是否配好了支付。 */
|
||||
enabledFor(source: DataSourceEnv): boolean {
|
||||
const c = this.configFor(source);
|
||||
return c.offerId !== '' && c.appkey !== '';
|
||||
}
|
||||
|
||||
/** 服务端身份签名。uri 不带问号参数。 */
|
||||
calcPaySig(uri: string, postBody: string): string {
|
||||
return createHmac('sha256', this.appkey).update(`${uri}&${postBody}`).digest('hex');
|
||||
calcPaySig(uri: string, postBody: string, appkey: string): string {
|
||||
return createHmac('sha256', appkey).update(`${uri}&${postBody}`).digest('hex');
|
||||
}
|
||||
|
||||
/** 用户态签名。 */
|
||||
@@ -102,20 +113,21 @@ export class WxPayService {
|
||||
}
|
||||
|
||||
/** 拼出带 access_token 与 pay_sig 的接口地址。 */
|
||||
private async signedUrl(uri: string, postBody: string): Promise<string> {
|
||||
private async signedUrl(uri: string, postBody: string, cfg: WxPayConfig): Promise<string> {
|
||||
const token = await this.accessToken();
|
||||
return `${XPAY_BASE}${uri}?access_token=${encodeURIComponent(token)}&pay_sig=${this.calcPaySig(uri, postBody)}`;
|
||||
return `${XPAY_BASE}${uri}?access_token=${encodeURIComponent(token)}&pay_sig=${this.calcPaySig(uri, postBody, cfg.appkey)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查单。用于发货推送丢失时兜底补发。
|
||||
* 参数名是 order_id(传自己的 outTradeNo),不是 out_trade_no。
|
||||
*/
|
||||
async queryOrder(outTradeNo: string, openid: string): Promise<XpayQueryResult> {
|
||||
async queryOrder(outTradeNo: string, openid: string, source: DataSourceEnv): Promise<XpayQueryResult> {
|
||||
const cfg = this.configFor(source);
|
||||
const uri = '/xpay/query_order';
|
||||
const body = JSON.stringify({ openid, env: this.env, order_id: outTradeNo });
|
||||
const body = JSON.stringify({ openid, env: cfg.env, order_id: outTradeNo });
|
||||
|
||||
const res = await fetch(await this.signedUrl(uri, body), {
|
||||
const res = await fetch(await this.signedUrl(uri, body, cfg), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body,
|
||||
@@ -142,13 +154,14 @@ export class WxPayService {
|
||||
reasonCode: string;
|
||||
/** 商家自定义数据,必填,退款原因的文字记在这里,query_order 可原样查回 */
|
||||
bizMeta: string;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
if (!this.enabled) throw new AppError(4002, '虚拟支付未配置', 503);
|
||||
}, source: DataSourceEnv): Promise<Record<string, unknown>> {
|
||||
const cfg = this.configFor(source);
|
||||
if (!cfg.offerId || !cfg.appkey) throw new AppError(4002, '虚拟支付未配置', 503);
|
||||
|
||||
// left_fee 是「当前单剩余可退金额」,接口必填,只能先查单拿到。
|
||||
// 千万不要拿订单金额兜底——那等于编一个数发给微信,会被 268490016 打回来,
|
||||
// 而且把「这单根本不能退」这个真实原因盖掉了。
|
||||
const queried = await this.queryOrder(input.outTradeNo, input.openid);
|
||||
const queried = await this.queryOrder(input.outTradeNo, input.openid, source);
|
||||
const status = queried.order?.status;
|
||||
const statusText = status === undefined ? '未知' : XPAY_STATUS[status] ?? String(status);
|
||||
const leftFee = queried.order?.left_fee;
|
||||
@@ -183,10 +196,10 @@ export class WxPayService {
|
||||
refund_reason: input.reasonCode,
|
||||
/** 1-人工客服退款:后台管理员发起的都属于这一类 */
|
||||
req_from: '1',
|
||||
env: this.env,
|
||||
env: cfg.env,
|
||||
});
|
||||
|
||||
const res = await fetch(await this.signedUrl(uri, body), {
|
||||
const res = await fetch(await this.signedUrl(uri, body, cfg), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body,
|
||||
@@ -215,3 +228,10 @@ export interface XpayQueryResult {
|
||||
status?: number;
|
||||
wx_order_id?: string;
|
||||
}
|
||||
/** 某个数据源实际使用的支付配置。 */
|
||||
export interface WxPayConfig {
|
||||
offerId: string;
|
||||
appkey: string;
|
||||
/** 0 现网 / 1 沙箱 */
|
||||
env: number;
|
||||
}
|
||||
Reference in new issue
Block a user