webadmin系统设置
This commit is contained in:
@@ -1,16 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { loadAppConfig } from '@dukang/shared-types';
|
||||
import { SmsCodeStore } from './sms/sms-code.store';
|
||||
import { SmsMockProvider } from './sms/sms.mock.provider';
|
||||
import { SmsAliyunProvider } from './sms/sms.aliyun.provider';
|
||||
import { SmsRouterProvider } from './sms/sms.router.provider';
|
||||
import { PayMockProvider } from './pay/pay.mock.provider';
|
||||
import { PayWechatProvider } from './pay/pay.wechat.provider';
|
||||
import { PayRouterProvider } from './pay/pay.router.provider';
|
||||
import { DeliveryMockProvider } from './delivery/delivery.mock.provider';
|
||||
import { WechatApiProvider } from './wechat/wechat.api.provider';
|
||||
import { WechatDisabledProvider } from './wechat/wechat.disabled.provider';
|
||||
import { WechatMockProvider } from './wechat/wechat.mock.provider';
|
||||
import { OssMockProvider } from './oss/oss.mock.provider';
|
||||
import { WechatRouterProvider } from './wechat/wechat.router.provider';
|
||||
import { OssAliyunProvider } from './oss/oss.aliyun.provider';
|
||||
import { TencentLbsProvider } from './map/tencent-lbs.provider';
|
||||
import {
|
||||
@@ -23,10 +24,6 @@ import {
|
||||
} from './integrations.constants';
|
||||
import { CourierModule } from './courier/courier.module';
|
||||
import { DELIVERY_QUEUE } from '../jobs/jobs.constants';
|
||||
import type { IWechatProvider } from './wechat/wechat.interface';
|
||||
import type { IPayProvider } from './pay/pay.interface';
|
||||
import type { IOssProvider } from './oss/oss.interface';
|
||||
import type { ISmsProvider } from './sms/sms.interface';
|
||||
|
||||
@Module({
|
||||
imports: [BullModule.registerQueue({ name: DELIVERY_QUEUE }), CourierModule],
|
||||
@@ -34,60 +31,20 @@ import type { ISmsProvider } from './sms/sms.interface';
|
||||
SmsCodeStore,
|
||||
SmsMockProvider,
|
||||
SmsAliyunProvider,
|
||||
{
|
||||
provide: SMS_PROVIDER,
|
||||
useFactory: (mock: SmsMockProvider, aliyun: SmsAliyunProvider): ISmsProvider => {
|
||||
const cfg = loadAppConfig();
|
||||
if (cfg.mockSms) return mock;
|
||||
if (!aliyun.isEnabled()) {
|
||||
throw new Error('MOCK_SMS=false but Aliyun SMS credentials are missing');
|
||||
}
|
||||
return aliyun;
|
||||
},
|
||||
inject: [SmsMockProvider, SmsAliyunProvider],
|
||||
},
|
||||
SmsRouterProvider,
|
||||
{ provide: SMS_PROVIDER, useExisting: SmsRouterProvider },
|
||||
WechatApiProvider,
|
||||
WechatDisabledProvider,
|
||||
WechatMockProvider,
|
||||
{
|
||||
provide: WECHAT_PROVIDER,
|
||||
useFactory: (
|
||||
api: WechatApiProvider,
|
||||
disabled: WechatDisabledProvider,
|
||||
mock: WechatMockProvider,
|
||||
): IWechatProvider => {
|
||||
const cfg = loadAppConfig();
|
||||
const enabled =
|
||||
(cfg.wechatAuthEnabled || cfg.wechatPayEnabled) &&
|
||||
(!!cfg.wxAppId || !!process.env.WX_MCH_ID);
|
||||
if (enabled) return api;
|
||||
// preV1:真实微信未配置但开启 Mock 授权登录
|
||||
if (cfg.mockWechat) return mock;
|
||||
return disabled;
|
||||
},
|
||||
inject: [WechatApiProvider, WechatDisabledProvider, WechatMockProvider],
|
||||
},
|
||||
WechatRouterProvider,
|
||||
{ provide: WECHAT_PROVIDER, useExisting: WechatRouterProvider },
|
||||
PayMockProvider,
|
||||
PayWechatProvider,
|
||||
{
|
||||
provide: PAY_PROVIDER,
|
||||
useFactory: (mock: PayMockProvider, wechat: PayWechatProvider): IPayProvider => {
|
||||
const cfg = loadAppConfig();
|
||||
return cfg.mockPay ? mock : wechat;
|
||||
},
|
||||
inject: [PayMockProvider, PayWechatProvider],
|
||||
},
|
||||
PayRouterProvider,
|
||||
{ provide: PAY_PROVIDER, useExisting: PayRouterProvider },
|
||||
{ provide: DELIVERY_PROVIDER, useClass: DeliveryMockProvider },
|
||||
OssMockProvider,
|
||||
OssAliyunProvider,
|
||||
{
|
||||
provide: OSS_PROVIDER,
|
||||
useFactory: (mock: OssMockProvider, aliyun: OssAliyunProvider): IOssProvider => {
|
||||
const cfg = loadAppConfig();
|
||||
return cfg.ossEnabled && aliyun.isEnabled() ? aliyun : mock;
|
||||
},
|
||||
inject: [OssMockProvider, OssAliyunProvider],
|
||||
},
|
||||
{ provide: OSS_PROVIDER, useExisting: OssAliyunProvider },
|
||||
DeliveryMockProvider,
|
||||
TencentLbsProvider,
|
||||
{ provide: MAP_PROVIDER, useExisting: TencentLbsProvider },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import type {
|
||||
IOssProvider,
|
||||
OssPutObjectInput,
|
||||
@@ -30,10 +30,15 @@ export class OssAliyunProvider implements IOssProvider {
|
||||
return !!(this.accessKeyId && this.accessKeySecret && this.bucket);
|
||||
}
|
||||
|
||||
private assertConfigured() {
|
||||
if (this.isEnabled()) return;
|
||||
throw new InternalServerErrorException(
|
||||
'OSS 未配置:请设置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_BUCKET(及 OSS_REGION)',
|
||||
);
|
||||
}
|
||||
|
||||
private getClient() {
|
||||
if (!this.isEnabled()) {
|
||||
throw new Error('OSS credentials are not configured');
|
||||
}
|
||||
this.assertConfigured();
|
||||
if (!this.client) {
|
||||
this.client = createAliyunOssClient({
|
||||
accessKeyId: this.accessKeyId,
|
||||
@@ -49,14 +54,8 @@ export class OssAliyunProvider implements IOssProvider {
|
||||
|
||||
buildPublicUrl(ossKey: string) {
|
||||
const key = ossKey.replace(/^\//, '');
|
||||
const client = this.isEnabled() ? this.getClient() : null;
|
||||
if (client) {
|
||||
return client.generateObjectUrl(key, this.cdnBase || undefined);
|
||||
}
|
||||
if (this.cdnBase) {
|
||||
return `${this.cdnBase.replace(/\/$/, '')}/${key}`;
|
||||
}
|
||||
return `${resolveOssUploadHost(this.bucket, this.region)}/${key}`;
|
||||
const client = this.getClient();
|
||||
return client.generateObjectUrl(key, this.cdnBase || undefined);
|
||||
}
|
||||
|
||||
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
|
||||
|
||||
@@ -4,10 +4,8 @@ import type { IPayProvider, PayOrderResult } from './pay.interface';
|
||||
|
||||
@Injectable()
|
||||
export class PayMockProvider implements IPayProvider {
|
||||
private readonly config = loadAppConfig();
|
||||
|
||||
async payOrder(_orderId: bigint, _openId?: string): Promise<PayOrderResult> {
|
||||
if (!this.config.mockPay) {
|
||||
if (!loadAppConfig().mockPay) {
|
||||
throw new Error('Real WeChat pay requires PayWechatProvider');
|
||||
}
|
||||
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { loadAppConfig } from '@dukang/shared-types';
|
||||
import type { IPayProvider, PayOrderResult } from './pay.interface';
|
||||
import { PayMockProvider } from './pay.mock.provider';
|
||||
import { PayWechatProvider } from './pay.wechat.provider';
|
||||
|
||||
/** 按当前 process.env 动态选择 Mock / 微信 JSAPI 支付 */
|
||||
@Injectable()
|
||||
export class PayRouterProvider implements IPayProvider {
|
||||
constructor(
|
||||
private readonly mock: PayMockProvider,
|
||||
private readonly wechat: PayWechatProvider,
|
||||
) {}
|
||||
|
||||
private resolve(): IPayProvider {
|
||||
return loadAppConfig().mockPay ? this.mock : this.wechat;
|
||||
}
|
||||
|
||||
payOrder(orderId: bigint, openId?: string): Promise<PayOrderResult> {
|
||||
return this.resolve().payOrder(orderId, openId);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import type { IPayProvider, PayOrderResult } from './pay.interface';
|
||||
@Injectable()
|
||||
export class PayWechatProvider implements IPayProvider {
|
||||
private readonly logger = new Logger(PayWechatProvider.name);
|
||||
private readonly config = loadAppConfig();
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@@ -16,11 +15,11 @@ export class PayWechatProvider implements IPayProvider {
|
||||
) {}
|
||||
|
||||
async payOrder(orderId: bigint, openId?: string): Promise<PayOrderResult> {
|
||||
if (this.config.mockPay) {
|
||||
if (loadAppConfig().mockPay) {
|
||||
return { mode: 'mock', externalNo: `MOCK-${Date.now()}` };
|
||||
}
|
||||
if (!this.wechat.isPayEnabled()) {
|
||||
throw new Error('微信支付未配置:请设置 WECHAT_PAY_ENABLED=true 与 WX_MCH_ID 等商户参数');
|
||||
throw new Error('微信支付未配置:请关闭 MOCK_PAY 并配置 WX_MCH_ID 等商户参数');
|
||||
}
|
||||
if (!openId) {
|
||||
throw new Error('微信支付需要用户 openId,请先完成微信授权登录');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { SMS_CODE_TTL_SECONDS, loadAppConfig } from '@dukang/shared-types';
|
||||
import { SMS_CODE_TTL_SECONDS } from '@dukang/shared-types';
|
||||
import { RedisService } from '../../common/redis/redis.service';
|
||||
|
||||
const RATE_TTL_SECONDS = 60;
|
||||
@@ -18,8 +18,6 @@ function randomSixDigitCode() {
|
||||
|
||||
@Injectable()
|
||||
export class SmsCodeStore {
|
||||
private readonly config = loadAppConfig();
|
||||
|
||||
constructor(private readonly redis: RedisService) {}
|
||||
|
||||
async assertSendCooldown(phone: string) {
|
||||
@@ -34,7 +32,7 @@ export class SmsCodeStore {
|
||||
}
|
||||
|
||||
async generateAndStore(phone: string, scene: string): Promise<string> {
|
||||
const code = this.config.mockSms ? this.config.mockSmsCode : randomSixDigitCode();
|
||||
const code = randomSixDigitCode();
|
||||
await this.redis.client.set(codeKey(phone, scene), code, 'EX', SMS_CODE_TTL_SECONDS);
|
||||
return code;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { MockSmsCodeService } from '../../common/mock-sms-code/mock-sms-code.service';
|
||||
import type { ISmsProvider, SmsActorRef, SmsSendResult } from './sms.interface';
|
||||
import { SmsCodeStore } from './sms-code.store';
|
||||
|
||||
@@ -14,10 +15,12 @@ export class SmsMockProvider implements ISmsProvider {
|
||||
constructor(
|
||||
private readonly smsCodeStore: SmsCodeStore,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mockSmsCodeService: MockSmsCodeService,
|
||||
) {}
|
||||
|
||||
async send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult> {
|
||||
const code = await this.smsCodeStore.generateAndStore(phone, scene);
|
||||
await this.mockSmsCodeService.record(phone, scene, code);
|
||||
const masked = maskPhone(phone);
|
||||
this.logger.log(`Mock SMS → ${masked} scene=${scene} code=${code}`);
|
||||
|
||||
@@ -28,7 +31,7 @@ export class SmsMockProvider implements ISmsProvider {
|
||||
refType: actorRef?.refType,
|
||||
refId: actorRef?.refId,
|
||||
requestBody: { phone: masked, mode: 'MOCK', scene },
|
||||
responseBody: { mock: true, hint: 'use MOCK_SMS_CODE or check server log' },
|
||||
responseBody: { mock: true, hint: 'see HQ system settings mock SMS list' },
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { loadAppConfig } from '@dukang/shared-types';
|
||||
import type { ISmsProvider, SmsActorRef, SmsSendResult } from './sms.interface';
|
||||
import { SmsMockProvider } from './sms.mock.provider';
|
||||
import { SmsAliyunProvider } from './sms.aliyun.provider';
|
||||
|
||||
/** 按当前 process.env 动态选择 Mock / 阿里云短信 */
|
||||
@Injectable()
|
||||
export class SmsRouterProvider implements ISmsProvider {
|
||||
constructor(
|
||||
private readonly mock: SmsMockProvider,
|
||||
private readonly aliyun: SmsAliyunProvider,
|
||||
) {}
|
||||
|
||||
private resolve(): ISmsProvider {
|
||||
const cfg = loadAppConfig();
|
||||
if (cfg.mockSms) return this.mock;
|
||||
if (!this.aliyun.isEnabled()) {
|
||||
throw new Error('MOCK_SMS=false but Aliyun SMS credentials are missing');
|
||||
}
|
||||
return this.aliyun;
|
||||
}
|
||||
|
||||
send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult> {
|
||||
return this.resolve().send(phone, scene, actorRef);
|
||||
}
|
||||
|
||||
verify(phone: string, code: string, scene: string): Promise<void> {
|
||||
return this.resolve().verify(phone, code, scene);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ const JSAPI_TICKET_KEY = 'wechat:jsapi_ticket';
|
||||
@Injectable()
|
||||
export class WechatApiProvider implements IWechatProvider {
|
||||
private readonly logger = new Logger(WechatApiProvider.name);
|
||||
private readonly config = loadAppConfig();
|
||||
private readonly appId = process.env.WX_APP_ID ?? '';
|
||||
private readonly appSecret = process.env.WX_APP_SECRET ?? '';
|
||||
/** 小程序独立凭证;未配置时回退公众号/H5 的 WX_APP_ID(须与开发者工具 appid 一致) */
|
||||
@@ -39,7 +38,8 @@ export class WechatApiProvider implements IWechatProvider {
|
||||
) {}
|
||||
|
||||
isEnabled() {
|
||||
return this.config.wechatAuthEnabled && !!this.appId && !!this.appSecret;
|
||||
const cfg = loadAppConfig();
|
||||
return !cfg.mockWechat && !!this.appId && !!this.appSecret;
|
||||
}
|
||||
|
||||
isMock() {
|
||||
@@ -47,8 +47,9 @@ export class WechatApiProvider implements IWechatProvider {
|
||||
}
|
||||
|
||||
isPayEnabled() {
|
||||
const cfg = loadAppConfig();
|
||||
return (
|
||||
this.config.wechatPayEnabled &&
|
||||
!cfg.mockPay &&
|
||||
!!this.appId &&
|
||||
!!this.mchId &&
|
||||
!!this.mchSerialNo &&
|
||||
@@ -290,7 +291,7 @@ export class WechatApiProvider implements IWechatProvider {
|
||||
}) {
|
||||
if (!this.isPayEnabled()) {
|
||||
throw new InternalServerErrorException(
|
||||
'微信支付未配置:请设置 WECHAT_PAY_ENABLED=true、WX_MCH_ID、WX_MCH_SERIAL_NO、WX_MCH_PRIVATE_KEY、WX_API_V3_KEY',
|
||||
'微信支付未配置:请关闭 MOCK_PAY 并配置 WX_MCH_ID、WX_MCH_SERIAL_NO、WX_MCH_PRIVATE_KEY、WX_API_V3_KEY',
|
||||
);
|
||||
}
|
||||
const notifyUrl = params.notifyUrl || this.notifyUrl;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { loadAppConfig, needsRealWechatApi } from '@dukang/shared-types';
|
||||
import type { IWechatProvider } from './wechat.interface';
|
||||
import { WechatApiProvider } from './wechat.api.provider';
|
||||
import { WechatDisabledProvider } from './wechat.disabled.provider';
|
||||
import { WechatMockProvider } from './wechat.mock.provider';
|
||||
|
||||
/** 按当前 process.env 动态选择 Mock / 真实微信 / 禁用 */
|
||||
@Injectable()
|
||||
export class WechatRouterProvider implements IWechatProvider {
|
||||
constructor(
|
||||
private readonly api: WechatApiProvider,
|
||||
private readonly disabled: WechatDisabledProvider,
|
||||
private readonly mock: WechatMockProvider,
|
||||
) {}
|
||||
|
||||
private resolve(): IWechatProvider {
|
||||
const cfg = loadAppConfig();
|
||||
if (needsRealWechatApi(cfg)) return this.api;
|
||||
if (cfg.mockWechat) return this.mock;
|
||||
return this.disabled;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.resolve().isEnabled();
|
||||
}
|
||||
|
||||
isMock() {
|
||||
return this.resolve().isMock();
|
||||
}
|
||||
|
||||
isPayEnabled() {
|
||||
return this.resolve().isPayEnabled();
|
||||
}
|
||||
|
||||
getMchId() {
|
||||
return this.resolve().getMchId();
|
||||
}
|
||||
|
||||
code2Session(code: string, actorRef?: { refType: string; refId: bigint }) {
|
||||
return this.resolve().code2Session(code, actorRef);
|
||||
}
|
||||
|
||||
oauth2AccessToken(code: string, actorRef?: { refType: string; refId: bigint }) {
|
||||
return this.resolve().oauth2AccessToken(code, actorRef);
|
||||
}
|
||||
|
||||
fetchOAuthUserInfo(
|
||||
accessToken: string,
|
||||
openId: string,
|
||||
actorRef?: { refType: string; refId: bigint },
|
||||
) {
|
||||
return this.resolve().fetchOAuthUserInfo(accessToken, openId, actorRef);
|
||||
}
|
||||
|
||||
createJssdkConfig(url: string, actorRef?: { refType: string; refId: bigint }) {
|
||||
return this.resolve().createJssdkConfig(url, actorRef);
|
||||
}
|
||||
|
||||
buildOAuthUrl(redirectUri: string, state: string, scope?: string) {
|
||||
return this.resolve().buildOAuthUrl(redirectUri, state, scope);
|
||||
}
|
||||
|
||||
getPhoneNumberByCode(
|
||||
code: string,
|
||||
platform: 'mini' | 'h5',
|
||||
actorRef?: { refType: string; refId: bigint },
|
||||
) {
|
||||
return this.resolve().getPhoneNumberByCode(code, platform, actorRef);
|
||||
}
|
||||
|
||||
createJsapiPrepay(params: {
|
||||
orderNo: string;
|
||||
description: string;
|
||||
amountFen: number;
|
||||
openId: string;
|
||||
notifyUrl: string;
|
||||
}) {
|
||||
return this.resolve().createJsapiPrepay(params);
|
||||
}
|
||||
|
||||
parsePayNotification(
|
||||
headers: Record<string, string | string[] | undefined>,
|
||||
rawBody: string,
|
||||
) {
|
||||
return this.resolve().parsePayNotification(headers, rawBody);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user