mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1887 字
5 分钟
基于 Nest.js 和 Angular 的竞价平台-以及Jest测试和CICD
2024-12-07
2024-12-08

基于 Nest.js 和 Angular 的竞价平台#

项目总体描述#

本项目是一个基于 Nest.js 和 Angular 的竞价平台,旨在提供一个完整的竞标和管理系统。

项目的主要功能包括用户注册和登录、项目创建和管理、投标管理以及用户角色管理。项目的前端使用 Angular 框架构建,后端使用 Nest.js 框架构建,数据库使用 PostgreSQL,并通过 Swagger 提供 API 文档。

项目部署在 DigitalOcean 的 Droplet 上,前端通过 Nginx 进行部署。

前端 (Angular)
↓(API 请求)
Cognito (用户认证)
↓(验证通过后请求转发)
后端 (Nest.js)
↓(数据库查询)
数据库 (PostgreSQL)
↑(数据返回)
后端 (Nest.js)
↑(处理后的响应)
前端 (Angular)

项目结构#

  • frontend: 包含所有前端代码,使用 Angular 框架构建。
  • backend: 包含所有后端代码,使用 Nest.js 框架构建。
  • .github: 包含 GitHub Actions 的配置文件,用于持续集成和部署。

后端#

后端构建#

后端使用 Nest.js 框架构建,提供了一个模块化、可扩展的架构。主要功能包括用户认证、项目管理、投标管理等。后端通过 TypeORM 进行数据库操作,支持多种数据库类型。

后端技术栈#

  • Nest.js: 用于构建高效、可扩展的 Node.js 服务器端应用程序。
  • TypeORM: 用于数据库交互的 ORM 框架。
  • Swagger: 用于生成 API 文档,方便开发者查看和测试 API。

后端构建步骤#

  1. 安装依赖: 在 backend 目录下运行 npm install 安装所有必要的依赖。
  2. 配置环境变量: 在项目根目录下创建 .env 文件,配置数据库连接信息和其他环境变量。
  3. 运行开发服务器: 使用 npm run start:dev 启动开发服务器,支持热重载。
  4. 生产构建: 使用 npm run build 进行生产环境构建,生成的文件位于 dist 目录。

数据库#

项目使用 PostgreSQL 作为数据库,所有的数据库操作通过 TypeORM 进行。数据库初始化脚本位于 backend/SQL/init-script.sql,可以用于创建和初始化数据库。

后端代码结构清晰,模块化设计使得功能扩展和维护更加方便。

后端安全认证#

后端的安全认证通过 AWS Cognito 实现,结合 Nest.js 的拦截器和服务,确保用户的身份验证和授权。

安全认证架构#

  • AWS Cognito: 用于用户注册、登录和身份验证。Cognito 提供了安全的用户池和身份池管理。
  • Nest.js 拦截器: 用于拦截 HTTP 请求,验证请求头中的 JWT Token,确保用户身份的合法性。
  • Service 层: 负责处理与 Cognito 的交互,以及将 Cognito 用户与数据库中的用户信息关联。

实现步骤#

  1. Cognito 用户池配置: 在 AWS Cognito 中创建用户池,并配置应用客户端以支持 JWT Token 的生成和验证。

  2. JWT 拦截器: 在 Nest.js 中创建一个拦截器,解析请求头中的 JWT Token,验证其有效性,并将用户信息附加到请求对象中。

    import { Injectable, NestInterceptor, ExecutionContext, CallHandler, UnauthorizedException } from '@nestjs/common';
    import { Observable } from 'rxjs';
    import { AuthService } from './auth.service';
    @Injectable()
    export class JwtInterceptor implements NestInterceptor {
    constructor(private readonly authService: AuthService) {}
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];
    if (!token) {
    throw new UnauthorizedException('Token not found');
    }
    const user = this.authService.validateToken(token);
    if (!user) {
    throw new UnauthorizedException('Invalid token');
    }
    request.user = user;
    return next.handle();
    }
    }
  3. 用户服务: 创建一个用户服务,负责从数据库中检索用户信息,并将其与 Cognito 用户进行关联。通过 Cognito ID 作为唯一标识符,将用户信息存储在数据库中。

    import { Injectable } from '@nestjs/common';
    import { UsersRepository } from './users.repository';
    @Injectable()
    export class UsersService {
    constructor(private readonly usersRepository: UsersRepository) {}
    async findOrCreateUser(cognitoId: string, email: string) {
    let user = await this.usersRepository.findOneByCognitoId(cognitoId);
    if (!user) {
    user = await this.usersRepository.create({ cognitoId, email });
    }
    return user;
    }
    }
  4. 角色和权限管理: 在数据库中定义用户角色(如管理员、客户、投标者),并在拦截器中根据角色进行权限验证。

    import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
    import { Reflector } from '@nestjs/core';
    @Injectable()
    export class RolesGuard implements CanActivate {
    constructor(private reflector: Reflector) {}
    canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
    return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return roles.includes(user.role);
    }
    }

    在需要权限验证的 API 上添加 @Roles('admin') 装饰器,指定需要的角色。

    @Post()
    @Roles('admin')
    createProject(@Body() createProjectDto: CreateProjectDto) {
    return this.projectsService.createProject(createProjectDto);
    }

通过这种方式,后端能够有效地管理用户身份和权限,确保系统的安全性和可靠性。

项目管理实现#

在项目管理模块中,将展示如何通过 Controller 调用 Service,再通过 Service 与数据库交互。

Controller#

ProjectsController 中,定义处理 HTTP 请求的路由和方法。

import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common';
import { ProjectsService } from './projects.service';
import { ProjectsDto } from '../entities/DTO/projects.dto';
@Controller('projects')
export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {}
@Get()
findAll() {
return this.projectsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: number) {
return this.projectsService.findOne(id);
}
@Post()
create(@Body() projectDto: ProjectsDto) {
return this.projectsService.create(projectDto);
}
@Put(':id')
update(@Param('id') id: number, @Body() projectDto: ProjectsDto) {
return this.projectsService.update(id, projectDto);
}
@Delete(':id')
delete(@Param('id') id: number) {
return this.projectsService.delete(id);
}
}

Service#

ProjectsService 负责业务逻辑处理,并与数据库进行交互。

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Project } from '../entities/projects.entity';
import { ProjectsDto } from '../entities/DTO/projects.dto';
@Injectable()
export class ProjectsService {
constructor(private dataSource: DataSource) {}
findAll() {
return this.dataSource.getRepository(Project).find();
}
findOne(id: number) {
return this.dataSource.getRepository(Project).findOneBy({ project_id: id });
}
create(project: ProjectsDto) {
return this.dataSource.getRepository(Project).save(project);
}
update(id: number, project: ProjectsDto) {
return this.dataSource.getRepository(Project).update(id, project);
}
delete(id: number) {
return this.dataSource.getRepository(Project).delete(id);
}
}

数据库实体#

Project 实体定义了项目在数据库中的结构,通过 @Entity() 装饰器定义实体,通过 @Column() 装饰器定义列。

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Project {
@PrimaryGeneratedColumn()
project_id: number;
@Column()
title: string;
@Column()
description: string;
@Column('decimal')
budget_min: number;
@Column('decimal')
budget_max: number;
@Column('date')
deadline: Date;
@Column({ default: 'open' })
status: string;
}

通过这种方式,Controller 负责处理 HTTP 请求,Service 负责业务逻辑,数据库实体定义数据结构,三者协同工作,实现完整的项目管理功能。

前端#

前端使用 Angular 框架构建,提供了用户友好的界面和交互体验。主要功能包括项目展示、投标管理、用户注册和登录等。

前端技术栈#

  • Angular: 用于构建现代化的单页应用程序。
  • RxJS: 用于处理异步数据流。
  • Angular CLI: 提供了强大的开发工具和命令行接口。

前端构建步骤#

  1. 安装依赖: 在 frontend 目录下运行 npm install 安装所有必要的依赖。
  2. 开发服务器: 使用 ng serve 启动开发服务器,默认在 http://localhost:4200/ 运行。
  3. 生产构建: 使用 ng build 进行生产环境构建,生成的文件位于 dist 目录。

项目详情组件#

前端应用由多个组件组成,每个组件负责特定的功能模块。以下是一个示例组件的实现。

ProjectDetailComponent 用于展示单个项目的详细信息。

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProjectsService } from '../../services/projects.service';
import { BidsService } from '../../services/bids.service';
import { AuthService } from '../../services/auth.service';
@Component({
selector: 'app-project-detail',
templateUrl: './project-detail.component.html',
styleUrls: ['./project-detail.component.css']
})
export class ProjectDetailComponent implements OnInit {
project: any = null;
bids: any[] = [];
loading = false;
error = '';
userRole: string = '';
constructor(
private route: ActivatedRoute,
private projectsService: ProjectsService,
private bidsService: BidsService,
private authService: AuthService
) {}
ngOnInit() {
this.userRole = this.authService.getUserRole();
const projectId = this.route.snapshot.paramMap.get('id');
if (projectId) {
this.loadProject(+projectId);
this.loadBids(+projectId);
}
}
loadProject(id: number) {
this.loading = true;
this.projectsService.getProjectById(id).subscribe({
next: (data) => {
this.project = data;
this.loading = false;
},
error: (err) => {
this.error = '加载项目详情失败';
this.loading = false;
console.error('加载项目详情错误:', err);
}
});
}
loadBids(projectId: number) {
this.bidsService.getBidsByProjectId(projectId).subscribe({
next: (data) => {
this.bids = data;
},
error: (err) => {
console.error('加载投标列表错误:', err);
}
});
}
}

模板文件#

project-detail.component.html 定义了项目详情的展示结构。

<div class="project-detail">
<div *ngIf="loading" class="loading">
加载中...
</div>
<div *ngIf="error" class="error">
{{ error }}
</div>
<div *ngIf="project && !loading" class="project-info">
<h2>{{ project.title }}</h2>
<div class="project-meta">
<p>预算: ¥{{ project.budget_min }} - ¥{{ project.budget_max }}</p>
<p>截止日期: {{ project.deadline | date }}</p>
<p>状态: {{ project.status }}</p>
</div>
<div class="project-description">
<h3>项目描述</h3>
<p>{{ project.description }}</p>
</div>
<app-bid-form
*ngIf="userRole === 'bidder' && project.status === 'open'"
[projectId]="project.project_id"
(bidSubmitted)="loadBids(project.project_id)">
</app-bid-form>
<div class="bids-section" *ngIf="userRole === 'client' || userRole === 'admin'">
<h3>投标列表</h3>
<div *ngFor="let bid of bids" class="bid-card">
<p>投标人: {{ bid.bidder_id }}</p>
<p>投标金额: ¥{{ bid.amount }}</p>
<p>投标说明: {{ bid.message }}</p>
<p>状态: {{ bid.status }}</p>
</div>
</div>
</div>
</div>

通过这种方式,前端应用能够提供丰富的用户交互和数据展示功能。

测试#

项目使用 Jest 进行单元测试和集成测试,确保代码的正确性和稳定性。同时,使用 ESLint 进行代码质量检查,确保代码风格一致。

Jest 测试#

Jest 是一个强大的 JavaScript 测试框架,支持断言、模拟和快照测试。

Jest 配置#

在项目的 package.json 中配置 Jest:

"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "src",
"testRegex": ".*\\\\.spec\\\\.ts$",
"transform": {
"^.+\\\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

示例测试#

以下是一个简单的服务测试示例:

import { Test, TestingModule } from '@nestjs/testing';
import { ProjectsService } from './projects.service';
describe('ProjectsService', () => {
let service: ProjectsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProjectsService],
}).compile();
service = module.get<ProjectsService>(ProjectsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findOne', () => {
it('应该返回单个项目', async () => {
const result = await service.findOne(1);
expect(result).toEqual(mockProject);
});
});
});

ESLint 代码质量检查#

ESLint 是一个用于识别和报告 JavaScript 代码中的模式的工具,帮助开发者保持代码的一致性和质量。

ESLint 配置#

在项目根目录下创建 .eslintrc.js 文件:

module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

运行 ESLint#

package.json 中添加脚本:

"scripts": {
"lint": "eslint . --ext .ts"
}

通过运行 npm run lint 来检查代码质量。

通过使用 Jest 和 ESLint,项目能够确保代码的正确性和一致性,提高开发效率和代码质量。

CI/CD#

项目使用 GitHub Actions 实现持续集成和持续部署(CI/CD),确保代码在每次提交后自动构建、测试和部署。

GitHub Actions#

GitHub Actions 是一个用于自动化软件开发工作流的工具。通过定义工作流文件,可以在代码库中自动执行构建、测试和部署任务。

工作流配置#

在项目的 .github/workflows/deploy.yml 文件中定义了 CI/CD 工作流:

name: CI/CD Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '20.18.0'
- name: Install dependencies
run: |
cd backend
npm install
cd ../frontend
npm install
- name: Run tests
run: |
cd backend
npm run test:cov
cd ../frontend
npm run test
- name: Lint code
run: |
cd backend
npm run lint
cd ../frontend
npm run lint
- name: Build project
run: |
cd backend
npm run build
cd ../frontend
npm run build
- name: Create Release Package
run: |
mkdir -p build
cd backend
tar -czvf ../build/backend.tar.gz dist
cd ../frontend
tar -czvf ../build/frontend.tar.gz dist
cd ..
- name: Deploy to DigitalOcean
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
run: |
# 部署脚本或命令

部署#

  • DigitalOcean: 项目部署在 DigitalOcean 的 Droplet 上,前端通过 Nginx 进行部署。
  • 自动化流程: 每次代码提交到 main 分支时,GitHub Actions 会自动执行构建、测试和部署流程。

通过这种方式,项目能够快速响应代码变更,确保每次提交的代码都经过严格的测试和验证,并自动部署到生产环境。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

基于 Nest.js 和 Angular 的竞价平台-以及Jest测试和CICD
https://dreaife.tokyo/posts/bidding-platform-nest-angular/
作者
dreaife
发布于
2024-12-07
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时