feat: 添加Docker部署配置
parent
69976a67f8
commit
e11e1a7f59
|
|
@ -0,0 +1,7 @@
|
|||
dist
|
||||
node_modules
|
||||
scripts
|
||||
.git
|
||||
.gitea
|
||||
.gitignore
|
||||
README.md
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
name: 自动部署
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- .gitignore
|
||||
- .vscode/**
|
||||
- README.md
|
||||
|
||||
env:
|
||||
docker_host: ${{ secrets.DOCKER_REGISTRY }}
|
||||
docker_name: ${{ secrets.DOCKER_REGISTRY }}/${{ gitea.repository }}
|
||||
docker_user: ${{ secrets.DOCKER_USERNAME }}
|
||||
docker_pass: ${{ secrets.DOCKER_PASSWORD }}
|
||||
deploy_host: ${{ secrets.DEPLOY_HOSTNAME }}
|
||||
deploy_port: ${{ secrets.DEPLOY_PORT }}
|
||||
deploy_user: ${{ secrets.DEPLOY_USERNAME }}
|
||||
deploy_pass: ${{ secrets.DEPLOY_PASSWORD }}
|
||||
deploy_name: demo_server
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
id: checkout
|
||||
uses: https://gitea.com/actions/checkout@v3
|
||||
|
||||
- name: 构建镜像
|
||||
run: |
|
||||
docker build -t ${{ env.docker_name }}:latest .
|
||||
|
||||
- name: 登陆镜像
|
||||
run: |
|
||||
docker login -u "${{env.docker_user}}" -p "${{env.docker_pass}}" ${{env.docker_host}}
|
||||
|
||||
- name: 推送镜像
|
||||
shell: bash
|
||||
run: |
|
||||
docker push ${{ env.docker_name }}:latest
|
||||
|
||||
- name: 更新服务
|
||||
uses: http://git.dev.juetan.cn/mirror/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ env.deploy_host }}
|
||||
port: ${{ env.deploy_port }}
|
||||
username: ${{ env.deploy_user }}
|
||||
password: ${{ env.deploy_pass }}
|
||||
script: |
|
||||
docker service ls | grep -q ${{ env.deploy_name }} || exit 0
|
||||
docker service update --image ${{ env.docker_name }}:latest ${{ env.deploy_name }}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
FROM node:18-alpine As dev
|
||||
RUN apk update && apk add sqlite
|
||||
RUN apk add --no-cache --virtual .build-deps g++ gcc libgcc libstdc++ linux-headers make python3
|
||||
WORKDIR /app
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
FROM node:18-alpine As build
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
COPY --from=dev /app/content/ ./content/
|
||||
COPY --from=dev /app/dist/ ./dist/
|
||||
COPY --from=dev /app/node_modules/ ./node_modules/
|
||||
COPY --from=dev /app/.env ./
|
||||
COPY --from=dev /app/package.json ./
|
||||
|
||||
EXPOSE 3030
|
||||
CMD [ "node", "./dist/main.js" ]
|
||||
16
README.md
16
README.md
|
|
@ -26,4 +26,18 @@
|
|||
- 角色模块
|
||||
- 权限模块
|
||||
- 上传模块
|
||||
- 文章模块
|
||||
- 文章模块
|
||||
|
||||
## 部署
|
||||
|
||||
目前基于 Gitea 和 Gitea Actions 实现,大致流程是这样的:提交代码到 Gitea 仓库后,触发流水线任务进行构建并打包成 Docker 镜像,推送到 Gitea 自带的软件包仓库,然后登陆生产服务器执行更新命令。
|
||||
|
||||
使用 Github Actions 也是可以的,两者使用上是兼容的。本仓库有关部署的内容涉及三个地方,不需要的话可自行删除,如下:
|
||||
|
||||
- Dockerfile 构建镜像的配置文件
|
||||
- .dockerignore 配置哪些文件应该被忽略掉
|
||||
- .gitea/workflows/depoy.yaml 流水线任务的配置文件,语法上与 Github Actions 一致
|
||||
|
||||
## 最后
|
||||
|
||||
如果你在使用过程中遇到问题,欢迎在 Issue 中提问。
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
|
|
@ -33,6 +33,7 @@
|
|||
"@nestjs/config": "^2.3.4",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/platform-express": "^9.4.3",
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"axios": "^1.5.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
|
|
@ -45,13 +46,20 @@
|
|||
"typeorm-naming-strategies": "^4.1.0",
|
||||
"ua-parser-js": "^1.0.36",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.10.0",
|
||||
"nodemailer": "^6.9.5",
|
||||
"mysql2": "^3.6.1",
|
||||
"cache-manager": "^5.2.3",
|
||||
"cache-manager-redis-store": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mockjs": "^1.1.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"redis": "^4.6.8",
|
||||
"sqlite3": "^5.1.6",
|
||||
"winston": "^3.10.0",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"@nestjs/cli": "^9.5.0",
|
||||
"@nestjs/schematics": "^9.2.0",
|
||||
"@nestjs/testing": "^9.4.3",
|
||||
|
|
@ -67,21 +75,13 @@
|
|||
"@types/uuid": "^9.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"cache-manager": "^5.2.3",
|
||||
"cache-manager-redis-store": "^3.0.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "28.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mockjs": "^1.1.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"plop": "^3.1.2",
|
||||
"prettier": "^2.8.8",
|
||||
"redis": "^4.6.8",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sqlite3": "^5.1.6",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "28.0.5",
|
||||
"ts-loader": "^9.4.4",
|
||||
|
|
@ -107,4 +107,4 @@
|
|||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PostModule } from '@/modules/post';
|
||||
import { PostModule } from '@/content/post';
|
||||
import { RoleModule } from '@/modules/role';
|
||||
import { UploadModule } from '@/modules/upload';
|
||||
import { UploadModule } from '@/storage/upload';
|
||||
import { PermissionModule } from '@/modules/permission';
|
||||
import { ConfigModule } from '@/config';
|
||||
import { LoggerModule } from '@/common/logger';
|
||||
|
|
@ -14,6 +14,7 @@ import { ResponseModule } from '@/common/response';
|
|||
import { SerializationModule } from '@/common/serialization';
|
||||
import { CacheModule } from './common/cache';
|
||||
import { ScanModule } from './utils/scan.module';
|
||||
import { ContentModule } from './content/content.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -85,6 +86,7 @@ import { ScanModule } from './utils/scan.module';
|
|||
* 文章模块
|
||||
*/
|
||||
PostModule,
|
||||
ContentModule
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class AllExecptionFilter implements ExceptionFilter {
|
|||
return response.status(exception.status).json(
|
||||
Response.create({
|
||||
code: ResponseCode.ERROR,
|
||||
message: '访问的路径不存在',
|
||||
message: '路径不存在',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ export class AllExecptionFilter implements ExceptionFilter {
|
|||
return response.status(HttpStatus.UNAUTHORIZED).json(
|
||||
Response.create({
|
||||
code: ResponseCode.TOKEN_EXPIRED,
|
||||
message: '登陆已过期'
|
||||
message: '登陆令牌已过期'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { BaseController } from '@/common/base';
|
||||
import { Respond, RespondType } from '@/common/response';
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, ParseIntPipe } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateCategoryDto } from './dto/create-category.dto';
|
||||
import { FindCategoryDto } from './dto/find-category.dto';
|
||||
import { UpdateCategoryDto } from './dto/update-category.dto';
|
||||
import { Category } from './entities/category.entity';
|
||||
import { CategoryService } from './category.service';
|
||||
|
||||
@ApiTags('category')
|
||||
@Controller('categories')
|
||||
export class CategoryController extends BaseController {
|
||||
constructor(private categoryService: CategoryService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分类
|
||||
*/
|
||||
@Post()
|
||||
addCategory(@Body() createCategoryDto: CreateCategoryDto) {
|
||||
return this.categoryService.create(createCategoryDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分页/过滤参数查询分类
|
||||
*/
|
||||
@Get()
|
||||
@Respond(RespondType.PAGINATION)
|
||||
@ApiOkResponse({ isArray: true, type: Category })
|
||||
getCategorys(@Query() query: FindCategoryDto) {
|
||||
return this.categoryService.findMany(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询分类
|
||||
*/
|
||||
@Get(':id')
|
||||
getCategory(@Param('id', ParseIntPipe) id: number): Promise<Category> {
|
||||
return this.categoryService.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID更新分类
|
||||
*/
|
||||
@Patch(':id')
|
||||
updateCategory(@Param('id', ParseIntPipe) id: number, @Body() updateCategoryDto: UpdateCategoryDto) {
|
||||
return this.categoryService.update(+id, updateCategoryDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID删除分类
|
||||
*/
|
||||
@Delete(':id')
|
||||
delCategory(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.categoryService.remove(+id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Category } from './entities/category.entity';
|
||||
import { CategoryController } from './category.controller';
|
||||
import { CategoryService } from './category.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Category])],
|
||||
controllers: [CategoryController],
|
||||
providers: [CategoryService],
|
||||
exports: [CategoryService],
|
||||
})
|
||||
export class CategoryModule {}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { BaseService } from '@/common/base';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Like, Repository } from 'typeorm';
|
||||
import { CreateCategoryDto } from './dto/create-category.dto';
|
||||
import { FindCategoryDto } from './dto/find-category.dto';
|
||||
import { UpdateCategoryDto } from './dto/update-category.dto';
|
||||
import { Category } from './entities/category.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService extends BaseService {
|
||||
constructor(@InjectRepository(Category) private categoryRepository: Repository<Category>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分类
|
||||
*/
|
||||
async create(createCategoryDto: CreateCategoryDto) {
|
||||
const category = this.categoryRepository.create(createCategoryDto);
|
||||
await this.categoryRepository.save(category);
|
||||
return category.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件/分页查询
|
||||
*/
|
||||
async findMany(findCategorydto: FindCategoryDto) {
|
||||
const { page, size } = findCategorydto;
|
||||
const { skip, take } = this.formatPagination(page, size, true);
|
||||
return this.categoryRepository.findAndCount({ skip, take });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
findOne(idOrOptions: number | Partial<Category>) {
|
||||
const where = typeof idOrOptions === 'number' ? { id: idOrOptions } : (idOrOptions as any);
|
||||
return this.categoryRepository.findOne({ where });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID更新
|
||||
*/
|
||||
update(id: number, updateCategoryDto: UpdateCategoryDto) {
|
||||
return this.categoryRepository.update(id, updateCategoryDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID删除(软删除)
|
||||
*/
|
||||
remove(id: number) {
|
||||
return this.categoryRepository.softDelete(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateCategoryDto {
|
||||
/**
|
||||
* 分类名称
|
||||
* @example '待分类'
|
||||
*/
|
||||
@IsString()
|
||||
title: string;
|
||||
/**
|
||||
* 分类别名
|
||||
* @example 'default'
|
||||
*/
|
||||
@IsString()
|
||||
slug: string;
|
||||
/**
|
||||
* 分类描述
|
||||
* @example '默认分类'
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
/**
|
||||
* 分类图标
|
||||
* @example 'default'
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
icon?: string;
|
||||
/**
|
||||
* 分类排序
|
||||
* @example 0
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
sort?: number;
|
||||
/**
|
||||
* 分类类型
|
||||
* @example 'category'
|
||||
*/
|
||||
@IsEnum(['category', 'tag'])
|
||||
@IsOptional()
|
||||
type: 'category' | 'tag';
|
||||
/**
|
||||
* 父级分类ID
|
||||
* @example 0
|
||||
*/
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
parentId?: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { PaginationDto } from '@/common/response';
|
||||
import { IntersectionType } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class FindCategoryDto extends IntersectionType(PaginationDto) {
|
||||
/**
|
||||
* 字段描述(Swagger用途)
|
||||
* @example '示例值'
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
demo?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateCategoryDto } from './create-category.dto';
|
||||
|
||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity({ orderBy: { id: 'DESC' } })
|
||||
export class Category extends BaseEntity {
|
||||
/**
|
||||
* 分类名称
|
||||
* @example '待分类'
|
||||
*/
|
||||
@Column()
|
||||
title: string;
|
||||
/**
|
||||
* 分类别名
|
||||
* @example 'default'
|
||||
*/
|
||||
@Column()
|
||||
slug: string;
|
||||
/**
|
||||
* 分类描述
|
||||
* @example '默认分类'
|
||||
*/
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
/**
|
||||
* 分类图标
|
||||
* @example 'default'
|
||||
*/
|
||||
@Column({ nullable: true })
|
||||
icon?: string;
|
||||
/**
|
||||
* 分类排序
|
||||
* @example 0
|
||||
*/
|
||||
@Column({ default: 0 })
|
||||
sort?: number;
|
||||
/**
|
||||
* 分类类型
|
||||
* @example 'category'
|
||||
*/
|
||||
@Column({ default: 'category' })
|
||||
type?: 'category' | 'tag';
|
||||
/**
|
||||
* 父级分类ID
|
||||
* @example 0
|
||||
*/
|
||||
@Column({ default: 0, nullable: true })
|
||||
parentId?: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export * from './entities/category.entity';
|
||||
export * from './category.controller';
|
||||
export * from './category.module';
|
||||
export * from './category.service';
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { CategoryModule } from './category';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
/**
|
||||
* 分类模块
|
||||
*/
|
||||
CategoryModule
|
||||
],
|
||||
})
|
||||
export class ContentModule {}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseEntity } from '@/database';
|
||||
import { Post } from '@/modules/post';
|
||||
import { Post } from '@/content/post';
|
||||
import { Role } from '@/modules/role';
|
||||
import { ApiHideProperty } from '@nestjs/swagger';
|
||||
import { Exclude } from 'class-transformer';
|
||||
|
|
|
|||
Loading…
Reference in New Issue