Files
dukang/server/dukang-api/src/integrations/wechat/wechat.mock.provider.ts
T

133 lines
3.9 KiB
TypeScript

import { createHash } from 'crypto';
import { Injectable, NotImplementedException } from '@nestjs/common';
import * as QRCode from 'qrcode';
import type {
IWechatProvider,
WechatCodeSession,
WechatOAuthSession,
WechatWxaCodeUnlimitedInput,
} from './wechat.interface';
/**
* preV1 Mock 微信 Provider。
*
* 目的:让「微信授权登录」按钮在不接真实微信的情况下走通。前端仍按真实 OAuth 流程
* (跳转 oauth-url → 回调携带 code),Mock 端将授权 URL 直接回跳并返回稳定 openId。
* 后续填入 WX_APP_ID/WX_APP_SECRET 并置 MOCK_WECHAT=false 即切换到真实实现。
*/
@Injectable()
export class WechatMockProvider implements IWechatProvider {
isEnabled() {
return true;
}
isMock() {
return true;
}
isPayEnabled() {
return false;
}
getMchId() {
return '';
}
/** 由 code 派生稳定 openId,保证同一 code 多次授权指向同一账号 */
private openIdFromCode(code: string): string {
return `mockwx_${createHash('md5').update(code).digest('hex').slice(0, 24)}`;
}
async code2Session(code: string): Promise<WechatCodeSession> {
return { openId: this.openIdFromCode(code), sessionKey: 'mock-session-key' };
}
async oauth2AccessToken(code: string): Promise<WechatOAuthSession> {
return { openId: this.openIdFromCode(code), accessToken: 'mock-access-token' };
}
async fetchOAuthUserInfo(accessToken: string, openId: string) {
return {
openId,
nickname: 'Mock微信用户',
headImgUrl: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(openId)}`,
};
}
async createJssdkConfig(url: string) {
return {
appId: 'mock-appid',
timestamp: Math.floor(Date.now() / 1000),
nonceStr: 'mocknonce',
signature: 'mocksignature',
url,
jsApiList: ['getLocation', 'scanQRCode', 'chooseImage'],
} as unknown as Awaited<ReturnType<IWechatProvider['createJssdkConfig']>>;
}
/** 直接把授权链接回跳到 redirectUri 并附带 mock code,模拟微信授权完成 */
buildOAuthUrl(redirectUri: string, state: string): string {
const sep = redirectUri.includes('?') ? '&' : '?';
const code = `mockcode_${state || 'default'}`;
return `${redirectUri}${sep}code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
}
async getPhoneNumberByCode(): Promise<string> {
throw new NotImplementedException('Mock 微信不支持获取手机号,请用短信绑定');
}
createJsapiPrepay(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
createNativePrepay(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
parsePayNotification(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
createDomesticRefund(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
parseRefundNotification(): never {
throw new NotImplementedException('FEATURE_DISABLED');
}
/** Mock:用普通二维码 PNG 占位,内容含 scene,便于本地联调上传 OSS */
async getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
const scene = (input.scene ?? '').trim() || 'mock';
return QRCode.toBuffer(`mock-wxa://promo?scene=${encodeURIComponent(scene)}`, {
width: input.width ?? 430,
margin: 1,
type: 'png',
color: { dark: '#1f1a17', light: '#ffffff' },
});
}
async uploadShippingInfo() {
return { errcode: 0, errmsg: 'ok' };
}
async getOrderShippingInfo() {
return {
errcode: 0,
errmsg: 'ok',
orderState: 3,
finishShipping: true,
};
}
async getDeliveryList() {
return [
{ deliveryId: 'SF', deliveryName: '顺丰速运' },
{ deliveryId: 'STO', deliveryName: '申通快递' },
{ deliveryId: 'YTO', deliveryName: '圆通速递' },
{ deliveryId: 'ZTO', deliveryName: '中通快递' },
{ deliveryId: 'YD', deliveryName: '韵达速递' },
];
}
}