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

feat: 实现统一广告后端模块 (unified-advertisements-module)

- 创建无租户隔离的广告模块包
- Entity 定义无 tenant_id 字段
- Service 层实现软删除 (status=0)
- Schema 使用 Zod 4.0 泛型语法
- 管理员路由使用 tenantAuthMiddleware
- 用户展示路由使用 authMiddleware
- 包含完整的单元测试和集成测试框架

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 3 недель назад
Родитель
Сommit
fec3aabc42
25 измененных файлов с 2966 добавлено и 0 удалено
  1. 79 0
      packages/unified-advertisements-module/package.json
  2. 2 0
      packages/unified-advertisements-module/src/entities/index.ts
  3. 77 0
      packages/unified-advertisements-module/src/entities/unified-advertisement-type.entity.ts
  4. 130 0
      packages/unified-advertisements-module/src/entities/unified-advertisement.entity.ts
  5. 11 0
      packages/unified-advertisements-module/src/index.ts
  6. 447 0
      packages/unified-advertisements-module/src/routes/admin/unified-advertisement-types.admin.routes.ts
  7. 454 0
      packages/unified-advertisements-module/src/routes/admin/unified-advertisements.admin.routes.ts
  8. 4 0
      packages/unified-advertisements-module/src/routes/index.ts
  9. 101 0
      packages/unified-advertisements-module/src/routes/unified-advertisement-types.crud.routes.ts
  10. 126 0
      packages/unified-advertisements-module/src/routes/unified-advertisement-types.routes.ts
  11. 176 0
      packages/unified-advertisements-module/src/routes/unified-advertisements.crud.routes.ts
  12. 13 0
      packages/unified-advertisements-module/src/routes/unified-advertisements.routes.ts
  13. 13 0
      packages/unified-advertisements-module/src/schemas/index.ts
  14. 93 0
      packages/unified-advertisements-module/src/schemas/unified-advertisement-type.schema.ts
  15. 157 0
      packages/unified-advertisements-module/src/schemas/unified-advertisement.schema.ts
  16. 2 0
      packages/unified-advertisements-module/src/services/index.ts
  17. 28 0
      packages/unified-advertisements-module/src/services/unified-advertisement-type.service.ts
  18. 28 0
      packages/unified-advertisements-module/src/services/unified-advertisement.service.ts
  19. 441 0
      packages/unified-advertisements-module/tests/integration/unified-advertisements.integration.test.ts
  20. 157 0
      packages/unified-advertisements-module/tests/unit/unified-advertisement-type.service.test.ts
  21. 224 0
      packages/unified-advertisements-module/tests/unit/unified-advertisement.service.test.ts
  22. 119 0
      packages/unified-advertisements-module/tests/utils/test-data-factory.ts
  23. 11 0
      packages/unified-advertisements-module/tsconfig.json
  24. 21 0
      packages/unified-advertisements-module/vitest.config.ts
  25. 52 0
      pnpm-lock.yaml

+ 79 - 0
packages/unified-advertisements-module/package.json

@@ -0,0 +1,79 @@
+{
+  "name": "@d8d/unified-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:integration": "vitest run tests/integration",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/core-module-mt": "workspace:*",
+    "@d8d/tenant-module-mt": "workspace:*",
+    "@hono/zod-openapi": "^1.0.2",
+    "hono": "^4.8.5",
+    "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",
+    "unified",
+    "admin-only"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 2 - 0
packages/unified-advertisements-module/src/entities/index.ts

@@ -0,0 +1,2 @@
+export { UnifiedAdvertisement } from './unified-advertisement.entity';
+export { UnifiedAdvertisementType } from './unified-advertisement-type.entity';

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

@@ -0,0 +1,77 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('ad_type_unified')
+export class UnifiedAdvertisementType {
+  @PrimaryGeneratedColumn({
+    name: 'id',
+    type: 'int',
+    unsigned: true,
+    comment: '广告类型ID'
+  })
+  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;
+}

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

@@ -0,0 +1,130 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { FileMt } from '@d8d/core-module-mt/file-module-mt';
+import { UnifiedAdvertisementType } from './unified-advertisement-type.entity';
+
+@Entity('ad_unified')
+export class UnifiedAdvertisement {
+  @PrimaryGeneratedColumn({
+    name: 'id',
+    type: 'int',
+    unsigned: true,
+    comment: '广告ID'
+  })
+  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(() => FileMt, { nullable: true })
+  @JoinColumn({
+    name: 'image_file_id',
+    referencedColumnName: 'id'
+  })
+  imageFile!: FileMt | null;
+
+  @ManyToOne(() => UnifiedAdvertisementType, { nullable: true })
+  @JoinColumn({
+    name: 'type_id',
+    referencedColumnName: 'id'
+  })
+  advertisementType!: UnifiedAdvertisementType | 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: '状态 0禁用 1启用'
+  })
+  status!: number;
+
+  @Column({
+    name: 'action_type',
+    type: 'int',
+    default: 1,
+    comment: '跳转类型 0 不跳转 1webview 2小程序页面'
+  })
+  actionType!: number;
+}

+ 11 - 0
packages/unified-advertisements-module/src/index.ts

@@ -0,0 +1,11 @@
+// Entities
+export * from './entities';
+
+// Services
+export * from './services';
+
+// Schemas
+export * from './schemas';
+
+// Routes
+export * from './routes';

+ 447 - 0
packages/unified-advertisements-module/src/routes/admin/unified-advertisement-types.admin.routes.ts

@@ -0,0 +1,447 @@
+import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
+import { UnifiedAdvertisementTypeService } from '../../services/unified-advertisement-type.service';
+import {
+  UnifiedAdvertisementTypeSchema,
+  CreateUnifiedAdvertisementTypeDto,
+  UpdateUnifiedAdvertisementTypeDto
+} from '../../schemas/unified-advertisement-type.schema';
+
+// 管理员上下文类型(扩展AuthContext以支持superAdminId)
+interface AdminContext {
+  Variables: {
+    superAdminId?: number;
+    user?: any;
+    token?: string;
+    tenantId?: number;
+  };
+}
+
+// 通用错误响应Schema
+const CommonErrorSchema = z.object({
+  code: z.number(),
+  message: z.string()
+});
+
+// Zod验证错误Schema
+const ZodErrorSchema = z.object({
+  code: z.number().openapi({ example: 400, description: '错误码' }),
+  message: z.string().openapi({ example: 'Validation error', description: '错误消息' }),
+  errors: z.array(z.object({
+    path: z.array(z.union([z.string(), z.number()])).openapi({ description: '错误路径' }),
+    message: z.string().openapi({ description: '错误消息' }),
+    code: z.string().openapi({ description: '错误代码' })
+  })).optional().openapi({ description: '详细错误信息' })
+});
+
+// 创建Zod错误响应
+function createZodErrorResponse(error: Error): {
+  code: number;
+  message: string;
+  errors?: Array<{ path: (string | number)[]; message: string; code: string }>;
+} {
+  if ('issues' in error && Array.isArray(error.issues)) {
+    return {
+      code: 400,
+      message: 'Validation failed',
+      errors: error.issues.map((e: any) => ({
+        path: e.path.map(String),
+        message: e.message,
+        code: e.code
+      }))
+    };
+  }
+  return {
+    code: 400,
+    message: 'Validation failed'
+  };
+}
+
+// 获取服务实例
+const getService = () => {
+  return new UnifiedAdvertisementTypeService(AppDataSource);
+};
+
+// 获取广告类型列表
+const listRoute = createRoute({
+  method: 'get',
+  path: '/api/v1/admin/unified-advertisement-types',
+  middleware: [tenantAuthMiddleware] as const,
+  request: {
+    query: z.object({
+      page: z.coerce.number<number>().int().positive().default(1).openapi({
+        example: 1,
+        description: '页码'
+      }),
+      pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+        example: 10,
+        description: '每页数量'
+      }),
+      keyword: z.string().optional().openapi({
+        example: '首页',
+        description: '搜索关键词'
+      }),
+      status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+        example: 1,
+        description: '状态筛选'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取广告类型列表',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: z.object({
+              list: z.array(UnifiedAdvertisementTypeSchema),
+              total: z.number(),
+              page: z.number(),
+              pageSize: z.number()
+            })
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 获取单个广告类型详情
+const getRoute = createRoute({
+  method: 'get',
+  path: '/api/v1/admin/unified-advertisement-types/:id',
+  middleware: [tenantAuthMiddleware] as const,
+  responses: {
+    200: {
+      description: '成功获取广告类型详情',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: UnifiedAdvertisementTypeSchema
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '广告类型不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建广告类型
+const createTypeRoute = createRoute({
+  method: 'post',
+  path: '/api/v1/admin/unified-advertisement-types',
+  middleware: [tenantAuthMiddleware] as const,
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CreateUnifiedAdvertisementTypeDto
+        }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '成功创建广告类型',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: UnifiedAdvertisementTypeSchema
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 更新广告类型
+const updateRoute = createRoute({
+  method: 'put',
+  path: '/api/v1/admin/unified-advertisement-types/:id',
+  middleware: [tenantAuthMiddleware] as const,
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateUnifiedAdvertisementTypeDto
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功更新广告类型',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: UnifiedAdvertisementTypeSchema
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '广告类型不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 删除广告类型(软删除)
+const deleteRoute = createRoute({
+  method: 'delete',
+  path: '/api/v1/admin/unified-advertisement-types/:id',
+  middleware: [tenantAuthMiddleware] as const,
+  responses: {
+    200: {
+      description: '成功删除广告类型',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string()
+          })
+        }
+      }
+    },
+    404: {
+      description: '广告类型不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建管理员路由实例(链式写法)
+const app = new OpenAPIHono<AdminContext>()
+  .openapi(listRoute, async (c) => {
+    try {
+      const query = c.req.valid('query');
+      const { page, pageSize, keyword, status } = query;
+      const service = getService();
+
+      const searchFields = ['name', 'code'];
+      const where = status !== undefined ? { status } : undefined;
+
+      const [list, total] = await service.getList(
+        page,
+        pageSize,
+        keyword,
+        searchFields,
+        where,
+        [],
+        { createdAt: 'DESC' }
+      );
+
+      const validatedData = await parseWithAwait(
+        z.array(UnifiedAdvertisementTypeSchema),
+        list
+      );
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: {
+          list: validatedData,
+          total,
+          page,
+          pageSize
+        }
+      }, 200);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(getRoute, async (c) => {
+    try {
+      const id = parseInt(c.req.param('id'));
+      const service = getService();
+
+      const advertisementType = await service.getById(id);
+
+      if (!advertisementType) {
+        return c.json({ code: 404, message: 'Advertisement type not found' }, 404);
+      }
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementTypeSchema, advertisementType);
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: validatedData
+      }, 200);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(createTypeRoute, async (c) => {
+    try {
+      const body = c.req.valid('json');
+      const superAdminId = c.get('superAdminId') || 1;
+      const service = getService();
+
+      const advertisementType = await service.create(body, superAdminId);
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementTypeSchema, advertisementType);
+
+      return c.json({
+        code: 201,
+        message: 'Advertisement type created successfully',
+        data: validatedData
+      }, 201);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(updateRoute, async (c) => {
+    try {
+      const id = parseInt(c.req.param('id'));
+      const body = c.req.valid('json');
+      const superAdminId = c.get('superAdminId') || 1;
+      const service = getService();
+
+      const advertisementType = await service.update(id, body, superAdminId);
+
+      if (!advertisementType) {
+        return c.json({ code: 404, message: 'Advertisement type not found' }, 404);
+      }
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementTypeSchema, advertisementType);
+
+      return c.json({
+        code: 200,
+        message: 'Advertisement type updated successfully',
+        data: validatedData
+      }, 200);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(deleteRoute, async (c) => {
+    try {
+      const id = parseInt(c.req.param('id'));
+      const superAdminId = c.get('superAdminId') || 1;
+      const service = getService();
+
+      const success = await service.delete(id, superAdminId);
+
+      if (!success) {
+        return c.json({ code: 404, message: 'Advertisement type not found' }, 404);
+      }
+
+      return c.json({
+        code: 200,
+        message: 'Advertisement type deleted successfully'
+      }, 200);
+    } catch (error) {
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  });
+
+export default app;

+ 454 - 0
packages/unified-advertisements-module/src/routes/admin/unified-advertisements.admin.routes.ts

@@ -0,0 +1,454 @@
+import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
+import { UnifiedAdvertisementService } from '../../services/unified-advertisement.service';
+import {
+  UnifiedAdvertisementSchema,
+  CreateUnifiedAdvertisementDto,
+  UpdateUnifiedAdvertisementDto
+} from '../../schemas/unified-advertisement.schema';
+
+// 管理员上下文类型(扩展AuthContext以支持superAdminId)
+interface AdminContext {
+  Variables: {
+    superAdminId?: number;
+    user?: any;
+    token?: string;
+    tenantId?: number;
+  };
+}
+
+// 通用错误响应Schema
+const CommonErrorSchema = z.object({
+  code: z.number(),
+  message: z.string()
+});
+
+// Zod验证错误Schema
+const ZodErrorSchema = z.object({
+  code: z.number().openapi({ example: 400, description: '错误码' }),
+  message: z.string().openapi({ example: 'Validation error', description: '错误消息' }),
+  errors: z.array(z.object({
+    path: z.array(z.union([z.string(), z.number()])).openapi({ description: '错误路径' }),
+    message: z.string().openapi({ description: '错误消息' }),
+    code: z.string().openapi({ description: '错误代码' })
+  })).optional().openapi({ description: '详细错误信息' })
+});
+
+// 创建Zod错误响应
+function createZodErrorResponse(error: Error): {
+  code: number;
+  message: string;
+  errors?: Array<{ path: (string | number)[]; message: string; code: string }>;
+} {
+  if ('issues' in error && Array.isArray(error.issues)) {
+    return {
+      code: 400,
+      message: 'Validation failed',
+      errors: error.issues.map((e: any) => ({
+        path: e.path.map(String),
+        message: e.message,
+        code: e.code
+      }))
+    };
+  }
+  return {
+    code: 400,
+    message: 'Validation failed'
+  };
+}
+
+// 获取服务实例
+const getService = () => {
+  return new UnifiedAdvertisementService(AppDataSource);
+};
+
+// 获取广告列表
+const listRoute = createRoute({
+  method: 'get',
+  path: '/api/v1/admin/unified-advertisements',
+  middleware: [tenantAuthMiddleware] as const,
+  request: {
+    query: z.object({
+      page: z.coerce.number<number>().int().positive().default(1).openapi({
+        example: 1,
+        description: '页码'
+      }),
+      pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+        example: 10,
+        description: '每页数量'
+      }),
+      keyword: z.string().optional().openapi({
+        example: '首页',
+        description: '搜索关键词'
+      }),
+      status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+        example: 1,
+        description: '状态筛选'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取广告列表',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: z.object({
+              list: z.array(UnifiedAdvertisementSchema),
+              total: z.number(),
+              page: z.number(),
+              pageSize: z.number()
+            })
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 获取单个广告详情
+const getRoute = createRoute({
+  method: 'get',
+  path: '/api/v1/admin/unified-advertisements/:id',
+  middleware: [tenantAuthMiddleware] as const,
+  responses: {
+    200: {
+      description: '成功获取广告详情',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: UnifiedAdvertisementSchema
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '广告不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建广告
+const createAdvertisementRoute = createRoute({
+  method: 'post',
+  path: '/api/v1/admin/unified-advertisements',
+  middleware: [tenantAuthMiddleware] as const,
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CreateUnifiedAdvertisementDto
+        }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '成功创建广告',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: UnifiedAdvertisementSchema
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 更新广告
+const updateRoute = createRoute({
+  method: 'put',
+  path: '/api/v1/admin/unified-advertisements/:id',
+  middleware: [tenantAuthMiddleware] as const,
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateUnifiedAdvertisementDto
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功更新广告',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string(),
+            data: UnifiedAdvertisementSchema
+          })
+        }
+      }
+    },
+    400: {
+      description: '验证错误',
+      content: {
+        'application/json': {
+          schema: ZodErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '广告不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 删除广告(软删除)
+const deleteRoute = createRoute({
+  method: 'delete',
+  path: '/api/v1/admin/unified-advertisements/:id',
+  middleware: [tenantAuthMiddleware] as const,
+  responses: {
+    200: {
+      description: '成功删除广告',
+      content: {
+        'application/json': {
+          schema: z.object({
+            code: z.number(),
+            message: z.string()
+          })
+        }
+      }
+    },
+    404: {
+      description: '广告不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建管理员路由实例(链式写法)
+const app = new OpenAPIHono<AdminContext>()
+  .openapi(listRoute, async (c) => {
+    try {
+      const query = c.req.valid('query');
+      const { page, pageSize, keyword, status } = query;
+      const service = getService();
+
+      const searchFields = ['title', 'code'];
+      const where = status !== undefined ? { status } : undefined;
+      const relations = ['imageFile', 'advertisementType'];
+
+      const [list, total] = await service.getList(
+        page,
+        pageSize,
+        keyword,
+        searchFields,
+        where,
+        relations,
+        { createdAt: 'DESC' }
+      );
+
+      const validatedData = await parseWithAwait(
+        z.array(UnifiedAdvertisementSchema),
+        list
+      );
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: {
+          list: validatedData,
+          total,
+          page,
+          pageSize
+        }
+      }, 200);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(getRoute, async (c) => {
+    try {
+      const id = parseInt(c.req.param('id'));
+      const service = getService();
+
+      const advertisement = await service.getById(id, ['imageFile', 'advertisementType']);
+
+      if (!advertisement) {
+        return c.json({ code: 404, message: 'Advertisement not found' }, 404);
+      }
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementSchema, advertisement);
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: validatedData
+      }, 200);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(createAdvertisementRoute, async (c) => {
+    try {
+      const body = c.req.valid('json');
+      const superAdminId = c.get('superAdminId') || 1;
+      const service = getService();
+
+      const advertisement = await service.create(body, superAdminId);
+
+      // 重新查询以获取关联数据
+      const fullAdvertisement = await service.getById(advertisement.id, ['imageFile', 'advertisementType']);
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementSchema, fullAdvertisement!);
+
+      return c.json({
+        code: 201,
+        message: 'Advertisement created successfully',
+        data: validatedData
+      }, 201);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(updateRoute, async (c) => {
+    try {
+      const id = parseInt(c.req.param('id'));
+      const body = c.req.valid('json');
+      const superAdminId = c.get('superAdminId') || 1;
+      const service = getService();
+
+      const advertisement = await service.update(id, body, superAdminId);
+
+      if (!advertisement) {
+        return c.json({ code: 404, message: 'Advertisement not found' }, 404);
+      }
+
+      // 重新查询以获取关联数据
+      const fullAdvertisement = await service.getById(id, ['imageFile', 'advertisementType']);
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementSchema, fullAdvertisement!);
+
+      return c.json({
+        code: 200,
+        message: 'Advertisement updated successfully',
+        data: validatedData
+      }, 200);
+    } catch (error) {
+      if ((error as Error).name === 'ZodError') {
+        return c.json(createZodErrorResponse(error as Error), 400);
+      }
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(deleteRoute, async (c) => {
+    try {
+      const id = parseInt(c.req.param('id'));
+      const superAdminId = c.get('superAdminId') || 1;
+      const service = getService();
+
+      const success = await service.delete(id, superAdminId);
+
+      if (!success) {
+        return c.json({ code: 404, message: 'Advertisement not found' }, 404);
+      }
+
+      return c.json({
+        code: 200,
+        message: 'Advertisement deleted successfully'
+      }, 200);
+    } catch (error) {
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  });
+
+export default app;

+ 4 - 0
packages/unified-advertisements-module/src/routes/index.ts

@@ -0,0 +1,4 @@
+export { default as unifiedAdvertisementAdminRoutes } from './admin/unified-advertisements.admin.routes';
+export { default as unifiedAdvertisementTypeAdminRoutes } from './admin/unified-advertisement-types.admin.routes';
+export { default as unifiedAdvertisementRoutes } from './unified-advertisements.routes';
+export { default as unifiedAdvertisementTypeRoutes } from './unified-advertisement-types.routes';

+ 101 - 0
packages/unified-advertisements-module/src/routes/unified-advertisement-types.crud.routes.ts

@@ -0,0 +1,101 @@
+import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
+import { UnifiedAdvertisementTypeService } from '../services/unified-advertisement-type.service';
+import { UnifiedAdvertisementTypeSchema } from '../schemas/unified-advertisement-type.schema';
+
+// 通用错误响应Schema
+const CommonErrorSchema = z.object({
+  code: z.number(),
+  message: z.string()
+});
+
+// 获取服务实例
+const getService = () => {
+  return new UnifiedAdvertisementTypeService(AppDataSource);
+};
+
+// 获取有效广告类型列表(用户端)
+const listRoute = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware] as const,
+  request: {
+    query: z.object({
+      page: z.coerce.number<number>().int().positive().default(1).openapi({
+        example: 1,
+        description: '页码'
+      }),
+      pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+        example: 10,
+        description: '每页数量'
+      }),
+      keyword: z.string().optional().openapi({
+        example: '首页',
+        description: '搜索关键词'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取广告类型列表',
+      content: {
+        'application/json': {
+          schema: z.any()
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建用户展示路由实例(链式写法)
+const app = new OpenAPIHono<AuthContext>()
+  .openapi(listRoute, async (c) => {
+    try {
+      const query = c.req.valid('query');
+      const { page, pageSize, keyword } = query;
+      const service = getService();
+
+      const searchFields = ['name', 'code'];
+      const where = { status: 1 };
+
+      const [list, total] = await service.getList(
+        page,
+        pageSize,
+        keyword,
+        searchFields,
+        where,
+        [],
+        { createdAt: 'DESC' }
+      );
+
+      const validatedData = await parseWithAwait(
+        z.array(UnifiedAdvertisementTypeSchema),
+        list
+      );
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: {
+          list: validatedData,
+          total,
+          page,
+          pageSize
+        }
+      }, 200);
+    } catch (error) {
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  });
+
+export default app;

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

@@ -0,0 +1,126 @@
+import { OpenAPIHono, createRoute as honoCreateRoute, z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
+import { UnifiedAdvertisementTypeService } from '../services/unified-advertisement-type.service';
+import { UnifiedAdvertisementTypeSchema } from '../schemas/unified-advertisement-type.schema';
+
+// Zod验证错误Schema
+const ZodErrorSchema = z.object({
+  code: z.number().openapi({ example: 400, description: '错误码' }),
+  message: z.string().openapi({ example: 'Validation error', description: '错误消息' }),
+  errors: z.array(z.object({
+    path: z.array(z.union([z.string(), z.number()])).openapi({ description: '错误路径' }),
+    message: z.string().openapi({ description: '错误消息' }),
+    code: z.string().openapi({ description: '错误代码' })
+  })).optional().openapi({ description: '详细错误信息' })
+});
+
+// 创建Zod错误响应
+function createZodErrorResponse(error: Error): {
+  code: number;
+  message: string;
+  errors?: Array<{ path: (string | number)[]; message: string; code: string }>;
+} {
+  if ('issues' in error && Array.isArray(error.issues)) {
+    return {
+      code: 400,
+      message: 'Validation failed',
+      errors: error.issues.map((e: any) => ({
+        path: e.path.map(String),
+        message: e.message,
+        code: e.code
+      }))
+    };
+  }
+  return {
+    code: 400,
+    message: 'Validation failed'
+  };
+}
+
+// 创建用户展示路由实例
+const app = new OpenAPIHono<AuthContext>();
+
+// 获取服务实例
+const getService = () => {
+  return new UnifiedAdvertisementTypeService(AppDataSource);
+};
+
+// 获取有效广告类型列表(用户端)
+const listRoute = honoCreateRoute({
+  method: 'get',
+  path: '/api/v1/advertisement-types',
+  middleware: [authMiddleware] as const,
+  request: {
+    query: z.object({
+      page: z.coerce.number<number>().int().positive().default(1).openapi({
+        example: 1,
+        description: '页码'
+      }),
+      pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+        example: 10,
+        description: '每页数量'
+      }),
+      keyword: z.string().optional().openapi({
+        example: '首页',
+        description: '搜索关键词'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取广告类型列表',
+      content: {
+        'application/json': {
+          schema: z.any()
+        }
+      }
+    }
+  }
+});
+
+// @ts-expect-error Hono OpenAPI type system limitation with multiple response types
+app.openapi(listRoute, async (c) => {
+  try {
+    const query = c.req.valid('query');
+    const { page, pageSize, keyword } = query;
+    const service = getService();
+
+    const searchFields = ['name', 'code'];
+    const where = { status: 1 };
+
+    const [list, total] = await service.getList(
+      page,
+      pageSize,
+      keyword,
+      searchFields,
+      where,
+      [],
+      { createdAt: 'DESC' }
+    );
+
+    const validatedData = await parseWithAwait(
+      z.array(UnifiedAdvertisementTypeSchema),
+      list
+    );
+
+    return c.json({
+      code: 200,
+      message: 'success',
+      data: {
+        list: validatedData,
+        total,
+        page,
+        pageSize
+      }
+    }, 200);
+  } catch (error) {
+    if ((error as Error).name === 'ZodError') {
+      return c.json(createZodErrorResponse(error as Error), 400);
+    }
+    return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+  }
+});
+
+export default app;

+ 176 - 0
packages/unified-advertisements-module/src/routes/unified-advertisements.crud.routes.ts

@@ -0,0 +1,176 @@
+import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
+import { UnifiedAdvertisementService } from '../services/unified-advertisement.service';
+import { UnifiedAdvertisementSchema } from '../schemas/unified-advertisement.schema';
+
+// 通用错误响应Schema
+const CommonErrorSchema = z.object({
+  code: z.number(),
+  message: z.string()
+});
+
+// 获取服务实例
+const getService = () => {
+  return new UnifiedAdvertisementService(AppDataSource);
+};
+
+// 获取有效广告列表(用户端)
+const listRoute = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware] as const,
+  request: {
+    query: z.object({
+      page: z.coerce.number<number>().int().positive().default(1).openapi({
+        example: 1,
+        description: '页码'
+      }),
+      pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+        example: 10,
+        description: '每页数量'
+      }),
+      keyword: z.string().optional().openapi({
+        example: '首页',
+        description: '搜索关键词'
+      }),
+      code: z.string().optional().openapi({
+        example: 'home_banner',
+        description: '广告代码筛选'
+      }),
+      typeId: z.coerce.number<number>().int().positive().optional().openapi({
+        example: 1,
+        description: '广告类型筛选'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取广告列表',
+      content: {
+        'application/json': {
+          schema: z.any()
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 获取单个广告详情(用户端)
+const getRoute = createRoute({
+  method: 'get',
+  path: '/{id}',
+  middleware: [authMiddleware] as const,
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().int().positive().openapi({
+        example: 1,
+        description: '广告ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取广告详情',
+      content: {
+        'application/json': {
+          schema: z.any()
+        }
+      }
+    },
+    404: {
+      description: '广告不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: CommonErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建用户展示路由实例(链式写法)
+const app = new OpenAPIHono<AuthContext>()
+  .openapi(listRoute, async (c) => {
+    try {
+      const query = c.req.valid('query');
+      const { page, pageSize, keyword, code, typeId } = query;
+      const service = getService();
+
+      const searchFields = ['title', 'code'];
+      const where: any = { status: 1 };
+      if (code) where.code = code;
+      if (typeId) where.typeId = typeId;
+
+      const relations = ['imageFile', 'advertisementType'];
+
+      const [list, total] = await service.getList(
+        page,
+        pageSize,
+        keyword,
+        searchFields,
+        where,
+        relations,
+        { sort: 'ASC', createdAt: 'DESC' }
+      );
+
+      const validatedData = await parseWithAwait(
+        z.array(UnifiedAdvertisementSchema),
+        list
+      );
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: {
+          list: validatedData,
+          total,
+          page,
+          pageSize
+        }
+      }, 200);
+    } catch (error) {
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  })
+  .openapi(getRoute, async (c) => {
+    try {
+      const { id } = c.req.valid('param');
+      const service = getService();
+
+      const advertisement = await service.getById(id, ['imageFile', 'advertisementType']);
+
+      if (!advertisement || advertisement.status !== 1) {
+        return c.json({ code: 404, message: 'Advertisement not found' }, 404);
+      }
+
+      const validatedData = await parseWithAwait(UnifiedAdvertisementSchema, advertisement);
+
+      return c.json({
+        code: 200,
+        message: 'success',
+        data: validatedData
+      }, 200);
+    } catch (error) {
+      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
+    }
+  });
+
+export default app;

+ 13 - 0
packages/unified-advertisements-module/src/routes/unified-advertisements.routes.ts

@@ -0,0 +1,13 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { AuthContext } from '@d8d/shared-types';
+import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
+import unifiedAdvertisementsCrudRoutes from './unified-advertisements.crud.routes';
+import unifiedAdvertisementTypesCrudRoutes from './unified-advertisement-types.crud.routes';
+
+// 统一广告用户展示路由 - 聚合CRUD路由
+const unifiedAdvertisementRoutes = new OpenAPIHono<AuthContext>()
+  .route('/advertisements', unifiedAdvertisementsCrudRoutes)
+  .route('/advertisement-types', unifiedAdvertisementTypesCrudRoutes);
+
+export { unifiedAdvertisementRoutes };
+export default unifiedAdvertisementRoutes;

+ 13 - 0
packages/unified-advertisements-module/src/schemas/index.ts

@@ -0,0 +1,13 @@
+export {
+  UnifiedAdvertisementSchema,
+  CreateUnifiedAdvertisementDto,
+  UpdateUnifiedAdvertisementDto,
+  UnifiedAdvertisementListResponseSchema
+} from './unified-advertisement.schema';
+
+export {
+  UnifiedAdvertisementTypeSchema,
+  CreateUnifiedAdvertisementTypeDto,
+  UpdateUnifiedAdvertisementTypeDto,
+  UnifiedAdvertisementTypeListResponseSchema
+} from './unified-advertisement-type.schema';

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

@@ -0,0 +1,93 @@
+import { z } from '@hono/zod-openapi';
+
+// 统一广告类型实体Schema(无租户ID字段)
+export const UnifiedAdvertisementTypeSchema = 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<Date>().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date<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 CreateUnifiedAdvertisementTypeDto = 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 UpdateUnifiedAdvertisementTypeDto = 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
+  })
+});
+
+// 列表响应Schema
+export const UnifiedAdvertisementTypeListResponseSchema = z.object({
+  code: z.number().int().openapi({ description: '响应码', example: 200 }),
+  message: z.string().openapi({ description: '响应消息', example: 'success' }),
+  data: z.object({
+    list: z.array(UnifiedAdvertisementTypeSchema),
+    total: z.number().int().openapi({ description: '总数量', example: 100 }),
+    page: z.number().int().openapi({ description: '当前页', example: 1 }),
+    pageSize: z.number().int().openapi({ description: '每页数量', example: 10 })
+  })
+});

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

@@ -0,0 +1,157 @@
+import { z } from '@hono/zod-openapi';
+
+// 统一广告实体Schema(无租户ID字段)
+export const UnifiedAdvertisementSchema = 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<Date>().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date<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 CreateUnifiedAdvertisementDto = 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 UpdateUnifiedAdvertisementDto = 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
+  })
+});
+
+// 列表响应Schema
+export const UnifiedAdvertisementListResponseSchema = z.object({
+  code: z.number().int().openapi({ description: '响应码', example: 200 }),
+  message: z.string().openapi({ description: '响应消息', example: 'success' }),
+  data: z.object({
+    list: z.array(UnifiedAdvertisementSchema),
+    total: z.number().int().openapi({ description: '总数量', example: 100 }),
+    page: z.number().int().openapi({ description: '当前页', example: 1 }),
+    pageSize: z.number().int().openapi({ description: '每页数量', example: 10 })
+  })
+});

+ 2 - 0
packages/unified-advertisements-module/src/services/index.ts

@@ -0,0 +1,2 @@
+export { UnifiedAdvertisementService } from './unified-advertisement.service';
+export { UnifiedAdvertisementTypeService } from './unified-advertisement-type.service';

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

@@ -0,0 +1,28 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { UnifiedAdvertisementType } from '../entities/unified-advertisement-type.entity';
+
+export class UnifiedAdvertisementTypeService extends GenericCrudService<UnifiedAdvertisementType> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, UnifiedAdvertisementType);
+  }
+
+  override async create(data: Partial<UnifiedAdvertisementType>, userId?: string | number): Promise<UnifiedAdvertisementType> {
+    // 设置默认状态为启用
+    const typeData = {
+      ...data,
+      status: data.status ?? 1
+    };
+    return super.create(typeData, userId);
+  }
+
+  override async update(id: number, data: Partial<UnifiedAdvertisementType>, userId?: string | number): Promise<UnifiedAdvertisementType | null> {
+    return super.update(id, data, userId);
+  }
+
+  override async delete(id: number, userId?: string | number): Promise<boolean> {
+    // 软删除:设置status=0
+    const result = await this.repository.update(id, { status: 0 });
+    return (result.affected ?? 0) > 0;
+  }
+}

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

@@ -0,0 +1,28 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { UnifiedAdvertisement } from '../entities/unified-advertisement.entity';
+
+export class UnifiedAdvertisementService extends GenericCrudService<UnifiedAdvertisement> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, UnifiedAdvertisement);
+  }
+
+  override async create(data: Partial<UnifiedAdvertisement>, userId?: string | number): Promise<UnifiedAdvertisement> {
+    // 设置默认状态为启用
+    const advertisementData = {
+      ...data,
+      status: data.status ?? 1
+    };
+    return super.create(advertisementData, userId);
+  }
+
+  override async update(id: number, data: Partial<UnifiedAdvertisement>, userId?: string | number): Promise<UnifiedAdvertisement | null> {
+    return super.update(id, data, userId);
+  }
+
+  override async delete(id: number, userId?: string | number): Promise<boolean> {
+    // 软删除:设置status=0
+    const result = await this.repository.update(id, { status: 0 });
+    return (result.affected ?? 0) > 0;
+  }
+}

+ 441 - 0
packages/unified-advertisements-module/tests/integration/unified-advertisements.integration.test.ts

@@ -0,0 +1,441 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntityMt, RoleMt } from '@d8d/core-module-mt/user-module-mt';
+import { FileMt } from '@d8d/core-module-mt/file-module-mt';
+import unifiedAdvertisementAdminRoutes from '../../src/routes/admin/unified-advertisements.admin.routes';
+import unifiedAdvertisementRoutes from '../../src/routes/unified-advertisements.routes';
+import { UnifiedAdvertisement } from '../../src/entities/unified-advertisement.entity';
+import { UnifiedAdvertisementType } from '../../src/entities/unified-advertisement-type.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntityMt, RoleMt, FileMt, UnifiedAdvertisement, UnifiedAdvertisementType])
+
+describe('统一广告模块集成测试', () => {
+  describe('管理员路由(超级管理员专用)', () => {
+    let adminClient: ReturnType<typeof testClient<typeof unifiedAdvertisementAdminRoutes>>;
+    let superAdminToken: string;
+    let regularUserToken: string;
+    let testUser: UserEntityMt;
+    let testAdvertisementType: UnifiedAdvertisementType;
+
+    beforeEach(async () => {
+      // 创建测试客户端
+      adminClient = testClient(unifiedAdvertisementAdminRoutes);
+
+      // 获取数据源
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userRepository = dataSource.getRepository(UserEntityMt);
+      const typeRepository = dataSource.getRepository(UnifiedAdvertisementType);
+
+      // 创建测试用户
+      testUser = userRepository.create({
+        username: `test_user_${Date.now()}`,
+        password: 'test_password',
+        nickname: '测试用户',
+        registrationSource: 'web',
+        tenantId: 1  // UserEntityMt requires tenantId
+      });
+      await userRepository.save(testUser);
+
+      // 创建测试广告类型
+      testAdvertisementType = typeRepository.create({
+        name: '首页轮播',
+        code: 'home_banner',
+        remark: '用于首页轮播图展示',
+        status: 1
+      });
+      await typeRepository.save(testAdvertisementType);
+
+      // 生成超级管理员token (ID=1)
+      superAdminToken = JWTUtil.generateToken({
+        id: 1,
+        username: 'admin',
+        roles: [{ name: 'admin' }]
+      });
+
+      // 生成普通用户token (ID=2)
+      regularUserToken = JWTUtil.generateToken({
+        id: 2,
+        username: 'user',
+        roles: [{ name: 'user' }]
+      });
+    });
+
+    describe('GET /api/v1/admin/unified-advertisements', () => {
+      it('应该允许超级管理员获取广告列表', async () => {
+        const response = await adminClient['/api/v1/admin/unified-advertisements'].$get({
+          query: { page: 1, pageSize: 10 }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${superAdminToken}`
+          }
+        });
+
+        console.debug('管理员广告列表响应状态:', response.status);
+        expect(response.status).toBe(200);
+
+        if (response.status === 200) {
+          const data = await response.json();
+          expect(data).toHaveProperty('code', 200);
+          expect(data).toHaveProperty('data');
+          expect(data.data).toHaveProperty('list');
+          expect(Array.isArray(data.data.list)).toBe(true);
+        }
+      });
+
+      it('应该拒绝普通用户访问管理员接口', async () => {
+        const response = await adminClient['/api/v1/admin/unified-advertisements'].$get({
+          query: { page: 1, pageSize: 10 }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${regularUserToken}`
+          }
+        });
+
+        expect(response.status).toBe(403);
+      });
+
+      it('应该拒绝未认证用户访问', async () => {
+        const response = await adminClient['/api/v1/admin/unified-advertisements'].$get({
+          query: { page: 1, pageSize: 10 }
+        });
+
+        expect(response.status).toBe(401);
+      });
+    });
+
+    describe('POST /api/v1/admin/unified-advertisements', () => {
+      it('应该允许超级管理员创建广告', async () => {
+        const createData = {
+          title: '测试广告',
+          typeId: testAdvertisementType.id,
+          code: `test_ad_${Date.now()}`,
+          url: 'https://example.com',
+          sort: 10,
+          status: 1,
+          actionType: 1
+        };
+
+        const response = await adminClient['/api/v1/admin/unified-advertisements'].$post({
+          json: createData
+        }, {
+          headers: {
+            'Authorization': `Bearer ${superAdminToken}`
+          }
+        });
+
+        console.debug('创建广告响应状态:', response.status);
+        expect(response.status).toBe(201);
+
+        if (response.status === 201) {
+          const data = await response.json();
+          expect(data).toHaveProperty('code', 201);
+          expect(data.data).toHaveProperty('id');
+          expect(data.data.title).toBe(createData.title);
+          expect(data.data.code).toBe(createData.code);
+        }
+      });
+
+      it('应该拒绝普通用户创建广告', async () => {
+        const response = await adminClient['/api/v1/admin/unified-advertisements'].$post({
+          json: {
+            title: '测试广告',
+            typeId: testAdvertisementType.id,
+            code: 'test_ad',
+            url: 'https://example.com'
+          }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${regularUserToken}`
+          }
+        });
+
+        expect(response.status).toBe(403);
+      });
+    });
+
+    describe('PUT /api/v1/admin/unified-advertisements/:id', () => {
+      it('应该允许超级管理员更新广告', async () => {
+        const dataSource = await IntegrationTestDatabase.getDataSource();
+        const advertisementRepository = dataSource.getRepository(UnifiedAdvertisement);
+
+        const testAdvertisement = advertisementRepository.create({
+          title: '原始广告',
+          typeId: testAdvertisementType.id,
+          code: `original_ad_${Date.now()}`,
+          url: 'https://example.com',
+          sort: 5,
+          status: 1,
+          actionType: 1,
+          createdBy: 1
+        });
+        await advertisementRepository.save(testAdvertisement);
+
+        const updateData = {
+          title: '更新后的广告',
+          code: `updated_ad_${Date.now()}`,
+          sort: 15
+        };
+
+        const response = await adminClient['/api/v1/admin/unified-advertisements/:id'].$put({
+          param: { id: testAdvertisement.id },
+          json: updateData
+        }, {
+          headers: {
+            'Authorization': `Bearer ${superAdminToken}`
+          }
+        });
+
+        console.debug('更新广告响应状态:', response.status);
+        expect(response.status).toBe(200);
+
+        if (response.status === 200) {
+          const data = await response.json();
+          expect(data.data.title).toBe(updateData.title);
+          expect(data.data.sort).toBe(updateData.sort);
+        }
+      });
+    });
+
+    describe('DELETE /api/v1/admin/unified-advertisements/:id', () => {
+      it('应该允许超级管理员软删除广告', async () => {
+        const dataSource = await IntegrationTestDatabase.getDataSource();
+        const advertisementRepository = dataSource.getRepository(UnifiedAdvertisement);
+
+        const testAdvertisement = advertisementRepository.create({
+          title: '待删除广告',
+          typeId: testAdvertisementType.id,
+          code: `delete_ad_${Date.now()}`,
+          url: 'https://example.com',
+          sort: 5,
+          status: 1,
+          actionType: 1,
+          createdBy: 1
+        });
+        await advertisementRepository.save(testAdvertisement);
+
+        const response = await adminClient['/api/v1/admin/unified-advertisements/:id'].$delete({
+          param: { id: testAdvertisement.id }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${superAdminToken}`
+          }
+        });
+
+        console.debug('删除广告响应状态:', response.status);
+        expect(response.status).toBe(200);
+
+        // 验证软删除
+        const deletedAdvertisement = await advertisementRepository.findOne({
+          where: { id: testAdvertisement.id }
+        });
+        expect(deletedAdvertisement).toBeDefined();
+        expect(deletedAdvertisement?.status).toBe(0);
+      });
+    });
+  });
+
+  describe('用户展示路由', () => {
+    let userClient: ReturnType<typeof testClient<typeof unifiedAdvertisementRoutes>>;
+    let testToken: string;
+    let testUser: UserEntityMt;
+    let testAdvertisementType: UnifiedAdvertisementType;
+
+    beforeEach(async () => {
+      // 创建测试客户端
+      userClient = testClient(unifiedAdvertisementRoutes);
+
+      // 获取数据源
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userRepository = dataSource.getRepository(UserEntityMt);
+      const typeRepository = dataSource.getRepository(UnifiedAdvertisementType);
+
+      // 创建测试用户
+      testUser = userRepository.create({
+        username: `test_user_${Date.now()}`,
+        password: 'test_password',
+        nickname: '测试用户',
+        registrationSource: 'web',
+        tenantId: 1  // UserEntityMt requires tenantId
+      });
+      await userRepository.save(testUser);
+
+      // 创建测试广告类型
+      testAdvertisementType = typeRepository.create({
+        name: '首页轮播',
+        code: 'home_banner',
+        remark: '用于首页轮播图展示',
+        status: 1
+      });
+      await typeRepository.save(testAdvertisementType);
+
+      // 生成测试用户的token
+      testToken = JWTUtil.generateToken({
+        id: testUser.id,
+        username: testUser.username,
+        roles: [{ name: 'user' }]
+      });
+    });
+
+    describe('GET /api/v1/advertisements', () => {
+      beforeEach(async () => {
+        const dataSource = await IntegrationTestDatabase.getDataSource();
+        const advertisementRepository = dataSource.getRepository(UnifiedAdvertisement);
+
+        // 创建多个测试广告
+        await advertisementRepository.save([
+          {
+            title: '首页轮播1',
+            typeId: testAdvertisementType.id,
+            code: `home_banner_1_${Date.now()}`,
+            url: 'https://example.com/1',
+            sort: 1,
+            status: 1,
+            actionType: 1,
+            createdBy: 1
+          },
+          {
+            title: '首页轮播2',
+            typeId: testAdvertisementType.id,
+            code: `home_banner_2_${Date.now()}`,
+            url: 'https://example.com/2',
+            sort: 2,
+            status: 1,
+            actionType: 1,
+            createdBy: 1
+          },
+          {
+            title: '禁用广告',
+            typeId: testAdvertisementType.id,
+            code: `disabled_ad_${Date.now()}`,
+            url: 'https://example.com/3',
+            sort: 3,
+            status: 0,
+            actionType: 1,
+            createdBy: 1
+          }
+        ]);
+      });
+
+      it('应该返回有效的广告列表(status=1)', async () => {
+        const response = await userClient['/api/v1/advertisements'].$get({
+          query: { page: 1, pageSize: 10 }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+        console.debug('用户广告列表响应状态:', response.status);
+        expect(response.status).toBe(200);
+
+        if (response.status === 200) {
+          const data = await response.json();
+          expect(data).toHaveProperty('code', 200);
+          expect(data.data).toHaveProperty('list');
+          expect(Array.isArray(data.data.list)).toBe(true);
+          // 验证只返回启用状态的广告
+          expect(data.data.list.every((ad: any) => ad.status === 1)).toBe(true);
+        }
+      });
+
+      it('应该支持按code筛选', async () => {
+        const response = await userClient['/api/v1/advertisements'].$get({
+          query: { page: 1, pageSize: 10, code: 'home_banner_1' }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+        expect(response.status).toBe(200);
+      });
+
+      it('应该拒绝未认证用户访问', async () => {
+        const response = await userClient['/api/v1/advertisements'].$get({
+          query: { page: 1, pageSize: 10 }
+        });
+
+        expect(response.status).toBe(401);
+      });
+    });
+
+    describe('GET /api/v1/advertisements/:id', () => {
+      it('应该返回有效的广告详情', async () => {
+        const dataSource = await IntegrationTestDatabase.getDataSource();
+        const advertisementRepository = dataSource.getRepository(UnifiedAdvertisement);
+
+        const testAdvertisement = advertisementRepository.create({
+          title: '测试广告详情',
+          typeId: testAdvertisementType.id,
+          code: `test_ad_detail_${Date.now()}`,
+          url: 'https://example.com',
+          sort: 5,
+          status: 1,
+          actionType: 1,
+          createdBy: 1
+        });
+        await advertisementRepository.save(testAdvertisement);
+
+        const response = await userClient['/api/v1/advertisements/:id'].$get({
+          param: { id: testAdvertisement.id }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+        console.debug('广告详情响应状态:', response.status);
+        expect(response.status).toBe(200);
+
+        if (response.status === 200) {
+          const data = await response.json();
+          expect(data.data.id).toBe(testAdvertisement.id);
+          expect(data.data.title).toBe(testAdvertisement.title);
+        }
+      });
+
+      it('应该返回404对于禁用的广告', async () => {
+        const dataSource = await IntegrationTestDatabase.getDataSource();
+        const advertisementRepository = dataSource.getRepository(UnifiedAdvertisement);
+
+        const disabledAdvertisement = advertisementRepository.create({
+          title: '禁用广告',
+          typeId: testAdvertisementType.id,
+          code: `disabled_ad_${Date.now()}`,
+          url: 'https://example.com',
+          sort: 5,
+          status: 0,
+          actionType: 1,
+          createdBy: 1
+        });
+        await advertisementRepository.save(disabledAdvertisement);
+
+        const response = await userClient['/api/v1/advertisements/:id'].$get({
+          param: { id: disabledAdvertisement.id }
+        }, {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+        expect(response.status).toBe(404);
+      });
+    });
+  });
+
+  describe('API兼容性测试', () => {
+    it('用户展示路由API路径与原模块一致', async () => {
+      // 这个测试确保路由路径与原 advertisements-module-mt 完全一致
+      const userClient = testClient(unifiedAdvertisementRoutes);
+
+      // 验证路由存在(虽然会返回401,但说明路由注册成功)
+      const listResponse = await userClient['/api/v1/advertisements'].$get({
+        query: { page: 1, pageSize: 10 }
+      });
+      expect([200, 401]).toContain(listResponse.status);
+    });
+  });
+});

+ 157 - 0
packages/unified-advertisements-module/tests/unit/unified-advertisement-type.service.test.ts

@@ -0,0 +1,157 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { AppDataSource } from '@d8d/shared-utils';
+import { UnifiedAdvertisementTypeService } from '../../src/services/unified-advertisement-type.service';
+import { UnifiedAdvertisementTestDataFactory } from '../utils/test-data-factory';
+
+describe('UnifiedAdvertisementTypeService', () => {
+  let service: UnifiedAdvertisementTypeService;
+
+  beforeEach(async () => {
+    await AppDataSource.initialize();
+    service = new UnifiedAdvertisementTypeService(AppDataSource);
+  });
+
+  afterEach(async () => {
+    await UnifiedAdvertisementTestDataFactory.cleanupTestData();
+    await AppDataSource.destroy();
+  });
+
+  describe('create', () => {
+    it('should create advertisement type with default status=1', async () => {
+      const adType = await service.create({
+        name: '首页轮播',
+        code: 'home_banner',
+        remark: '首页轮播图广告'
+      }, 1);
+
+      expect(adType.id).toBeDefined();
+      expect(adType.name).toBe('首页轮播');
+      expect(adType.status).toBe(1); // 默认启用
+    });
+
+    it('should create advertisement type with custom status', async () => {
+      const adType = await service.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 0
+      }, 1);
+
+      expect(adType.status).toBe(0);
+    });
+  });
+
+  describe('getList', () => {
+    beforeEach(async () => {
+      await service.create({
+        name: '首页轮播',
+        code: 'home_banner',
+        status: 1
+      }, 1);
+
+      await service.create({
+        name: '分类广告',
+        code: 'category_ad',
+        status: 1
+      }, 1);
+
+      await service.create({
+        name: '禁用类型',
+        code: 'disabled_type',
+        status: 0
+      }, 1);
+    });
+
+    it('should return paginated list', async () => {
+      const [list, total] = await service.getList(1, 10);
+
+      expect(total).toBe(3);
+      expect(list).toHaveLength(3);
+    });
+
+    it('should filter by keyword', async () => {
+      const [list, total] = await service.getList(1, 10, '首页');
+
+      expect(total).toBe(1);
+      expect(list[0].name).toBe('首页轮播');
+    });
+
+    it('should filter by status', async () => {
+      const [list, total] = await service.getList(
+        1,
+        10,
+        undefined,
+        undefined,
+        { status: 1 }
+      );
+
+      expect(total).toBe(2);
+      expect(list.every(type => type.status === 1)).toBe(true);
+    });
+  });
+
+  describe('getById', () => {
+    it('should return advertisement type', async () => {
+      const created = await service.create({
+        name: '首页轮播',
+        code: 'home_banner',
+        status: 1
+      }, 1);
+
+      const found = await service.getById(created.id);
+
+      expect(found).toBeDefined();
+      expect(found?.id).toBe(created.id);
+      expect(found?.name).toBe('首页轮播');
+    });
+
+    it('should return null for non-existent id', async () => {
+      const found = await service.getById(99999);
+      expect(found).toBeNull();
+    });
+  });
+
+  describe('update', () => {
+    it('should update advertisement type', async () => {
+      const created = await service.create({
+        name: '原始名称',
+        code: 'original_code',
+        status: 1
+      }, 1);
+
+      const updated = await service.update(created.id, {
+        name: '更新名称'
+      }, 1);
+
+      expect(updated).toBeDefined();
+      expect(updated?.name).toBe('更新名称');
+    });
+
+    it('should return null for non-existent id', async () => {
+      const result = await service.update(99999, { name: '更新名称' }, 1);
+      expect(result).toBeNull();
+    });
+  });
+
+  describe('delete', () => {
+    it('should soft delete by setting status=0', async () => {
+      const created = await service.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      const deleted = await service.delete(created.id, 1);
+      expect(deleted).toBe(true);
+
+      // 验证软删除
+      const found = await service.getById(created.id);
+      expect(found).toBeDefined();
+      expect(found?.status).toBe(0);
+    });
+
+    it('should return false for non-existent id', async () => {
+      const result = await service.delete(99999, 1);
+      expect(result).toBe(false);
+    });
+  });
+});

+ 224 - 0
packages/unified-advertisements-module/tests/unit/unified-advertisement.service.test.ts

@@ -0,0 +1,224 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { AppDataSource } from '@d8d/shared-utils';
+import { UnifiedAdvertisementService } from '../../src/services/unified-advertisement.service';
+import { UnifiedAdvertisementTypeService } from '../../src/services/unified-advertisement-type.service';
+import { UnifiedAdvertisementTestDataFactory } from '../utils/test-data-factory';
+
+describe('UnifiedAdvertisementService', () => {
+  let service: UnifiedAdvertisementService;
+  let typeService: UnifiedAdvertisementTypeService;
+
+  beforeEach(async () => {
+    await AppDataSource.initialize();
+    service = new UnifiedAdvertisementService(AppDataSource);
+    typeService = new UnifiedAdvertisementTypeService(AppDataSource);
+  });
+
+  afterEach(async () => {
+    await UnifiedAdvertisementTestDataFactory.cleanupTestData();
+    await AppDataSource.destroy();
+  });
+
+  describe('create', () => {
+    it('should create advertisement with default status=1', async () => {
+      const adType = await typeService.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      const advertisement = await service.create({
+        title: '测试广告',
+        typeId: adType.id,
+        code: 'test_ad',
+        url: 'https://example.com',
+        sort: 10
+      }, 1);
+
+      expect(advertisement.id).toBeDefined();
+      expect(advertisement.title).toBe('测试广告');
+      expect(advertisement.status).toBe(1); // 默认启用
+    });
+
+    it('should create advertisement with custom status', async () => {
+      const adType = await typeService.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      const advertisement = await service.create({
+        title: '测试广告',
+        typeId: adType.id,
+        code: 'test_ad',
+        status: 0
+      }, 1);
+
+      expect(advertisement.status).toBe(0);
+    });
+  });
+
+  describe('getList', () => {
+    beforeEach(async () => {
+      const adType = await typeService.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      // 创建多个测试广告
+      await service.create({
+        title: '首页轮播1',
+        typeId: adType.id,
+        code: 'home_banner_1',
+        status: 1,
+        sort: 1
+      }, 1);
+
+      await service.create({
+        title: '首页轮播2',
+        typeId: adType.id,
+        code: 'home_banner_2',
+        status: 1,
+        sort: 2
+      }, 1);
+
+      await service.create({
+        title: '禁用广告',
+        typeId: adType.id,
+        code: 'disabled_ad',
+        status: 0,
+        sort: 3
+      }, 1);
+    });
+
+    it('should return paginated list', async () => {
+      const [list, total] = await service.getList(1, 10);
+
+      expect(total).toBe(3);
+      expect(list).toHaveLength(3);
+    });
+
+    it('should filter by keyword', async () => {
+      const [list, total] = await service.getList(1, 10, '首页');
+
+      expect(total).toBe(2);
+      expect(list.every(ad => ad.title?.includes('首页'))).toBe(true);
+    });
+
+    it('should filter by status', async () => {
+      const [list, total] = await service.getList(
+        1,
+        10,
+        undefined,
+        undefined,
+        { status: 1 }
+      );
+
+      expect(total).toBe(2);
+      expect(list.every(ad => ad.status === 1)).toBe(true);
+    });
+
+    it('should support sorting', async () => {
+      const [list] = await service.getList(
+        1,
+        10,
+        undefined,
+        undefined,
+        undefined,
+        [],
+        { sort: 'ASC' }
+      );
+
+      expect(list[0].sort).toBeLessThanOrEqual(list[1].sort);
+    });
+  });
+
+  describe('getById', () => {
+    it('should return advertisement with relations', async () => {
+      const adType = await typeService.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      const created = await service.create({
+        title: '测试广告',
+        typeId: adType.id,
+        code: 'test_ad',
+        status: 1
+      }, 1);
+
+      const found = await service.getById(created.id, ['advertisementType']);
+
+      expect(found).toBeDefined();
+      expect(found?.id).toBe(created.id);
+      expect(found?.advertisementType).toBeDefined();
+      expect(found?.advertisementType?.id).toBe(adType.id);
+    });
+
+    it('should return null for non-existent id', async () => {
+      const found = await service.getById(99999);
+      expect(found).toBeNull();
+    });
+  });
+
+  describe('update', () => {
+    it('should update advertisement', async () => {
+      const adType = await typeService.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      const created = await service.create({
+        title: '原始标题',
+        typeId: adType.id,
+        code: 'test_ad',
+        status: 1
+      }, 1);
+
+      const updated = await service.update(created.id, {
+        title: '更新标题'
+      }, 1);
+
+      expect(updated).toBeDefined();
+      expect(updated?.title).toBe('更新标题');
+    });
+
+    it('should return null for non-existent id', async () => {
+      const result = await service.update(99999, { title: '更新标题' }, 1);
+      expect(result).toBeNull();
+    });
+  });
+
+  describe('delete', () => {
+    it('should soft delete by setting status=0', async () => {
+      const adType = await typeService.create({
+        name: '测试类型',
+        code: 'test_type',
+        status: 1
+      }, 1);
+
+      const created = await service.create({
+        title: '测试广告',
+        typeId: adType.id,
+        code: 'test_ad',
+        status: 1
+      }, 1);
+
+      const deleted = await service.delete(created.id, 1);
+      expect(deleted).toBe(true);
+
+      // 验证软删除
+      const found = await service.getById(created.id);
+      expect(found).toBeDefined();
+      expect(found?.status).toBe(0);
+    });
+
+    it('should return false for non-existent id', async () => {
+      const result = await service.delete(99999, 1);
+      expect(result).toBe(false);
+    });
+  });
+});

+ 119 - 0
packages/unified-advertisements-module/tests/utils/test-data-factory.ts

@@ -0,0 +1,119 @@
+import { IntegrationTestDatabase } from '@d8d/shared-test-util';
+import { UnifiedAdvertisement } from '../../src/entities/unified-advertisement.entity';
+import { UnifiedAdvertisementType } from '../../src/entities/unified-advertisement-type.entity';
+import type { EntityTarget, ObjectLiteral } from 'typeorm';
+
+/**
+ * 统一广告模块测试数据工厂
+ */
+export class UnifiedAdvertisementTestDataFactory {
+  /**
+   * 创建测试广告类型
+   */
+  static async createTestAdvertisementType(overrides: Partial<UnifiedAdvertisementType> = {}): Promise<UnifiedAdvertisementType> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const repository = dataSource.getRepository(UnifiedAdvertisementType);
+
+    const timestamp = Date.now();
+    const defaultData: Partial<UnifiedAdvertisementType> = {
+      name: `测试广告类型_${timestamp}`,
+      code: `test_type_${timestamp}`,
+      remark: '测试用广告类型',
+      status: 1,
+      createdAt: new Date(),
+      updatedAt: new Date()
+    };
+
+    const typeData = { ...defaultData, ...overrides };
+    const entity = repository.create(typeData);
+    return await repository.save(entity);
+  }
+
+  /**
+   * 创建测试广告
+   */
+  static async createTestAdvertisement(overrides: Partial<UnifiedAdvertisement> = {}): Promise<UnifiedAdvertisement> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const repository = dataSource.getRepository(UnifiedAdvertisement);
+
+    const timestamp = Date.now();
+
+    // 如果没有提供typeId,先创建一个广告类型
+    let typeId = overrides.typeId;
+    if (!typeId) {
+      const adType = await this.createTestAdvertisementType();
+      typeId = adType.id;
+    }
+
+    const defaultData: Partial<UnifiedAdvertisement> = {
+      title: `测试广告_${timestamp}`,
+      typeId,
+      code: `test_ad_${timestamp}`,
+      url: 'https://example.com',
+      imageFileId: null,
+      sort: 0,
+      status: 1,
+      actionType: 1,
+      createdAt: new Date(),
+      updatedAt: new Date(),
+      createdBy: 1,
+      updatedBy: 1
+    };
+
+    const adData = { ...defaultData, ...overrides };
+    const entity = repository.create(adData);
+    return await repository.save(entity);
+  }
+
+  /**
+   * 创建完整的测试数据集合
+   */
+  static async createTestDataSet(): Promise<{
+    advertisementType: UnifiedAdvertisementType;
+    advertisement: UnifiedAdvertisement;
+  }> {
+    const advertisementType = await this.createTestAdvertisementType();
+    const advertisement = await this.createTestAdvertisement({ typeId: advertisementType.id });
+
+    return {
+      advertisementType,
+      advertisement
+    };
+  }
+
+  /**
+   * 通用实体创建方法
+   */
+  static async createEntity<T extends ObjectLiteral>(
+    entity: EntityTarget<T>,
+    data: Partial<T>
+  ): Promise<T> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const repository = dataSource.getRepository(entity);
+    const entityInstance = repository.create(data as T);
+    return await repository.save(entityInstance);
+  }
+
+  /**
+   * 批量创建实体
+   */
+  static async createEntities<T extends ObjectLiteral>(
+    entity: EntityTarget<T>,
+    dataArray: Partial<T>[]
+  ): Promise<T[]> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const repository = dataSource.getRepository(entity);
+    const entities = dataArray.map(data => repository.create(data as T));
+    return await repository.save(entities);
+  }
+
+  /**
+   * 清理测试数据
+   */
+  static async cleanupTestData(): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    await dataSource.getRepository(UnifiedAdvertisement).clear();
+    await dataSource.getRepository(UnifiedAdvertisementType).clear();
+  }
+}

+ 11 - 0
packages/unified-advertisements-module/tsconfig.json

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

+ 21 - 0
packages/unified-advertisements-module/vitest.config.ts

@@ -0,0 +1,21 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'tests/**',
+        '**/*.d.ts',
+        '**/*.config.*',
+        '**/dist/**'
+      ]
+    },
+    // 关闭并行测试以避免数据库连接冲突
+    fileParallelism: false
+  }
+});

+ 52 - 0
pnpm-lock.yaml

@@ -4805,6 +4805,58 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.94.1)(stylus@0.64.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/unified-advertisements-module:
+    dependencies:
+      '@d8d/core-module-mt':
+        specifier: workspace:*
+        version: link:../core-module-mt
+      '@d8d/shared-crud':
+        specifier: workspace:*
+        version: link:../shared-crud
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-utils':
+        specifier: workspace:*
+        version: link:../shared-utils
+      '@d8d/tenant-module-mt':
+        specifier: workspace:*
+        version: link:../tenant-module-mt
+      '@hono/zod-openapi':
+        specifier: ^1.0.2
+        version: 1.0.2(hono@4.8.5)(zod@4.1.12)
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      typeorm:
+        specifier: ^0.3.20
+        version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
+      zod:
+        specifier: ^4.1.12
+        version: 4.1.12
+    devDependencies:
+      '@d8d/shared-test-util':
+        specifier: workspace:*
+        version: link:../shared-test-util
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.1
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.39.1(jiti@2.6.1)
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.94.1)(stylus@0.64.0)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/user-management-ui:
     dependencies:
       '@d8d/shared-types':