微信SDK接通
This commit is contained in:
@@ -12,13 +12,23 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { ClientApp, SmsScene } from '@dukang/shared-types';
|
||||
import { generateUserNo } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { SMS_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import { RedisService } from '../../common/redis/redis.service';
|
||||
import { SMS_PROVIDER, WECHAT_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import { ISmsProvider } from '../../integrations/sms/sms.interface';
|
||||
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
import type { User } from '@prisma/client';
|
||||
|
||||
type WxSessionPayload = {
|
||||
openId: string;
|
||||
unionId?: string;
|
||||
sessionKey?: string;
|
||||
clientApp: ClientApp;
|
||||
guestId?: string;
|
||||
};
|
||||
|
||||
type UserRow = Pick<
|
||||
User,
|
||||
| 'id'
|
||||
@@ -29,6 +39,7 @@ type UserRow = Pick<
|
||||
| 'nickname'
|
||||
| 'avatarResourceId'
|
||||
| 'wxOpenId'
|
||||
| 'wxUnionId'
|
||||
| 'mergedIntoUserId'
|
||||
| 'status'
|
||||
> & {
|
||||
@@ -40,7 +51,9 @@ export class AuthService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly redis: RedisService,
|
||||
@Inject(SMS_PROVIDER) private readonly smsProvider: ISmsProvider,
|
||||
@Inject(WECHAT_PROVIDER) private readonly wechatProvider: IWechatProvider,
|
||||
) {}
|
||||
|
||||
async sendSms(phone: string, scene: string) {
|
||||
@@ -286,6 +299,242 @@ export class AuthService {
|
||||
throw new NotImplementedException('FEATURE_DISABLED');
|
||||
}
|
||||
|
||||
assertWechatEnabled() {
|
||||
if (!this.wechatProvider.isEnabled()) {
|
||||
this.wechatDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
async loginUserWechat(
|
||||
code: string,
|
||||
clientApp: ClientApp,
|
||||
platform: 'h5' | 'mini' = 'h5',
|
||||
guestId?: bigint,
|
||||
) {
|
||||
this.assertWechatEnabled();
|
||||
const session =
|
||||
platform === 'mini'
|
||||
? await this.wechatProvider.code2Session(code)
|
||||
: await this.wechatProvider.oauth2AccessToken(code);
|
||||
|
||||
let user = await this.prisma.user.findFirst({
|
||||
where: { wxOpenId: session.openId, status: 1, mergedIntoUserId: null },
|
||||
include: { avatar: true },
|
||||
});
|
||||
|
||||
if (user?.phone && user.phoneVerifiedAt) {
|
||||
let activeUser: UserRow =
|
||||
guestId && guestId !== user.id ? await this.mergeUsers(guestId, user.id) : (user as UserRow);
|
||||
activeUser = await this.prisma.user.update({
|
||||
where: { id: activeUser.id },
|
||||
data: {
|
||||
wxUnionId: session.unionId ?? activeUser.wxUnionId,
|
||||
},
|
||||
include: { avatar: true },
|
||||
});
|
||||
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey);
|
||||
}
|
||||
|
||||
const wxSessionKey = randomUUID();
|
||||
await this.redis.setJson(
|
||||
`wx:session:${wxSessionKey}`,
|
||||
{
|
||||
openId: session.openId,
|
||||
unionId: session.unionId,
|
||||
sessionKey: 'sessionKey' in session ? session.sessionKey : undefined,
|
||||
clientApp,
|
||||
guestId: guestId?.toString(),
|
||||
} satisfies WxSessionPayload,
|
||||
1800,
|
||||
);
|
||||
|
||||
if (user && !user.phoneVerifiedAt) {
|
||||
return {
|
||||
needBindPhone: true,
|
||||
wxSessionKey,
|
||||
actorType: 'USER',
|
||||
actorId: user.id.toString(),
|
||||
phoneVerified: false,
|
||||
user: this.formatUserProfile(user),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
needBindPhone: true,
|
||||
wxSessionKey,
|
||||
phoneVerified: false,
|
||||
};
|
||||
}
|
||||
|
||||
async bindWechatPhone(
|
||||
wxSessionKey: string,
|
||||
phone: string,
|
||||
code: string,
|
||||
clientApp: ClientApp,
|
||||
) {
|
||||
this.assertWechatEnabled();
|
||||
await this.smsProvider.verify(phone, code, SmsScene.BIND_PHONE);
|
||||
|
||||
const wxSession = await this.redis.getJson<WxSessionPayload>(`wx:session:${wxSessionKey}`);
|
||||
if (!wxSession) throw new BadRequestException('微信会话已过期,请重新授权');
|
||||
|
||||
const guestId = wxSession.guestId ? BigInt(wxSession.guestId) : undefined;
|
||||
const wxUser = await this.prisma.user.findFirst({
|
||||
where: { wxOpenId: wxSession.openId, status: 1, mergedIntoUserId: null },
|
||||
include: { avatar: true },
|
||||
});
|
||||
|
||||
const existingPhone = await this.prisma.user.findUnique({
|
||||
where: { phone },
|
||||
include: { avatar: true },
|
||||
});
|
||||
|
||||
let targetUserId: bigint | null = wxUser?.id ?? null;
|
||||
|
||||
if (!wxUser && !existingPhone) {
|
||||
if (guestId) {
|
||||
try {
|
||||
const guest = await this.assertActiveUser(guestId);
|
||||
const updated = await this.prisma.user.update({
|
||||
where: { id: guest.id },
|
||||
data: {
|
||||
phone,
|
||||
phoneVerifiedAt: new Date(),
|
||||
wxOpenId: wxSession.openId,
|
||||
wxUnionId: wxSession.unionId,
|
||||
nickname: guest.nickname === '访客' ? `用户${phone.slice(-4)}` : guest.nickname,
|
||||
},
|
||||
});
|
||||
targetUserId = updated.id;
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
if (!targetUserId) {
|
||||
const created = await this.prisma.user.create({
|
||||
data: {
|
||||
phone,
|
||||
phoneVerifiedAt: new Date(),
|
||||
wxOpenId: wxSession.openId,
|
||||
wxUnionId: wxSession.unionId,
|
||||
userNo: generateUserNo(),
|
||||
nickname: `用户${phone.slice(-4)}`,
|
||||
cityPreference: {
|
||||
create: {
|
||||
selectedCityCode: '410100',
|
||||
selectedDistrict: '郑州市',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
targetUserId = created.id;
|
||||
}
|
||||
} else if (existingPhone) {
|
||||
await this.assertActiveUser(existingPhone.id);
|
||||
const updated = await this.prisma.user.update({
|
||||
where: { id: existingPhone.id },
|
||||
data: {
|
||||
wxOpenId: wxSession.openId,
|
||||
wxUnionId: wxSession.unionId,
|
||||
phoneVerifiedAt: existingPhone.phoneVerifiedAt ?? new Date(),
|
||||
},
|
||||
});
|
||||
targetUserId = updated.id;
|
||||
if (guestId && guestId !== updated.id) {
|
||||
targetUserId = (await this.mergeUsers(guestId, updated.id)).id;
|
||||
}
|
||||
} else if (wxUser) {
|
||||
if (wxUser.phone && wxUser.phone !== phone) {
|
||||
throw new BadRequestException('手机号已被其他账号占用');
|
||||
}
|
||||
const updated = await this.prisma.user.update({
|
||||
where: { id: wxUser.id },
|
||||
data: {
|
||||
phone,
|
||||
phoneVerifiedAt: new Date(),
|
||||
wxUnionId: wxSession.unionId ?? wxUser.wxUnionId,
|
||||
},
|
||||
});
|
||||
targetUserId = updated.id;
|
||||
}
|
||||
|
||||
if (!targetUserId) throw new BadRequestException('绑定失败');
|
||||
const user = await this.assertActiveUser(targetUserId);
|
||||
await this.redis.del(`wx:session:${wxSessionKey}`);
|
||||
return this.buildSessionResponse(user, clientApp, user.deviceKey);
|
||||
}
|
||||
|
||||
async loginStoreWechat(code: string, clientApp: ClientApp, platform: 'h5' | 'mini' = 'h5') {
|
||||
this.assertWechatEnabled();
|
||||
const session =
|
||||
platform === 'mini'
|
||||
? await this.wechatProvider.code2Session(code)
|
||||
: await this.wechatProvider.oauth2AccessToken(code);
|
||||
|
||||
let account = await this.prisma.storeAccount.findFirst({
|
||||
where: { wxOpenId: session.openId },
|
||||
include: { store: true },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信');
|
||||
}
|
||||
|
||||
account = await this.prisma.storeAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
wxOpenId: session.openId,
|
||||
wxUnionId: session.unionId ?? account.wxUnionId,
|
||||
lastLoginAt: new Date(),
|
||||
},
|
||||
include: { store: true },
|
||||
});
|
||||
|
||||
return this.issueToken('STORE', account.id, clientApp, false, undefined, {
|
||||
id: account.id.toString(),
|
||||
storeId: account.storeId.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
storeName: account.store.name,
|
||||
});
|
||||
}
|
||||
|
||||
async loginPartnerWechat(code: string, clientApp: ClientApp, platform: 'h5' | 'mini' = 'h5') {
|
||||
this.assertWechatEnabled();
|
||||
const session =
|
||||
platform === 'mini'
|
||||
? await this.wechatProvider.code2Session(code)
|
||||
: await this.wechatProvider.oauth2AccessToken(code);
|
||||
|
||||
let account = await this.prisma.partnerAccount.findFirst({
|
||||
where: { wxOpenId: session.openId },
|
||||
include: { partner: true },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new BadRequestException('首次登录请使用手机验证码,登录后将自动关联微信');
|
||||
}
|
||||
|
||||
account = await this.prisma.partnerAccount.update({
|
||||
where: { id: account.id },
|
||||
data: {
|
||||
wxOpenId: session.openId,
|
||||
wxUnionId: session.unionId ?? account.wxUnionId,
|
||||
lastLoginAt: new Date(),
|
||||
},
|
||||
include: { partner: true },
|
||||
});
|
||||
|
||||
return this.issueToken('PARTNER', account.id, clientApp, false, undefined, undefined, {
|
||||
id: account.id.toString(),
|
||||
partnerId: account.partnerId.toString(),
|
||||
name: account.name,
|
||||
phone: account.phone,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
companyName: account.partner.companyName,
|
||||
});
|
||||
}
|
||||
|
||||
private async mergeUsers(guestId: bigint, primaryId: bigint): Promise<UserRow> {
|
||||
if (guestId === primaryId) {
|
||||
return this.assertActiveUser(primaryId);
|
||||
|
||||
Reference in New Issue
Block a user