IlhamTahir 1 год назад
Родитель
Сommit
3cb55e20d5

+ 1 - 1
.env.example

@@ -11,4 +11,4 @@ JWT_EXPIRES_IN=1h
 WX_APPID=wx123456
 WX_SECRET=secret
 WX_TOKEN=token
-WX_AESKEY=aeskey
+WX_AES_KEY=aeskey

+ 2 - 1
src/app.module.ts

@@ -1,9 +1,10 @@
 import { Module } from '@nestjs/common';
 import { CoreModule } from './core/core.module';
 import { ArticleModule } from './article/article.module';
+import { WeChatModule } from './we-chat/we-chat.module';
 
 @Module({
-  imports: [CoreModule, ArticleModule],
+  imports: [CoreModule, ArticleModule, WeChatModule],
   controllers: [],
   providers: [],
 })

+ 5 - 2
src/core/core.module.ts

@@ -14,6 +14,8 @@ import { RoleService } from './service/role.service';
 import { Role } from './entity/role.entity';
 import { Permission } from './entity/permission.entity';
 import { UserController } from './controller/user.controller';
+import { UserBindService } from './service/user-bind.service';
+import { UserBind } from './entity/user-bind.entity';
 
 @Module({
   controllers: [TokenController, RoleController, UserController],
@@ -42,7 +44,7 @@ import { UserController } from './controller/user.controller';
       }),
       inject: [ConfigService],
     }),
-    TypeOrmModule.forFeature([User, Role, Permission]),
+    TypeOrmModule.forFeature([User, Role, Permission, UserBind]),
     JwtModule.register({
       global: true,
       secret: JWT_SECRET,
@@ -60,8 +62,9 @@ import { UserController } from './controller/user.controller';
     JwtService,
     AuthService,
     RoleService,
+    UserBindService,
   ],
-  exports: [UserService],
+  exports: [UserService, UserBindService, AuthService],
 })
 @Global()
 export class CoreModule implements OnModuleInit {

+ 9 - 0
src/core/dto/create-user.request.ts

@@ -0,0 +1,9 @@
+import { IsNotEmpty } from 'class-validator';
+
+export class CreateUserRequest {
+  @IsNotEmpty({ message: '用户名不能为空' })
+  username: string;
+
+  @IsNotEmpty({ message: '密码不能为空' })
+  password: string;
+}

+ 20 - 0
src/core/entity/user-bind.entity.ts

@@ -0,0 +1,20 @@
+import { Column, Entity, JoinTable, ManyToOne } from 'typeorm';
+import { BaseEntity } from './base.entity';
+import { User } from './user.entity';
+
+@Entity()
+export class UserBind extends BaseEntity {
+  @ManyToOne(() => User, (user) => user.binds, {
+    onDelete: 'CASCADE',
+  })
+  @JoinTable({
+    name: 'user_id',
+  })
+  user: User;
+
+  @Column()
+  type: string; // 绑定类型,例如 'wechat', 'email'
+
+  @Column()
+  bindId: string; // 对应绑定表(如 WechatUser)的主键
+}

+ 7 - 3
src/core/entity/user.entity.ts

@@ -1,6 +1,7 @@
-import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToMany, OneToMany } from 'typeorm';
 import { BaseEntity } from './base.entity';
 import { Role } from './role.entity';
+import { UserBind } from './user-bind.entity';
 
 @Entity()
 export class User extends BaseEntity {
@@ -15,16 +16,19 @@ export class User extends BaseEntity {
   @Column({
     default: false,
   })
-  locked: boolean;
+  locked: boolean = false;
 
   @Column({
     default: true,
   })
-  enabled: boolean;
+  enabled: boolean = true;
 
   @ManyToMany(() => Role, (role) => role.users)
   @JoinTable({
     name: 'user_role',
   })
   roles: Role[];
+
+  @OneToMany(() => UserBind, (bind) => bind.user)
+  binds: UserBind[];
 }

+ 5 - 0
src/core/service/auth.service.ts

@@ -5,6 +5,7 @@ import { CreateTokenRequest } from '../dto/create-token.request';
 import * as bcrypt from 'bcrypt';
 import { TokenVo } from '../vo/token.vo';
 import { JWT_SECRET } from '../constants/jwt';
+import { User } from '../entity/user.entity';
 
 @Injectable()
 export class AuthService {
@@ -30,6 +31,10 @@ export class AuthService {
     if (!isPasswordValid) {
       throw new UnauthorizedException('用户名或密码错误');
     }
+    return await this.generateToken(user);
+  }
+
+  async generateToken(user: User) {
     const payload = { sub: user.id, username: user.username }; // payload 中包含用户 ID 和用户名
     const tokenVo = new TokenVo();
     try {

+ 29 - 0
src/core/service/user-bind.service.ts

@@ -0,0 +1,29 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { UserBind } from '../entity/user-bind.entity';
+import { Repository } from 'typeorm';
+import { User } from '../entity/user.entity';
+
+@Injectable()
+export class UserBindService {
+  constructor(
+    @InjectRepository(UserBind)
+    private readonly userBindRepository: Repository<UserBind>,
+  ) {}
+
+  async findByTypeAndBindId(type: string, bindId: string) {
+    return this.userBindRepository.findOne({
+      where: { type, bindId },
+      relations: ['user'],
+    });
+  }
+
+  async create(user: User, type: string, bindId: string) {
+    const userBind = this.userBindRepository.create({
+      user,
+      type,
+      bindId,
+    });
+    return this.userBindRepository.save(userBind);
+  }
+}

+ 9 - 0
src/core/service/user.service.ts

@@ -5,6 +5,7 @@ import { Repository } from 'typeorm';
 import * as bcrypt from 'bcrypt';
 import { SearchUserFilter } from '../dto/search-user.filter';
 import { JwtPayload } from 'jsonwebtoken';
+import { CreateUserRequest } from '../dto/create-user.request';
 
 @Injectable()
 export class UserService {
@@ -76,4 +77,12 @@ export class UserService {
       take: searchUserFilter.getSize(),
     });
   }
+
+  async create(createUserRequest: CreateUserRequest) {
+    const user = new User();
+    user.username = createUserRequest.username;
+    const salt = await bcrypt.genSalt();
+    user.encryptedPassword = await bcrypt.hash('admin123', salt);
+    return this.userRepository.save(user);
+  }
 }

+ 14 - 0
src/we-chat/controller/we-chat.controller.ts

@@ -0,0 +1,14 @@
+import { Controller, Param, Post } from '@nestjs/common';
+import { NoAuth } from '../../core/decorators/no-auth.decorator';
+import { MiniProgramService } from '../service/mini-program.service';
+
+@Controller('we-chat')
+export class WeChatController {
+  constructor(private readonly miniProgramService: MiniProgramService) {}
+
+  @Post('/tokens/:code')
+  @NoAuth()
+  async createTokenByCode(@Param('code') code: string) {
+    return this.miniProgramService.createTokenByCode(code);
+  }
+}

+ 16 - 0
src/we-chat/entity/mini-program-user.entity.ts

@@ -0,0 +1,16 @@
+import { BaseEntity } from '../../core/entity/base.entity';
+import { Column } from 'typeorm';
+
+export class MiniProgramUser extends BaseEntity {
+  @Column({ unique: true })
+  openId: string;
+
+  @Column({ nullable: true })
+  unionId: string;
+
+  @Column({ nullable: true })
+  nickname: string;
+
+  @Column({ nullable: true })
+  avatarUrl: string;
+}

+ 54 - 0
src/we-chat/service/mini-program.service.ts

@@ -0,0 +1,54 @@
+import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
+import { WeChatService } from 'nest-wechat';
+import { UserBindService } from '../../core/service/user-bind.service';
+import { InjectRepository } from '@nestjs/typeorm';
+import { MiniProgramUser } from '../entity/mini-program-user.entity';
+import { Repository } from 'typeorm';
+import { UserService } from '../../core/service/user.service';
+import { CreateUserRequest } from '../../core/dto/create-user.request';
+import { AuthService } from '../../core/service/auth.service';
+
+@Injectable()
+export class MiniProgramService {
+  constructor(
+    @InjectRepository(MiniProgramUser)
+    private readonly miniProgramUserRepository: Repository<MiniProgramUser>,
+    private readonly weChatService: WeChatService,
+    private readonly userBindService: UserBindService,
+    private readonly userService: UserService,
+    private readonly authService: AuthService,
+  ) {}
+
+  async createTokenByCode(code: string) {
+    const sessionResult = await this.weChatService.mp.code2Session(code);
+
+    if (!sessionResult || !sessionResult.openid) {
+      throw new HttpException('Invalid WeChat code', HttpStatus.BAD_REQUEST);
+    }
+
+    const { openid } = sessionResult;
+    let miniProgramUser = await this.miniProgramUserRepository.findOneBy({
+      openId: openid,
+    });
+
+    if (miniProgramUser) {
+      const userBind = await this.userBindService.findByTypeAndBindId(
+        'mini-program',
+        miniProgramUser.id,
+      );
+      return this.authService.generateToken(userBind.user);
+    }
+
+    miniProgramUser = this.miniProgramUserRepository.create({
+      openId: openid,
+    });
+    await this.miniProgramUserRepository.save(miniProgramUser);
+    const createUserRequest = new CreateUserRequest();
+    // 随机username
+    createUserRequest.username = `mini-program-${Math.random()}`;
+    createUserRequest.password = `mini-program-${Math.random()}`;
+    const user = await this.userService.create(createUserRequest);
+    await this.userBindService.create(user, 'mini-program', miniProgramUser.id);
+    return this.authService.generateToken(user);
+  }
+}

+ 10 - 5
src/mini-program/mini-program.module.ts → src/we-chat/we-chat.module.ts

@@ -1,10 +1,14 @@
 import { Module } from '@nestjs/common';
-import { RedisCache, WeChatModule } from 'nest-wechat';
+import { RedisCache, WeChatModule as NestWeChatModule } from 'nest-wechat';
 import { ConfigModule, ConfigService } from '@nestjs/config';
+import { WeChatController } from './controller/we-chat.controller';
+import { MiniProgramService } from './service/mini-program.service';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { MiniProgramUser } from './entity/mini-program-user.entity';
 
 @Module({
   imports: [
-    WeChatModule.forRootAsync({
+    NestWeChatModule.forRootAsync({
       imports: [ConfigModule],
       inject: [ConfigService],
       useFactory: (configService: ConfigService, cache: Cache) => ({
@@ -16,8 +20,9 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
         debug: true,
       }),
     }),
+    TypeOrmModule.forFeature([MiniProgramUser]),
   ],
-  controllers: [],
-  providers: [],
+  controllers: [WeChatController],
+  providers: [MiniProgramService],
 })
-export class MiniProgramModule {}
+export class WeChatModule {}