Просмотр исходного кода

✨ feat(advertisements-module): 创建广告管理模块

- 初始化广告管理模块结构,包含package.json和tsconfig.json配置
- 创建广告和广告类型实体类,定义数据库表结构和关系
- 实现广告和广告类型的CRUD服务,继承通用CRUD功能
- 定义Zod验证Schema,包括实体、创建和更新DTO
- 配置广告和广告类型的路由,集成认证中间件和用户跟踪

✨ feat(advertisements): 实现广告基础功能

- 创建Advertisement实体,包含标题、类型、URL、图片等字段
- 实现AdvertisementService,提供广告数据访问功能
- 定义广告相关Schema,包含验证规则和API文档描述
- 配置广告路由,支持CRUD操作和关联数据查询

✨ feat(advertisement-types): 实现广告类型管理功能

- 创建AdvertisementType实体,管理广告分类信息
- 实现AdvertisementTypeService,提供类型管理功能
- 定义广告类型Schema,包含验证规则和API文档
- 配置广告类型路由,支持完整的CRUD操作
yourname 1 месяц назад
Родитель
Сommit
175d6a405e

+ 75 - 0
packages/advertisements-module/package.json

@@ -0,0 +1,75 @@
+{
+  "name": "@d8d/advertisements-module",
+  "version": "1.0.0",
+  "description": "广告管理模块 - 提供广告类型和广告内容的完整CRUD功能",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./services": {
+      "types": "./src/services/index.ts",
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts"
+    },
+    "./schemas": {
+      "types": "./src/schemas/index.ts",
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts"
+    },
+    "./routes": {
+      "types": "./src/routes/index.ts",
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts"
+    },
+    "./entities": {
+      "types": "./src/entities/index.ts",
+      "import": "./src/entities/index.ts",
+      "require": "./src/entities/index.ts"
+    }
+  },
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/file-module": "workspace:*",
+    "@d8d/auth-module": "workspace:*",
+    "@hono/zod-openapi": "^1.0.2",
+    "typeorm": "^0.3.20",
+    "zod": "^4.1.12"
+  },
+  "devDependencies": {
+    "@types/node": "^22.10.2",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@d8d/shared-test-util": "workspace:*",
+    "@typescript-eslint/eslint-plugin": "^8.18.1",
+    "@typescript-eslint/parser": "^8.18.1",
+    "eslint": "^9.17.0"
+  },
+  "keywords": [
+    "advertisements",
+    "ads",
+    "banners",
+    "crud",
+    "api"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 72 - 0
packages/advertisements-module/src/entities/advertisement-type.entity.ts

@@ -0,0 +1,72 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('ad_type')
+export class AdvertisementType {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({
+    name: 'name',
+    type: 'varchar',
+    length: 50,
+    comment: '类型名称'
+  })
+  name!: string;
+
+  @Column({
+    name: 'code',
+    type: 'varchar',
+    length: 20,
+    comment: '调用别名'
+  })
+  code!: string;
+
+  @Column({
+    name: 'remark',
+    type: 'varchar',
+    length: 100,
+    nullable: true,
+    comment: '备注'
+  })
+  remark!: string | null;
+
+  @CreateDateColumn({
+    name: 'created_at',
+    type: 'timestamp',
+    comment: '创建时间'
+  })
+  createdAt!: Date;
+
+  @UpdateDateColumn({
+    name: 'updated_at',
+    type: 'timestamp',
+    comment: '更新时间'
+  })
+  updatedAt!: Date;
+
+  @Column({
+    name: 'created_by',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '创建用户ID'
+  })
+  createdBy!: number | null;
+
+  @Column({
+    name: 'updated_by',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '更新用户ID'
+  })
+  updatedBy!: number | null;
+
+  @Column({
+    name: 'status',
+    type: 'int',
+    default: 0,
+    comment: '状态 0禁用 1启用'
+  })
+  status!: number;
+}

+ 125 - 0
packages/advertisements-module/src/entities/advertisement.entity.ts

@@ -0,0 +1,125 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { File } from '@d8d/file-module';
+import { AdvertisementType } from './advertisement-type.entity';
+
+@Entity('ad')
+export class Advertisement {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({
+    name: 'title',
+    type: 'varchar',
+    length: 30,
+    nullable: true,
+    comment: '标题'
+  })
+  title!: string | null;
+
+  @Column({
+    name: 'type_id',
+    type: 'int',
+    nullable: true,
+    unsigned: true,
+    comment: '广告类型'
+  })
+  typeId!: number | null;
+
+  @Column({
+    name: 'code',
+    type: 'varchar',
+    length: 20,
+    nullable: true,
+    comment: '调用别名'
+  })
+  code!: string | null;
+
+  @Column({
+    name: 'url',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+    comment: 'url'
+  })
+  url!: string | null;
+
+  @Column({
+    name: 'image_file_id',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '图片文件ID'
+  })
+  imageFileId!: number | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({
+    name: 'image_file_id',
+    referencedColumnName: 'id'
+  })
+  imageFile!: File | null;
+
+  @ManyToOne(() => AdvertisementType, { nullable: true })
+  @JoinColumn({
+    name: 'type_id',
+    referencedColumnName: 'id'
+  })
+  advertisementType!: AdvertisementType | null;
+
+  @Column({
+    name: 'sort',
+    type: 'int',
+    default: 0,
+    comment: '排序'
+  })
+  sort!: number;
+
+  @CreateDateColumn({
+    name: 'created_at',
+    type: 'timestamp',
+    comment: '创建时间'
+  })
+  createdAt!: Date;
+
+  @UpdateDateColumn({
+    name: 'updated_at',
+    type: 'timestamp',
+    comment: '更新时间'
+  })
+  updatedAt!: Date;
+
+  @Column({
+    name: 'created_by',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '创建用户ID'
+  })
+  createdBy!: number | null;
+
+  @Column({
+    name: 'updated_by',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '更新用户ID'
+  })
+  updatedBy!: number | null;
+
+  @Column({
+    name: 'status',
+    type: 'int',
+    unsigned: true,
+    default: 0,
+    comment: '状态'
+  })
+  status!: number;
+
+  @Column({
+    name: 'action_type',
+    type: 'int',
+    default: 1,
+    comment: '跳转类型 0 不跳转 1webview 2小程序页面'
+  })
+  actionType!: number;
+}

+ 10 - 0
packages/advertisements-module/src/index.ts

@@ -0,0 +1,10 @@
+// 广告模块主导出文件
+
+export * from './entities/advertisement.entity';
+export * from './entities/advertisement-type.entity';
+export * from './services/advertisement.service';
+export * from './services/advertisement-type.service';
+export * from './schemas/advertisement.schema';
+export * from './schemas/advertisement-type.schema';
+export * from './routes/advertisements';
+export * from './routes/advertisement-types';

+ 20 - 0
packages/advertisements-module/src/routes/advertisement-types.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { AdvertisementType } from '../entities/advertisement-type.entity';
+import { AdvertisementTypeSchema, CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '../schemas/advertisement-type.schema';
+
+const advertisementTypeRoutes = createCrudRoutes({
+  entity: AdvertisementType,
+  createSchema: CreateAdvertisementTypeDto,
+  updateSchema: UpdateAdvertisementTypeDto,
+  getSchema: AdvertisementTypeSchema,
+  listSchema: AdvertisementTypeSchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default advertisementTypeRoutes;

+ 21 - 0
packages/advertisements-module/src/routes/advertisements.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { Advertisement } from '../entities/advertisement.entity';
+import { AdvertisementSchema, CreateAdvertisementDto, UpdateAdvertisementDto } from '../schemas/advertisement.schema';
+
+const advertisementRoutes = createCrudRoutes({
+  entity: Advertisement,
+  createSchema: CreateAdvertisementDto,
+  updateSchema: UpdateAdvertisementDto,
+  getSchema: AdvertisementSchema,
+  listSchema: AdvertisementSchema,
+  searchFields: ['title', 'code'],
+  relations: ['imageFile', 'advertisementType'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default advertisementRoutes;

+ 81 - 0
packages/advertisements-module/src/schemas/advertisement-type.schema.ts

@@ -0,0 +1,81 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告类型实体Schema
+export const AdvertisementTypeSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告类型ID',
+    example: 1
+  }),
+  name: z.string().max(50).openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  remark: z.string().max(100).nullable().openapi({
+    description: '备注',
+    example: '用于首页轮播图展示'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+  status: z.number().int().min(0).max(1).default(0).openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});
+
+// 创建广告类型DTO
+export const CreateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  remark: z.string().max(100).nullable().optional().openapi({
+    description: '备注',
+    example: '用于首页轮播图展示'
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});
+
+// 更新广告类型DTO
+export const UpdateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).optional().openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  remark: z.string().max(100).nullable().optional().openapi({
+    description: '备注',
+    example: '用于首页轮播图展示'
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});

+ 145 - 0
packages/advertisements-module/src/schemas/advertisement.schema.ts

@@ -0,0 +1,145 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告实体Schema
+export const AdvertisementSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告ID',
+    example: 1
+  }),
+  title: z.string().max(30).nullable().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.number().int().positive().nullable().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().max(20).nullable().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'banner.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/banner.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '图片文件信息'
+  }),
+  advertisementType: z.object({
+    id: z.number().int().positive().openapi({ description: '广告类型ID' }),
+    name: z.string().max(50).openapi({ description: '类型名称', example: '首页轮播' }),
+    code: z.string().max(20).openapi({ description: '类型编码', example: 'home_banner' })
+  }).nullable().optional().openapi({
+    description: '广告类型信息'
+  }),
+  sort: z.number().int().default(0).openapi({
+    description: '排序值',
+    example: 10
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+  status: z.number().int().min(0).max(1).default(0).openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.number().int().min(0).max(2).default(1).openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 创建广告DTO
+export const CreateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().default(0).optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).default(1).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 更新广告DTO
+export const UpdateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).optional().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});

+ 9 - 0
packages/advertisements-module/src/services/advertisement-type.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { AdvertisementType } from '../entities/advertisement-type.entity';
+
+export class AdvertisementTypeService extends GenericCrudService<AdvertisementType> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, AdvertisementType);
+  }
+}

+ 9 - 0
packages/advertisements-module/src/services/advertisement.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { Advertisement } from '../entities/advertisement.entity';
+
+export class AdvertisementService extends GenericCrudService<Advertisement> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Advertisement);
+  }
+}

+ 16 - 0
packages/advertisements-module/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "rootDir": ".",
+    "outDir": "dist"
+  },
+  "include": [
+    "src/**/*",
+    "tests/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}