Skip to content

Commit

Permalink
feat(#34): 支持微信登录&支持待办事项管理 (#37)
Browse files Browse the repository at this point in the history
Co-authored-by: Aiden-FE <[email protected]>
  • Loading branch information
Aiden-FE and Aiden-FE committed May 7, 2023
1 parent dcf11fc commit 9c006f9
Show file tree
Hide file tree
Showing 25 changed files with 407 additions and 23 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ COMPASS_PRIVACY_DATA_SECRET=example
#COMPASS_THROTTLER_TTL=60
# [可选] 默认全局在限流时间区间内最多可以调用几次, 默认值: 20
#COMPASS_THROTTLER_LIMIT=20
# [可选] 微信应用id
#COMPASS_WECHAT_APPID=example
# [可选] 微信应用密钥
#COMPASS_WECHAT_SECRET=example

# MySQL config
# mysql 连接地址
Expand Down
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
deploy/mysql
deploy/postgres
deploy/redis
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ export class ExampleController {
当遇见多个Dto联合类型时,内置ValidationPipe失效,可按照下列示例处理:

```typescript
import { IsNumber, IsString } from 'class-validator';
import { Body, Optional } from '@nestjs/common';
import { IsNumber, IsString, IsOptional } from 'class-validator';
import { Body } from '@nestjs/common';
import { validateMultipleDto } from '@shared';

class ADto {
Expand All @@ -193,7 +193,7 @@ class CDto {
@IsString()
name: string;

@Optional()
@IsOptional()
@IsString()
address?: string
}
Expand Down Expand Up @@ -251,6 +251,8 @@ export class ExampleController {

#### 迁移管理

`npx prisma db push` 本地或开发环境可通过此命令直接同步数据库架构 警告: 请不要在测试或生产等正式环境使用此命令

`npx prisma migrate dev --name [本次迁移的标题]` schema变更后创建迁移脚本

`npx prisma migrate deploy` 执行迁移脚本
Expand Down
3 changes: 2 additions & 1 deletion apps/compass-service/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ import { OauthModule } from './oauth/oauth.module';
import { UserModule } from './user/user.module';
import { RoleModule } from './role/role.module';
import { PermissionModule } from './permission/permission.module';
import { TodoModule } from './todo/todo.module';

export default [OauthModule, UserModule, RoleModule, PermissionModule];
export default [OauthModule, UserModule, RoleModule, PermissionModule, TodoModule];
39 changes: 39 additions & 0 deletions apps/compass-service/src/modules/oauth/oauth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
TelephoneLoginDto,
ValidAndSendEmailCodeDto,
ValidateRecaptchaDto,
WechatLoginDto,
} from './oauth.dto';
import { OauthService } from './oauth.service';
import { UserService } from '../user/user.service';
Expand Down Expand Up @@ -139,4 +140,42 @@ export class OauthController {
const signStr = this.jwtService.sign(result);
return { ...result, token: signStr };
}

// eslint-disable-next-line class-methods-use-this
@ApiOperation({
summary: '微信登录',
description: '微信账号登录',
})
@Public()
@Post('login/wechat')
async wechatLogin(@Body() body: WechatLoginDto) {
const authInfo = await OauthService.getWechatAuthInfo(body.code);
if (!authInfo) {
return new HttpResponse(null, {
statusCode: ResponseCode.FORBIDDEN,
message: '获取微信授权信息失败',
});
}
let user = await this.userService.findUser({
openid: authInfo.openid,
});
if (!user) {
Logger.log(`${authInfo.openid} 该微信用户尚未注册,准备创建初始账号`, APP_LOG_CONTEXT);
user = await this.userService.createUser({
openid: authInfo.openid,
});
}

// 签发token
const signStr = this.jwtService.sign({
...user,
additional: {
sessionKey: authInfo.session_key,
},
});
return new HttpResponse({
token: signStr,
...user,
});
}
}
14 changes: 14 additions & 0 deletions apps/compass-service/src/modules/oauth/oauth.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@ export class EmailRegisterDto {
@Length(SMS_LENGTH_LIMIT)
captcha: string;
}

// 微信登录
export class WechatLoginDto {
@IsString()
code: string;
}

// 微信授权响应数据
export interface WechatAuthInfo {
openid?: string;
session_key?: string;
errmsg?: string;
errcode?: number;
}
27 changes: 26 additions & 1 deletion apps/compass-service/src/modules/oauth/oauth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { CAPTCHA_REDIS_KEY, RedisManagerService } from '@app/redis-manager';
import { random } from 'lodash';
import { EMAIL_CAPTCHA_TEMPLATE } from '@app/email/templates';
import { EmailService } from '@app/email';
import { EMailLoginDto, TelephoneLoginDto } from './oauth.dto';
import { getWechatAuthInfo } from '@shared/http/wechat';
import { promiseTask } from '@compass-aiden/utils';
import { EMailLoginDto, TelephoneLoginDto, WechatAuthInfo } from './oauth.dto';
import { UserService } from '../user/user.service';

@Injectable()
Expand Down Expand Up @@ -120,4 +122,27 @@ export class OauthService {

return user;
}

static async getWechatAuthInfo(code: string) {
const [err, resp] = await promiseTask<WechatAuthInfo>(
getWechatAuthInfo({
appid: getEnv(CompassEnv.WECHAT_APPID),
secret: getEnv(CompassEnv.WECHAT_SECRET),
js_code: code,
}),
);
if (err) {
throw new HttpResponse(null, {
statusCode: ResponseCode.FORBIDDEN,
message: err?.message || err.toString(),
});
}
if (!resp.openid || !resp.session_key) {
throw new HttpResponse(null, {
statusCode: ResponseCode.FORBIDDEN,
message: resp.errmsg,
});
}
return resp;
}
}
7 changes: 3 additions & 4 deletions apps/compass-service/src/modules/role/role.dto.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { IsArray, IsString, MaxLength, MinLength } from 'class-validator';
import { IsArray, IsString, MaxLength, MinLength, IsOptional } from 'class-validator';
import { DESC_MAX_LIMIT, TITLE_MAX_LIMIT, TITLE_MIN_LIMIT } from '@shared';
import { Optional } from '@nestjs/common';

export class RoleCreateDto {
@IsString()
@MaxLength(TITLE_MAX_LIMIT)
@MinLength(TITLE_MIN_LIMIT)
name: string;

@Optional()
@IsOptional()
@MaxLength(DESC_MAX_LIMIT)
description?: string;

@Optional()
@IsOptional()
@IsArray()
permissions?: string[];
}
47 changes: 47 additions & 0 deletions apps/compass-service/src/modules/todo/todo.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Body, Controller, Delete, Get, Post, Put, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { User } from '@shared';
import { TodoService } from './todo.service';
import { CreateTodoDto, DeleteTodoDto, ModifyTodoDto, TodoListQueryDto } from './todo.dto';
import { UserContextDto } from '../user/user.dto';

@ApiTags('待办事项')
@Controller('todo')
export class TodoController {
constructor(private todoService: TodoService) {}

@ApiOperation({
summary: '获取待办事项列表',
})
@Get()
getTodos(@Query() query: TodoListQueryDto, @User() user: UserContextDto) {
return this.todoService.getTodoList(query, user.id);
}

@ApiOperation({
summary: '创建待办事项',
})
@Post()
createTodo(@Body() body: CreateTodoDto, @User() user: UserContextDto) {
return this.todoService.createTodo({
...body,
userId: user.id,
});
}

@ApiOperation({
summary: '修改待办事项',
})
@Put()
modifyTodo(@Body() body: ModifyTodoDto, @User() user: UserContextDto) {
return this.todoService.modifyTodo(body, user.id);
}

@ApiOperation({
summary: '删除待办事项',
})
@Delete()
deleteTodo(@Body() body: DeleteTodoDto, @User() user: UserContextDto) {
return this.todoService.deleteTodo(body.id, user.id);
}
}
41 changes: 41 additions & 0 deletions apps/compass-service/src/modules/todo/todo.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
DESC_MAX_LIMIT,
KEYWORD_MAX_LIMIT,
PaginationRequestFromURLDto,
TITLE_MAX_LIMIT,
TITLE_MIN_LIMIT,
} from '@shared';
import { IsBoolean, IsString, MaxLength, MinLength, IsOptional, IsNumber } from 'class-validator';

export class TodoListQueryDto extends PaginationRequestFromURLDto {
@IsOptional()
@IsString()
@MaxLength(KEYWORD_MAX_LIMIT)
keyword?: string;
}

export class CreateTodoDto {
@IsString()
@MaxLength(TITLE_MAX_LIMIT)
@MinLength(TITLE_MIN_LIMIT)
title: string;

@IsOptional()
@IsString()
@MaxLength(DESC_MAX_LIMIT)
description?: string;

@IsOptional()
@IsBoolean()
isFinished?: boolean;
}

export class ModifyTodoDto extends CreateTodoDto {
@IsNumber()
id: number;
}

export class DeleteTodoDto {
@IsNumber()
id: number;
}
10 changes: 10 additions & 0 deletions apps/compass-service/src/modules/todo/todo.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { DBService } from '@app/db';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';

@Module({
controllers: [TodoController],
providers: [TodoService, DBService],
})
export class TodoModule {}
90 changes: 90 additions & 0 deletions apps/compass-service/src/modules/todo/todo.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { DBService } from '@app/db';
import { HttpResponse, PaginationParams, PaginationResponse, ResponseCode } from '@shared';
import { CreateTodoDto, ModifyTodoDto, TodoListQueryDto } from './todo.dto';

@Injectable()
export class TodoService {
constructor(private dbService: DBService) {}

async deleteTodo(id: number, userId: string) {
const todo = await this.dbService.todo.findFirst({
where: {
id,
userId,
},
});
if (!todo) {
return new HttpResponse(null, {
statusCode: ResponseCode.NOT_FOUND,
message: '未找到可被删除的资源',
});
}
return this.dbService.todo.delete({
where: {
id,
},
});
}

async modifyTodo(body: ModifyTodoDto, userId: string) {
const todo = await this.dbService.todo.findFirst({
where: {
id: body.id,
userId,
},
});
if (!todo) {
return new HttpResponse(null, {
statusCode: ResponseCode.NOT_FOUND,
message: '未找到要更新的资源',
});
}
return this.dbService.todo.update({
where: {
id: body.id,
},
data: body,
});
}

async getTodoList(query: TodoListQueryDto, userId: string) {
const pagination = PaginationParams(query);
const where = {
userId,
title: {
contains: query.keyword,
},
};
const [total, list] = await this.dbService.$transaction([
this.dbService.todo.count({ where }),
this.dbService.todo.findMany({
skip: pagination.skip,
take: pagination.take,
where,
orderBy: [{ updatedAt: 'desc' }],
select: {
id: true,
title: true,
description: true,
isFinished: true,
updatedAt: true,
},
}),
]);

return new PaginationResponse({
total,
list,
...pagination,
});
}

createTodo(todo: CreateTodoDto & { userId: string }) {
return this.dbService.todo.create({
data: {
...todo,
},
});
}
}
1 change: 1 addition & 0 deletions apps/compass-service/src/modules/user/user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface UserContextDto {
id: string;
openid?: string;
telephone?: string; // 需要进行掩码脱敏处理
email?: string;
nickname?: string;
Expand Down
2 changes: 1 addition & 1 deletion apps/compass-service/src/modules/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class UserService {
* @param user
* @param selectOption
*/
async createUser(user: Partial<User> & { password: string }, selectOption: any = {}) {
async createUser(user: Partial<User>, selectOption: any = {}) {
const userModel = await this.dbService.user.create({
data: {
...user,
Expand Down
Loading

0 comments on commit 9c006f9

Please sign in to comment.