From 9d221cfcd277b531801f98f7a41aa9998ea386b5 Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Mon, 6 Jul 2026 20:56:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=8E=88=E6=9D=83=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/PhoneVerifySheet.tsx | 48 ++++- apps/h5-user/src/lib/wechat-auth.ts | 46 +++++ apps/h5-user/src/pages/MinePage.tsx | 27 +-- apps/h5-user/src/pages/OrderConfirmPage.tsx | 59 ++++++ apps/h5-user/src/pages/PayPage.tsx | 38 ++-- apps/h5-user/src/styles.css | 20 ++ .../wechat/wechat.api.provider.ts | 45 +++++ .../wechat/wechat.disabled.provider.ts | 4 + .../integrations/wechat/wechat.interface.ts | 14 ++ .../src/modules/iam/auth.controller.ts | 12 ++ .../src/modules/iam/auth.service.ts | 175 ++++++++++++++++++ .../src/modules/iam/dto/auth.dto.ts | 15 ++ 12 files changed, 475 insertions(+), 28 deletions(-) create mode 100644 apps/h5-user/src/lib/wechat-auth.ts diff --git a/apps/h5-user/src/components/PhoneVerifySheet.tsx b/apps/h5-user/src/components/PhoneVerifySheet.tsx index 17e236b..6db9910 100644 --- a/apps/h5-user/src/components/PhoneVerifySheet.tsx +++ b/apps/h5-user/src/components/PhoneVerifySheet.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; +import type { WechatLoginResult } from '@dukang/shared-types'; import { SmsScene } from '@dukang/shared-types'; -import { bindPhone, type SessionPayload } from '../lib/api'; +import { bindPhone, request, type SessionPayload } from '../lib/api'; import { normalizePhoneInput, validateMobilePhone } from '../lib/phone'; import { useSmsCode } from '../lib/use-sms-code'; import { useUserSession } from '../contexts/UserSessionContext'; @@ -9,6 +10,10 @@ type PhoneVerifySheetProps = { open: boolean; /** 打开时预填手机号(如收货地址中的手机号) */ defaultPhone?: string; + mode?: 'bind_phone' | 'wechat_bind_phone'; + wxSessionKey?: string; + title?: string; + description?: string; onClose: () => void; onSuccess: () => void; }; @@ -16,6 +21,10 @@ type PhoneVerifySheetProps = { export default function PhoneVerifySheet({ open, defaultPhone, + mode = 'bind_phone', + wxSessionKey, + title, + description, onClose, onSuccess, }: PhoneVerifySheetProps) { @@ -59,8 +68,28 @@ export default function PhoneVerifySheet({ setLoading(true); setError(''); try { - const session = await bindPhone(phone, code); - applySession(session as SessionPayload); + if (mode === 'wechat_bind_phone') { + if (!wxSessionKey) { + setError('微信会话已过期,请重新授权'); + return; + } + const data = await request('USER_H5', '/auth/wechat/bind-phone', { + method: 'POST', + body: JSON.stringify({ wxSessionKey, phone, code }), + }); + if (data.accessToken) { + applySession({ + accessToken: data.accessToken, + refreshToken: data.refreshToken ?? '', + deviceKey: data.deviceKey, + phoneVerified: !!data.phoneVerified, + user: data.user as SessionPayload['user'], + }); + } + } else { + const session = await bindPhone(phone, code); + applySession(session as SessionPayload); + } onSuccess(); onClose(); } catch (e) { @@ -72,12 +101,19 @@ export default function PhoneVerifySheet({ if (!open) return null; + const sheetTitle = title ?? (mode === 'wechat_bind_phone' ? '绑定手机号' : '验证手机号'); + const sheetDesc = + description ?? + (mode === 'wechat_bind_phone' + ? '微信授权成功,请绑定手机号以完成支付' + : '下单前需验证手机号,以便接收订单通知'); + return (
diff --git a/apps/h5-user/src/lib/wechat-auth.ts b/apps/h5-user/src/lib/wechat-auth.ts new file mode 100644 index 0000000..697605e --- /dev/null +++ b/apps/h5-user/src/lib/wechat-auth.ts @@ -0,0 +1,46 @@ +import type { WechatLoginResult } from '@dukang/shared-types'; +import { isWechatEnv, weixinSdk } from './weixin'; +import { + authorizeWechatForPay, + fetchClientConfig, + fetchUserProfile, + needsWechatAuthForPay, + saveWechatLoginResult, +} from './pay-wechat'; + +export type WechatAuthEnsureResult = + | { ok: true } + | { ok: false; redirecting: true } + | { ok: false; needBindPhone: true; wxSessionKey: string }; + +export async function checkNeedsWechatAuth(): Promise { + const [config, profile] = await Promise.all([fetchClientConfig(), fetchUserProfile()]); + return needsWechatAuthForPay(config, profile); +} + +/** 真实微信支付前确保已绑定微信;OAuth 跳转时返回 redirecting */ +export async function ensureWechatAuthForPay(): Promise { + if (!isWechatEnv()) return { ok: true }; + if (!(await checkNeedsWechatAuth())) return { ok: true }; + + const result = await authorizeWechatForPay(); + if (!result) return { ok: false, redirecting: true }; + + if (result.needBindPhone && result.wxSessionKey) { + return { ok: false, needBindPhone: true, wxSessionKey: result.wxSessionKey }; + } + + if (saveWechatLoginResult(result)) { + return { ok: true }; + } + return { ok: false, redirecting: true }; +} + +export async function handleWechatAuthCallback(): Promise { + if (!isWechatEnv()) return null; + return weixinSdk.handleOAuthCallback(); +} + +export function applyWechatLoginResult(result: WechatLoginResult): boolean { + return saveWechatLoginResult(result); +} diff --git a/apps/h5-user/src/pages/MinePage.tsx b/apps/h5-user/src/pages/MinePage.tsx index 73f1145..bd7a777 100644 --- a/apps/h5-user/src/pages/MinePage.tsx +++ b/apps/h5-user/src/pages/MinePage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import TabMainHeader from '../components/TabMainHeader'; import AppImage from '@dukang/shared-ui/AppImage'; -import { request } from '../lib/api'; +import { request, type UserProfile } from '../lib/api'; import { useUserSession } from '../contexts/UserSessionContext'; import ContactCustomerSheet from '../components/ContactCustomerSheet'; @@ -29,10 +29,8 @@ function formatMoney(amount: number) { export default function MinePage() { const navigate = useNavigate(); - const { profile: sessionProfile, resetSession } = useUserSession(); - const [profile, setProfile] = useState | null>( - sessionProfile as Record | null, - ); + const { profile: sessionProfile, refreshProfile, resetSession } = useUserSession(); + const [profile, setProfile] = useState(sessionProfile); const [benefitBalance, setBenefitBalance] = useState(0); const [orderCounts, setOrderCounts] = useState>({}); const [toast, setToast] = useState(''); @@ -40,7 +38,7 @@ export default function MinePage() { useEffect(() => { Promise.all([ - request>('USER_H5', '/auth/me'), + request('USER_H5', '/auth/me'), request>>('USER_H5', '/benefit/coupons'), ...ORDER_SHORTCUTS.map((s) => request<{ total: number }>('USER_H5', `/trade/orders?tab=${s.tab}&pageSize=1`), @@ -48,6 +46,7 @@ export default function MinePage() { ]) .then(([me, coupons, ...totals]) => { setProfile(me); + void refreshProfile(); const balance = coupons.reduce((sum, c) => { if (String(c.status) === 'ACTIVE') return sum + Number(c.balance || 0); return sum; @@ -60,7 +59,7 @@ export default function MinePage() { setOrderCounts(counts); }) .catch(() => {}); - }, []); + }, [refreshProfile]); function showToast(msg: string) { setToast(msg); @@ -77,9 +76,10 @@ export default function MinePage() { void resetSession().then(() => navigate('/')); } - const nickname = String(profile?.nickname || '用户'); - const userNo = String(profile?.userNo || ''); - const avatar = String(profile?.avatarUrl || DEFAULT_AVATAR); + const nickname = profile?.nickname || '用户'; + const userNo = profile?.userNo || ''; + const avatar = profile?.avatarUrl || DEFAULT_AVATAR; + const hasWechat = !!profile?.hasWechat; return (
@@ -89,12 +89,17 @@ export default function MinePage() {
+ {hasWechat && ( + + chat + + )}

{nickname}

{userNo && ID: {userNo}} - 好客会员 + {hasWechat ? '微信会员' : '好客会员'}
); } diff --git a/apps/h5-user/src/pages/PayPage.tsx b/apps/h5-user/src/pages/PayPage.tsx index 080b156..92f1ff0 100644 --- a/apps/h5-user/src/pages/PayPage.tsx +++ b/apps/h5-user/src/pages/PayPage.tsx @@ -1,19 +1,20 @@ import { useCallback, useEffect, useState } from 'react'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; -import type { WechatLoginResult, WechatPayOrderResult } from '@dukang/shared-types'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import type { WechatPayOrderResult } from '@dukang/shared-types'; import SubPageHeader from '../components/SubPageHeader'; +import PhoneVerifySheet from '../components/PhoneVerifySheet'; import { request } from '../lib/api'; import { buildOrderConfirmUrl } from '../lib/navigation'; import { authorizeWechatForPay, - buildLoginReturnUrl, fetchClientConfig, fetchUserProfile, isWechatAuthRequiredError, needsWechatAuthForPay, saveWechatLoginResult, } from '../lib/pay-wechat'; -import { isWechatEnv, weixinSdk } from '../lib/weixin'; +import { applyWechatLoginResult, handleWechatAuthCallback } from '../lib/wechat-auth'; +import { isWechatEnv } from '../lib/weixin'; function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -30,7 +31,6 @@ async function waitOrderPaid(orderId: string, maxAttempts = 15) { export default function PayPage() { const [params] = useSearchParams(); - const location = useLocation(); const orderId = params.get('orderId') || ''; const navigate = useNavigate(); const [loading, setLoading] = useState(false); @@ -38,6 +38,8 @@ export default function PayPage() { const [mockMode, setMockMode] = useState(true); const [needsWechatAuth, setNeedsWechatAuth] = useState(false); const [msg, setMsg] = useState(''); + const [showBindPhone, setShowBindPhone] = useState(false); + const [wxSessionKey, setWxSessionKey] = useState(null); const refreshPayReadiness = useCallback(async () => { try { @@ -51,10 +53,11 @@ export default function PayPage() { }, []); const handleWechatLoginResult = useCallback( - async (result: WechatLoginResult) => { + async (result: Parameters[0]) => { if (result.needBindPhone && result.wxSessionKey) { - setMsg('微信授权成功,请先绑定手机号'); - navigate(buildLoginReturnUrl(location.pathname, location.search)); + setWxSessionKey(result.wxSessionKey); + setShowBindPhone(true); + setMsg(''); return; } if (saveWechatLoginResult(result)) { @@ -62,7 +65,7 @@ export default function PayPage() { await refreshPayReadiness(); } }, - [location.pathname, location.search, navigate, refreshPayReadiness], + [refreshPayReadiness], ); useEffect(() => { @@ -71,8 +74,7 @@ export default function PayPage() { useEffect(() => { if (!isWechatEnv()) return; - weixinSdk - .handleOAuthCallback() + handleWechatAuthCallback() .then((result) => { if (result) void handleWechatLoginResult(result); }) @@ -121,6 +123,7 @@ export default function PayPage() { if (result.mode === 'jsapi' && result.prepay) { setMockMode(false); + const { weixinSdk } = await import('../lib/weixin'); await weixinSdk.pay(result.prepay); const paid = await waitOrderPaid(orderId); if (!paid) { @@ -190,6 +193,19 @@ export default function PayPage() { {loading ? '支付中...' : needsWechatAuth ? '请先授权微信' : '确认支付'}
+ + { + setShowBindPhone(false); + setWxSessionKey(null); + }} + onSuccess={() => { + void refreshPayReadiness(); + }} + /> ); } diff --git a/apps/h5-user/src/styles.css b/apps/h5-user/src/styles.css index 7507457..9fee4b3 100644 --- a/apps/h5-user/src/styles.css +++ b/apps/h5-user/src/styles.css @@ -4307,6 +4307,26 @@ flex-shrink: 0; } +.mine-wechat-badge { + position: absolute; + right: -2px; + bottom: -2px; + width: 24px; + height: 24px; + border-radius: 50%; + background: #07c160; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); +} + +.mine-wechat-badge .material-symbols-outlined { + font-size: 14px; +} + .mine-avatar { width: 80px; height: 80px; diff --git a/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts b/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts index 2ee5d4d..ca8a0d6 100644 --- a/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts +++ b/server/dukang-api/src/integrations/wechat/wechat.api.provider.ts @@ -149,6 +149,51 @@ export class WechatApiProvider implements IWechatProvider { }; } + async fetchOAuthUserInfo( + accessToken: string, + openId: string, + actorRef?: WechatActorRef, + ): Promise { + const maskedUrl = new URL('https://api.weixin.qq.com/sns/userinfo'); + maskedUrl.searchParams.set('access_token', '***'); + maskedUrl.searchParams.set('openid', openId); + maskedUrl.searchParams.set('lang', 'zh_CN'); + const apiUrl = new URL('https://api.weixin.qq.com/sns/userinfo'); + apiUrl.searchParams.set('access_token', accessToken); + apiUrl.searchParams.set('openid', openId); + apiUrl.searchParams.set('lang', 'zh_CN'); + const data = await this.fetchJson<{ + openid?: string; + nickname?: string; + headimgurl?: string; + unionid?: string; + errcode?: number; + errmsg?: string; + }>(apiUrl.toString()); + const ok = !!data.openid; + await logWechatAuth(this.prisma, { + scene: 'USERINFO', + requestUrl: maskedUrl.toString(), + requestBody: { lang: 'zh_CN' }, + responseBody: ok + ? { openid: data.openid, nickname: data.nickname, unionid: data.unionid } + : { errcode: data.errcode, errmsg: data.errmsg }, + externalNo: data.openid ?? openId, + status: ok ? 'SUCCESS' : 'FAILED', + errorMessage: ok ? undefined : data.errmsg || '微信用户信息获取失败', + actorRef, + }); + if (!data.openid) { + throw new InternalServerErrorException(data.errmsg || '微信用户信息获取失败'); + } + return { + openId: data.openid, + nickname: data.nickname, + headImgUrl: data.headimgurl, + unionId: data.unionid, + }; + } + async createJssdkConfig(url: string, actorRef?: WechatActorRef) { try { const ticket = await this.getJsapiTicket(); diff --git a/server/dukang-api/src/integrations/wechat/wechat.disabled.provider.ts b/server/dukang-api/src/integrations/wechat/wechat.disabled.provider.ts index ae28645..9ab566f 100644 --- a/server/dukang-api/src/integrations/wechat/wechat.disabled.provider.ts +++ b/server/dukang-api/src/integrations/wechat/wechat.disabled.provider.ts @@ -27,6 +27,10 @@ export class WechatDisabledProvider implements IWechatProvider { return this.disabled(); } + fetchOAuthUserInfo() { + return this.disabled(); + } + createJssdkConfig() { return this.disabled(); } diff --git a/server/dukang-api/src/integrations/wechat/wechat.interface.ts b/server/dukang-api/src/integrations/wechat/wechat.interface.ts index 4eed1e3..ad4efd1 100644 --- a/server/dukang-api/src/integrations/wechat/wechat.interface.ts +++ b/server/dukang-api/src/integrations/wechat/wechat.interface.ts @@ -14,6 +14,13 @@ export type WechatOAuthSession = { refreshToken?: string; }; +export type WechatOAuthUserInfo = { + openId: string; + nickname?: string; + headImgUrl?: string; + unionId?: string; +}; + export type WechatPayNotifyResult = { transactionId: string; outTradeNo: string; @@ -36,6 +43,13 @@ export interface IWechatProvider { /** 公众号 H5 OAuth code 换 openId */ oauth2AccessToken(code: string, actorRef?: { refType: string; refId: bigint }): Promise; + /** 公众号 OAuth access_token 拉取用户昵称头像(snsapi_userinfo) */ + fetchOAuthUserInfo( + accessToken: string, + openId: string, + actorRef?: { refType: string; refId: bigint }, + ): Promise; + /** JSSDK 签名配置 */ createJssdkConfig(url: string, actorRef?: { refType: string; refId: bigint }): Promise; diff --git a/server/dukang-api/src/modules/iam/auth.controller.ts b/server/dukang-api/src/modules/iam/auth.controller.ts index 66a7722..842a242 100644 --- a/server/dukang-api/src/modules/iam/auth.controller.ts +++ b/server/dukang-api/src/modules/iam/auth.controller.ts @@ -3,6 +3,7 @@ import type { Request } from 'express'; import { AuthService } from './auth.service'; import { BindPhoneDto, + BindWechatDto, BindWechatPhoneDto, BootstrapSessionDto, LoginSmsDto, @@ -78,6 +79,17 @@ export class UserAuthController { ); } + @Post('auth/wechat/bind') + @UseGuards(JwtAuthGuard) + bindWechat(@CurrentUser() user: AuthUser, @Body() dto: BindWechatDto) { + return this.authService.bindUserWechat( + user.actorId, + { code: dto.code, wxSessionKey: dto.wxSessionKey }, + ClientApp.USER_H5, + dto.platform ?? 'h5', + ); + } + @Get('auth/me') @UseGuards(JwtAuthGuard) me(@CurrentUser() user: AuthUser) { diff --git a/server/dukang-api/src/modules/iam/auth.service.ts b/server/dukang-api/src/modules/iam/auth.service.ts index bd98775..4e10256 100644 --- a/server/dukang-api/src/modules/iam/auth.service.ts +++ b/server/dukang-api/src/modules/iam/auth.service.ts @@ -28,6 +28,7 @@ type WxSessionPayload = { openId: string; unionId?: string; sessionKey?: string; + accessToken?: string; clientApp: ClientApp; guestId?: string; }; @@ -512,6 +513,10 @@ export class AuthService { }, include: { avatar: true }, }); + if (session.accessToken) { + const synced = await this.syncWechatUserProfile(activeUser.id, session.accessToken, session.openId); + if (synced) activeUser = synced as UserRow; + } this.analyticsService.trackOneSafe(activeUser.id, clientApp, { eventName: 'wechat_login', extraJson: { platform }, @@ -523,6 +528,18 @@ export class AuthService { return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey); } + if (guestId) { + try { + const guest = await this.assertActiveUser(guestId); + if (guest.phoneVerifiedAt && !guest.wxOpenId) { + const activeUser = await this.attachWechatToUser(guest.id, session, clientApp, platform); + return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey); + } + } catch (e) { + if (e instanceof BadRequestException) throw e; + } + } + const wxSessionKey = randomUUID(); await this.redis.setJson( `wx:session:${wxSessionKey}`, @@ -530,6 +547,7 @@ export class AuthService { openId: session.openId, unionId: session.unionId, sessionKey: 'sessionKey' in session ? session.sessionKey : undefined, + accessToken: session.accessToken, clientApp, guestId: guestId?.toString(), } satisfies WxSessionPayload, @@ -647,6 +665,9 @@ export class AuthService { } if (!targetUserId) throw new BadRequestException('绑定失败'); + if (wxSession.accessToken) { + await this.syncWechatUserProfile(targetUserId, wxSession.accessToken, wxSession.openId); + } const user = await this.assertActiveUser(targetUserId); await this.redis.del(`wx:session:${wxSessionKey}`); this.trackSmsUserEvent(user.id, clientApp, 'bind_phone', { @@ -664,6 +685,53 @@ export class AuthService { return this.buildSessionResponse(user, clientApp, user.deviceKey); } + async bindUserWechat( + userId: bigint, + input: { code?: string; wxSessionKey?: string }, + clientApp: ClientApp, + platform: 'h5' | 'mini' = 'h5', + ) { + this.assertWechatEnabled(); + if (!input.code && !input.wxSessionKey) { + throw new BadRequestException('请提供微信授权 code 或会话'); + } + + let openId: string; + let unionId: string | undefined; + let accessToken: string | undefined; + const actorRef = { refType: 'USER', refId: userId }; + + if (input.code) { + const session = + platform === 'mini' + ? await this.wechatProvider.code2Session(input.code, actorRef) + : await this.wechatProvider.oauth2AccessToken(input.code, actorRef); + openId = session.openId; + unionId = session.unionId; + accessToken = session.accessToken; + } else { + const wxSession = await this.redis.getJson(`wx:session:${input.wxSessionKey}`); + if (!wxSession) throw new BadRequestException('微信会话已过期,请重新授权'); + openId = wxSession.openId; + unionId = wxSession.unionId; + accessToken = wxSession.accessToken; + await this.redis.del(`wx:session:${input.wxSessionKey}`); + } + + const user = await this.attachWechatToUser( + userId, + { openId, unionId, accessToken }, + clientApp, + platform, + { skipLoginEvents: true }, + ); + this.analyticsService.trackOneSafe(userId, clientApp, { + eventName: 'wechat_bind', + extraJson: { platform }, + }); + return this.buildSessionResponse(user, clientApp, user.deviceKey); + } + async loginStoreWechat(code: string, clientApp: ClientApp, platform: 'h5' | 'mini' = 'h5') { this.assertWechatEnabled(); const session = @@ -871,6 +939,113 @@ export class AuthService { return this.issueToken('USER', user.id, clientApp, phoneVerified, this.formatUserProfile(user), undefined, undefined, deviceKey); } + private isDefaultNickname(nickname: string | null | undefined) { + if (!nickname || nickname === '访客') return true; + return /^用户\d{4}$/.test(nickname); + } + + private async syncWechatUserProfile( + userId: bigint, + accessToken: string, + openId: string, + ): Promise { + try { + const info = await this.wechatProvider.fetchOAuthUserInfo(accessToken, openId, { + refType: 'USER', + refId: userId, + }); + const current = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { avatar: true }, + }); + if (!current) return null; + + const data: { + nickname?: string; + avatarResourceId?: bigint; + } = {}; + + if (info.nickname && this.isDefaultNickname(current.nickname)) { + data.nickname = info.nickname; + } + + if (info.headImgUrl && !current.avatarResourceId) { + const avatar = await this.prisma.commonResource.create({ + data: { + ownerType: 'USER', + ownerId: userId, + bizType: 'AVATAR', + mediaType: 'IMAGE', + ossBucket: 'wechat', + ossKey: `wx-avatar/${openId}`, + url: info.headImgUrl, + status: 'ACTIVE', + }, + }); + data.avatarResourceId = avatar.id; + } + + if (!data.nickname && !data.avatarResourceId) { + return current as UserRow; + } + + return this.prisma.user.update({ + where: { id: userId }, + data, + include: { avatar: true }, + }); + } catch { + return null; + } + } + + private async attachWechatToUser( + userId: bigint, + session: { openId: string; unionId?: string; accessToken?: string }, + clientApp: ClientApp, + platform: string, + options?: { skipLoginEvents?: boolean }, + ): Promise { + const conflict = await this.prisma.user.findFirst({ + where: { + wxOpenId: session.openId, + id: { not: userId }, + status: 1, + mergedIntoUserId: null, + }, + }); + if (conflict) { + throw new BadRequestException('该微信已绑定其他账号'); + } + + let user = (await this.prisma.user.update({ + where: { id: userId }, + data: { + wxOpenId: session.openId, + wxUnionId: session.unionId ?? undefined, + }, + include: { avatar: true }, + })) as UserRow; + + if (session.accessToken) { + const synced = await this.syncWechatUserProfile(userId, session.accessToken, session.openId); + if (synced) user = synced as UserRow; + } + + if (!options?.skipLoginEvents) { + this.analyticsService.trackOneSafe(userId, clientApp, { + eventName: 'wechat_login', + extraJson: { platform }, + }); + this.analyticsService.trackOneSafe(userId, clientApp, { + eventName: 'login_success', + extraJson: { method: 'wechat', platform }, + }); + } + + return user; + } + private formatUserProfile(user: UserRow) { return { id: user.id.toString(), diff --git a/server/dukang-api/src/modules/iam/dto/auth.dto.ts b/server/dukang-api/src/modules/iam/dto/auth.dto.ts index 9c3625f..fccec9c 100644 --- a/server/dukang-api/src/modules/iam/dto/auth.dto.ts +++ b/server/dukang-api/src/modules/iam/dto/auth.dto.ts @@ -68,3 +68,18 @@ export class BindWechatPhoneDto { @IsNotEmpty() code: string; } + +export class BindWechatDto { + @IsString() + @IsOptional() + code?: string; + + @IsString() + @IsOptional() + wxSessionKey?: string; + + @IsString() + @IsIn(['h5', 'mini']) + @IsOptional() + platform?: 'h5' | 'mini'; +}