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 { return { openId: this.openIdFromCode(code), sessionKey: 'mock-session-key' }; } async oauth2AccessToken(code: string): Promise { 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>; } /** 直接把授权链接回跳到 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 { 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 { 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: '韵达速递' }, ]; } }