Browse Source

✨ feat(allin-channel-module): 移植渠道管理模块

- 创建 @d8d/allin-channel-module 包
- 完成实体转换:Channel 实体从下划线命名转换为驼峰命名
- 完成服务层转换:从 NestJS 自定义 Service 转换为 GenericCrudService 继承
- 实现自定义业务逻辑(覆盖 GenericCrudService 方法)
- 完成路由层转换:从 NestJS 控制器转换为 Hono 路由
- 实现自定义路由处理(适配业务逻辑)
- 完成验证系统转换:从 class-validator DTO 转换为 Zod Schema
- 配置 package.json:使用 @d8d/allin-channel-module 包名,workspace 依赖
- 编写 API 集成测试:覆盖所有路由端点,验证 CRUD 操作
- 更新 pnpm-workspace.yaml 添加 allin-packages 目录

🤖 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 5 days ago
parent
commit
1e722960dc

+ 77 - 0
allin-packages/channel-module/package.json

@@ -0,0 +1,77 @@
+{
+  "name": "@d8d/allin-channel-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-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "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"
+  },
+  "peerDependencies": {
+    "hono": "^4.8.5"
+  },
+  "keywords": [
+    "channel",
+    "management",
+    "crud",
+    "api"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 85 - 0
allin-packages/channel-module/src/entities/channel.entity.ts

@@ -0,0 +1,85 @@
+import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
+
+@Entity('channel_info')
+export class Channel {
+  @PrimaryGeneratedColumn({
+    name: 'channel_id',
+    type: 'int',
+    unsigned: true,
+    comment: '渠道ID'
+  })
+  channelId!: number;
+
+  @Column({
+    name: 'channel_name',
+    type: 'varchar',
+    length: 100,
+    nullable: false,
+    comment: '渠道名称'
+  })
+  @Index('idx_channel_name', { unique: true })
+  channelName!: string;
+
+  @Column({
+    name: 'channel_type',
+    type: 'varchar',
+    length: 50,
+    nullable: false,
+    default: '',
+    comment: '渠道类型'
+  })
+  channelType!: string;
+
+  @Column({
+    name: 'contact_person',
+    type: 'varchar',
+    length: 50,
+    nullable: false,
+    default: '',
+    comment: '联系人'
+  })
+  contactPerson!: string;
+
+  @Column({
+    name: 'contact_phone',
+    type: 'varchar',
+    length: 20,
+    nullable: false,
+    default: '',
+    comment: '联系电话'
+  })
+  contactPhone!: string;
+
+  @Column({
+    name: 'description',
+    type: 'text',
+    nullable: true,
+    comment: '描述'
+  })
+  description?: string;
+
+  @Column({
+    name: 'status',
+    type: 'int',
+    default: 1,
+    comment: '状态:1-正常,0-禁用'
+  })
+  status!: number;
+
+  @Column({
+    name: 'create_time',
+    type: 'timestamp',
+    default: () => 'CURRENT_TIMESTAMP',
+    comment: '创建时间'
+  })
+  createTime!: Date;
+
+  @Column({
+    name: 'update_time',
+    type: 'timestamp',
+    default: () => 'CURRENT_TIMESTAMP',
+    onUpdate: 'CURRENT_TIMESTAMP',
+    comment: '更新时间'
+  })
+  updateTime!: Date;
+}

+ 1 - 0
allin-packages/channel-module/src/entities/index.ts

@@ -0,0 +1 @@
+export * from './channel.entity';

+ 4 - 0
allin-packages/channel-module/src/index.ts

@@ -0,0 +1,4 @@
+export * from './entities';
+export * from './services';
+export * from './schemas';
+export * from './routes';

+ 19 - 0
allin-packages/channel-module/src/routes/channel-crud.routes.ts

@@ -0,0 +1,19 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { Channel } from '../entities/channel.entity';
+import { ChannelSchema, CreateChannelSchema, UpdateChannelSchema } from '../schemas/channel.schema';
+
+export const channelCrudRoutes = createCrudRoutes({
+  entity: Channel,
+  createSchema: CreateChannelSchema,
+  updateSchema: UpdateChannelSchema,
+  getSchema: ChannelSchema,
+  listSchema: ChannelSchema,
+  searchFields: ['channelName'],
+  middleware: [authMiddleware],
+  readOnly: true, // 设置为只读,因为创建、更新、删除操作通过自定义路由处理
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});

+ 389 - 0
allin-packages/channel-module/src/routes/channel-custom.routes.ts

@@ -0,0 +1,389 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
+import { authMiddleware } from '@d8d/auth-module';
+import { AuthContext } from '@d8d/shared-types';
+import { ChannelService } from '../services/channel.service';
+import {
+  CreateChannelSchema,
+  UpdateChannelSchema,
+  DeleteChannelSchema,
+  ChannelSchema,
+  PaginationQuerySchema,
+  SearchChannelQuerySchema
+} from '../schemas/channel.schema';
+
+// 创建渠道路由 - 返回布尔值
+const createChannelRoute = createRoute({
+  method: 'post',
+  path: '/createChannel',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreateChannelSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '渠道创建成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean().openapi({ description: '是否成功' }),
+            message: z.string().optional().openapi({ description: '错误信息' })
+          })
+        }
+      }
+    },
+    400: {
+      description: '参数错误或渠道名称已存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '创建渠道失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 删除渠道路由 - 返回布尔值
+const deleteChannelRoute = createRoute({
+  method: 'post',
+  path: '/deleteChannel',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: DeleteChannelSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '渠道删除成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean().openapi({ description: '是否成功' })
+          })
+        }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '删除渠道失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 更新渠道路由 - 返回布尔值
+const updateChannelRoute = createRoute({
+  method: 'post',
+  path: '/updateChannel',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: UpdateChannelSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '渠道更新成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean().openapi({ description: '是否成功' }),
+            message: z.string().optional().openapi({ description: '错误信息' })
+          })
+        }
+      }
+    },
+    400: {
+      description: '参数错误或渠道名称已存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '渠道不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '更新渠道失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 获取所有渠道路由 - 分页查询
+const getAllChannelsRoute = createRoute({
+  method: 'get',
+  path: '/getAllChannels',
+  middleware: [authMiddleware],
+  request: {
+    query: PaginationQuerySchema
+  },
+  responses: {
+    200: {
+      description: '获取渠道列表成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            data: z.array(ChannelSchema).openapi({ description: '渠道列表' }),
+            total: z.number().int().openapi({ description: '总记录数' })
+          })
+        }
+      }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '获取渠道列表失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 搜索渠道路由 - 按名称模糊搜索
+const searchChannelsRoute = createRoute({
+  method: 'get',
+  path: '/searchChannels',
+  middleware: [authMiddleware],
+  request: {
+    query: SearchChannelQuerySchema
+  },
+  responses: {
+    200: {
+      description: '搜索渠道成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            data: z.array(ChannelSchema).openapi({ description: '渠道列表' }),
+            total: z.number().int().openapi({ description: '总记录数' })
+          })
+        }
+      }
+    },
+    400: {
+      description: '搜索关键词不能为空',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '搜索渠道失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 获取单个渠道路由
+const getChannelRoute = createRoute({
+  method: 'get',
+  path: '/getChannel/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number().int().positive().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '渠道ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '获取渠道详情成功',
+      content: {
+        'application/json': { schema: ChannelSchema.nullable() }
+      }
+    },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '获取渠道详情失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>()
+  // 创建渠道
+  .openapi(createChannelRoute, async (c) => {
+    try {
+      const data = c.req.valid('json');
+      const channelService = new ChannelService(AppDataSource);
+
+      const result = await channelService.create(data);
+
+      return c.json({ success: true }, 200);
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        return c.json({
+          code: 400,
+          message: '参数错误',
+          errors: error.issues
+        }, 400);
+      }
+
+      // 处理渠道名称重复错误
+      if (error instanceof Error && error.message.includes('渠道名称已存在')) {
+        return c.json({
+          code: 400,
+          message: '渠道名称已存在'
+        }, 400);
+      }
+
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '创建渠道失败'
+      }, 500);
+    }
+  })
+  // 删除渠道
+  .openapi(deleteChannelRoute, async (c) => {
+    try {
+      const { channelId } = c.req.valid('json');
+      const channelService = new ChannelService(AppDataSource);
+
+      const success = await channelService.delete(channelId);
+
+      return c.json({ success }, 200);
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        return c.json({
+          code: 400,
+          message: '参数错误',
+          errors: error.issues
+        }, 400);
+      }
+
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '删除渠道失败'
+      }, 500);
+    }
+  })
+  // 更新渠道
+  .openapi(updateChannelRoute, async (c) => {
+    try {
+      const data = c.req.valid('json');
+      const channelService = new ChannelService(AppDataSource);
+
+      const result = await channelService.update(data.channelId, data);
+
+      if (!result) {
+        return c.json({ success: false, message: '渠道不存在' }, 404);
+      }
+
+      return c.json({ success: true }, 200);
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        return c.json({
+          code: 400,
+          message: '参数错误',
+          errors: error.issues
+        }, 400);
+      }
+
+      // 处理渠道名称重复错误
+      if (error instanceof Error && error.message.includes('渠道名称已存在')) {
+        return c.json({
+          code: 400,
+          message: '渠道名称已存在'
+        }, 400);
+      }
+
+      // 处理渠道不存在错误
+      if (error instanceof Error && error.message.includes('渠道不存在')) {
+        return c.json({
+          code: 404,
+          message: '渠道不存在'
+        }, 404);
+      }
+
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '更新渠道失败'
+      }, 500);
+    }
+  })
+  // 获取所有渠道
+  .openapi(getAllChannelsRoute, async (c) => {
+    try {
+      const { skip, take } = c.req.valid('query');
+      const channelService = new ChannelService(AppDataSource);
+
+      const result = await channelService.findAll(skip, take);
+
+      return c.json(result, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取渠道列表失败'
+      }, 500);
+    }
+  })
+  // 搜索渠道
+  .openapi(searchChannelsRoute, async (c) => {
+    try {
+      const { name, skip, take } = c.req.valid('query');
+      const channelService = new ChannelService(AppDataSource);
+
+      const result = await channelService.searchByName(name, skip, take);
+
+      return c.json(result, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '搜索渠道失败'
+      }, 500);
+    }
+  })
+  // 获取单个渠道
+  .openapi(getChannelRoute, async (c) => {
+    try {
+      const { id } = c.req.valid('param');
+      const channelService = new ChannelService(AppDataSource);
+
+      const result = await channelService.findOne(id);
+
+      if (!result) {
+        return c.json(null, 200);
+      }
+
+      const validatedResult = await parseWithAwait(ChannelSchema, result);
+      return c.json(validatedResult, 200);
+    } catch (error) {
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取渠道详情失败'
+      }, 500);
+    }
+  });
+
+export default app;

+ 12 - 0
allin-packages/channel-module/src/routes/channel.routes.ts

@@ -0,0 +1,12 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { AuthContext } from '@d8d/shared-types';
+import channelCustomRoutes from './channel-custom.routes';
+import { channelCrudRoutes } from './channel-crud.routes';
+
+// 创建路由实例 - 聚合自定义路由和CRUD路由
+const channelRoutes = new OpenAPIHono<AuthContext>()
+  .route('/', channelCustomRoutes)
+  .route('/', channelCrudRoutes);
+
+export { channelRoutes };
+export default channelRoutes;

+ 3 - 0
allin-packages/channel-module/src/routes/index.ts

@@ -0,0 +1,3 @@
+export * from './channel.routes';
+export * from './channel-custom.routes';
+export * from './channel-crud.routes';

+ 129 - 0
allin-packages/channel-module/src/schemas/channel.schema.ts

@@ -0,0 +1,129 @@
+import { z } from '@hono/zod-openapi';
+
+// 渠道实体Schema
+export const ChannelSchema = z.object({
+  channelId: z.number().int().positive().openapi({
+    description: '渠道ID',
+    example: 1
+  }),
+  channelName: z.string().max(100).openapi({
+    description: '渠道名称',
+    example: '微信小程序'
+  }),
+  channelType: z.string().max(50).openapi({
+    description: '渠道类型',
+    example: '小程序'
+  }),
+  contactPerson: z.string().max(50).openapi({
+    description: '联系人',
+    example: '张三'
+  }),
+  contactPhone: z.string().max(20).openapi({
+    description: '联系电话',
+    example: '13800138000'
+  }),
+  description: z.string().nullable().optional().openapi({
+    description: '描述',
+    example: '微信小程序渠道'
+  }),
+  status: z.number().int().min(0).max(1).default(1).openapi({
+    description: '状态:1-正常,0-禁用',
+    example: 1
+  }),
+  createTime: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updateTime: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  })
+});
+
+// 创建渠道DTO
+export const CreateChannelSchema = z.object({
+  channelName: z.string().min(1).max(100).openapi({
+    description: '渠道名称',
+    example: '微信小程序'
+  }),
+  channelType: z.string().max(50).optional().openapi({
+    description: '渠道类型',
+    example: '小程序'
+  }),
+  contactPerson: z.string().max(50).optional().openapi({
+    description: '联系人',
+    example: '张三'
+  }),
+  contactPhone: z.string().max(20).optional().openapi({
+    description: '联系电话',
+    example: '13800138000'
+  }),
+  description: z.string().optional().openapi({
+    description: '描述',
+    example: '微信小程序渠道'
+  })
+});
+
+// 更新渠道DTO
+export const UpdateChannelSchema = z.object({
+  channelId: z.number().int().positive().openapi({
+    description: '渠道ID',
+    example: 1
+  }),
+  channelName: z.string().min(1).max(100).optional().openapi({
+    description: '渠道名称',
+    example: '微信小程序'
+  }),
+  channelType: z.string().max(50).optional().openapi({
+    description: '渠道类型',
+    example: '小程序'
+  }),
+  contactPerson: z.string().max(50).optional().openapi({
+    description: '联系人',
+    example: '张三'
+  }),
+  contactPhone: z.string().max(20).optional().openapi({
+    description: '联系电话',
+    example: '13800138000'
+  }),
+  description: z.string().optional().openapi({
+    description: '描述',
+    example: '微信小程序渠道'
+  })
+});
+
+// 删除渠道DTO
+export const DeleteChannelSchema = z.object({
+  channelId: z.number().int().positive().openapi({
+    description: '渠道ID',
+    example: 1
+  })
+});
+
+// 分页查询参数Schema
+export const PaginationQuerySchema = z.object({
+  skip: z.coerce.number().int().min(0).default(0).optional().openapi({
+    description: '跳过记录数',
+    example: 0
+  }),
+  take: z.coerce.number().int().min(1).max(100).default(10).optional().openapi({
+    description: '获取记录数',
+    example: 10
+  })
+});
+
+// 搜索渠道参数Schema
+export const SearchChannelQuerySchema = PaginationQuerySchema.extend({
+  name: z.string().min(1).openapi({
+    description: '搜索关键词',
+    example: '微信'
+  })
+});
+
+// 类型定义
+export type Channel = z.infer<typeof ChannelSchema>;
+export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
+export type UpdateChannelDto = z.infer<typeof UpdateChannelSchema>;
+export type DeleteChannelDto = z.infer<typeof DeleteChannelSchema>;
+export type PaginationQuery = z.infer<typeof PaginationQuerySchema>;
+export type SearchChannelQuery = z.infer<typeof SearchChannelQuerySchema>;

+ 1 - 0
allin-packages/channel-module/src/schemas/index.ts

@@ -0,0 +1 @@
+export * from './channel.schema';

+ 110 - 0
allin-packages/channel-module/src/services/channel.service.ts

@@ -0,0 +1,110 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource, Repository, Like, Not } from 'typeorm';
+import { Channel } from '../entities/channel.entity';
+
+export class ChannelService extends GenericCrudService<Channel> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Channel);
+  }
+
+  /**
+   * 创建渠道 - 覆盖父类方法,添加名称唯一性检查
+   */
+  async create(data: Partial<Channel>, userId?: string | number): Promise<Channel> {
+    // 检查渠道名称是否已存在
+    if (data.channelName) {
+      const existingChannel = await this.repository.findOne({
+        where: { channelName: data.channelName }
+      });
+      if (existingChannel) {
+        throw new Error('渠道名称已存在');
+      }
+    }
+
+    // 设置默认值
+    const channelData = {
+      contactPerson: '',
+      contactPhone: '',
+      channelType: '',
+      description: '',
+      ...data,
+      status: 1,
+      createTime: new Date(),
+      updateTime: new Date()
+    };
+
+    return super.create(channelData, userId);
+  }
+
+  /**
+   * 更新渠道 - 覆盖父类方法,添加存在性和名称重复检查
+   */
+  async update(id: number, data: Partial<Channel>, userId?: string | number): Promise<Channel | null> {
+    // 检查渠道是否存在
+    const channel = await this.repository.findOne({ where: { channelId: id } });
+    if (!channel) {
+      throw new Error('渠道不存在');
+    }
+
+    // 检查渠道名称是否与其他渠道重复
+    if (data.channelName && data.channelName !== channel.channelName) {
+      const existingChannel = await this.repository.findOne({
+        where: { channelName: data.channelName, channelId: Not(id) }
+      });
+      if (existingChannel) {
+        throw new Error('渠道名称已存在');
+      }
+    }
+
+    // 设置更新时间
+    const updateData = {
+      ...data,
+      updateTime: new Date()
+    };
+
+    return super.update(id, updateData, userId);
+  }
+
+  /**
+   * 删除渠道 - 覆盖父类方法,改为软删除(设置status为0)
+   */
+  async delete(id: number, userId?: string | number): Promise<boolean> {
+    // 改为软删除:设置status为0
+    const result = await this.repository.update(id, { status: 0 });
+    return result.affected === 1;
+  }
+
+  /**
+   * 获取所有渠道(分页) - 自定义方法,返回源服务的格式
+   */
+  async findAll(skip?: number, take?: number): Promise<{ data: Channel[], total: number }> {
+    const [data, total] = await this.repository.findAndCount({
+      skip: skip ?? 0,
+      take: take ?? 10,
+      order: { channelId: 'DESC' }
+    });
+    return { data, total };
+  }
+
+  /**
+   * 按名称搜索渠道 - 自定义方法
+   */
+  async searchByName(name: string, skip?: number, take?: number): Promise<{ data: Channel[], total: number }> {
+    const [data, total] = await this.repository.findAndCount({
+      where: {
+        channelName: Like(`%${name}%`)
+      },
+      skip: skip ?? 0,
+      take: take ?? 10,
+      order: { channelId: 'DESC' }
+    });
+    return { data, total };
+  }
+
+  /**
+   * 获取单个渠道 - 自定义方法
+   */
+  async findOne(channelId: number): Promise<Channel | null> {
+    return this.repository.findOne({ where: { channelId } });
+  }
+}

+ 1 - 0
allin-packages/channel-module/src/services/index.ts

@@ -0,0 +1 @@
+export * from './channel.service';

+ 378 - 0
allin-packages/channel-module/tests/integration/channel.integration.test.ts

@@ -0,0 +1,378 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntity, Role } from '@d8d/user-module';
+import channelRoutes from '../../src/routes/channel.routes';
+import { Channel } from '../../src/entities/channel.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntity, Role, Channel])
+
+describe('渠道管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof channelRoutes>>;
+  let testToken: string;
+  let testUser: UserEntity;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(channelRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntity);
+    testUser = userRepository.create({
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testUser);
+
+    // 生成测试用户的token
+    testToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{name:'user'}]
+    });
+  });
+
+  describe('POST /channel/createChannel', () => {
+    it('应该成功创建渠道', async () => {
+      const createData = {
+        channelName: '微信小程序渠道',
+        channelType: '小程序',
+        contactPerson: '张三',
+        contactPhone: '13800138000',
+        description: '微信小程序渠道描述'
+      };
+
+      const response = await client.createChannel.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('创建渠道响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.success).toBe(true);
+      }
+    });
+
+    it('应该验证渠道名称重复', async () => {
+      // 先创建一个渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+      const existingChannel = channelRepository.create({
+        channelName: '微信小程序渠道',
+        channelType: '小程序',
+        contactPerson: '张三',
+        contactPhone: '13800138000',
+        status: 1
+      });
+      await channelRepository.save(existingChannel);
+
+      // 尝试创建同名渠道
+      const createData = {
+        channelName: '微信小程序渠道', // 重复的名称
+        channelType: '小程序',
+        contactPerson: '李四',
+        contactPhone: '13900139000'
+      };
+
+      const response = await client.createChannel.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+      if (response.status === 400) {
+        const data = await response.json();
+        expect(data.message).toContain('渠道名称已存在');
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const createData = {
+        channelName: '测试渠道',
+        channelType: '测试'
+      };
+
+      const response = await client.createChannel.$post({
+        json: createData
+      });
+
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST /channel/deleteChannel', () => {
+    it('应该成功删除渠道', async () => {
+      // 先创建一个渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+      const testChannel = channelRepository.create({
+        channelName: `待删除渠道_${Date.now()}`,
+        channelType: '测试',
+        contactPerson: '张三',
+        contactPhone: '13800138000',
+        status: 1
+      });
+      await channelRepository.save(testChannel);
+
+      const deleteData = {
+        channelId: testChannel.channelId
+      };
+
+      const response = await client.deleteChannel.$post({
+        json: deleteData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('删除渠道响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.success).toBe(true);
+      }
+
+      // 验证渠道状态变为0(软删除)
+      const deletedChannel = await channelRepository.findOne({
+        where: { channelId: testChannel.channelId }
+      });
+      expect(deletedChannel?.status).toBe(0);
+    });
+  });
+
+  describe('POST /channel/updateChannel', () => {
+    it('应该成功更新渠道', async () => {
+      // 先创建一个渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+      const testChannel = channelRepository.create({
+        channelName: `原始渠道_${Date.now()}`,
+        channelType: '原始类型',
+        contactPerson: '张三',
+        contactPhone: '13800138000',
+        status: 1
+      });
+      await channelRepository.save(testChannel);
+
+      const updateData = {
+        channelId: testChannel.channelId,
+        channelName: '更新后的渠道名称',
+        channelType: '更新类型',
+        contactPerson: '李四',
+        contactPhone: '13900139000'
+      };
+
+      const response = await client.updateChannel.$post({
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('更新渠道响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.success).toBe(true);
+      }
+
+      // 验证渠道已更新
+      const updatedChannel = await channelRepository.findOne({
+        where: { channelId: testChannel.channelId }
+      });
+      expect(updatedChannel?.channelName).toBe(updateData.channelName);
+      expect(updatedChannel?.channelType).toBe(updateData.channelType);
+    });
+
+    it('应该验证更新时的渠道名称重复', async () => {
+      // 创建两个渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+
+      const channel1 = channelRepository.create({
+        channelName: '渠道A',
+        channelType: '类型A',
+        status: 1
+      });
+      await channelRepository.save(channel1);
+
+      const channel2 = channelRepository.create({
+        channelName: '渠道B',
+        channelType: '类型B',
+        status: 1
+      });
+      await channelRepository.save(channel2);
+
+      // 尝试将渠道B的名称改为渠道A的名称
+      const updateData = {
+        channelId: channel2.channelId,
+        channelName: '渠道A' // 重复的名称
+      };
+
+      const response = await client.updateChannel.$post({
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+      if (response.status === 400) {
+        const data = await response.json();
+        expect(data.message).toContain('渠道名称已存在');
+      }
+    });
+  });
+
+  describe('GET /channel/getAllChannels', () => {
+    it('应该返回渠道列表', async () => {
+      // 创建一些测试渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+
+      for (let i = 0; i < 3; i++) {
+        const channel = channelRepository.create({
+          channelName: `测试渠道${i}`,
+          channelType: '测试',
+          contactPerson: `联系人${i}`,
+          contactPhone: `1380013800${i}`,
+          status: 1
+        });
+        await channelRepository.save(channel);
+      }
+
+      const response = await client.getAllChannels.$get({
+        query: { skip: 0, take: 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('data');
+        expect(data).toHaveProperty('total');
+        expect(Array.isArray(data.data)).toBe(true);
+        expect(data.total).toBeGreaterThanOrEqual(3);
+      }
+    });
+
+    it('应该支持分页参数', async () => {
+      const response = await client.getAllChannels.$get({
+        query: { skip: 0, take: 5 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+    });
+  });
+
+  describe('GET /channel/searchChannels', () => {
+    it('应该按名称搜索渠道', async () => {
+      // 创建测试渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+
+      const channel = channelRepository.create({
+        channelName: '微信小程序搜索测试',
+        channelType: '小程序',
+        contactPerson: '张三',
+        contactPhone: '13800138000',
+        status: 1
+      });
+      await channelRepository.save(channel);
+
+      const response = await client.searchChannels.$get({
+        query: { name: '微信', skip: 0, take: 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.data.length).toBeGreaterThan(0);
+        expect(data.data[0].channelName).toContain('微信');
+      }
+    });
+  });
+
+  describe('GET /channel/getChannel/:id', () => {
+    it('应该返回指定渠道的详情', async () => {
+      // 先创建一个渠道
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const channelRepository = dataSource.getRepository(Channel);
+      const testChannel = channelRepository.create({
+        channelName: `详情测试渠道_${Date.now()}`,
+        channelType: '测试',
+        contactPerson: '张三',
+        contactPhone: '13800138000',
+        status: 1
+      });
+      await channelRepository.save(testChannel);
+
+      const response = await client['getChannel/:id'].$get({
+        param: { id: testChannel.channelId }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('渠道详情响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.channelId).toBe(testChannel.channelId);
+        expect(data.channelName).toBe(testChannel.channelName);
+      }
+    });
+
+    it('应该处理不存在的渠道', async () => {
+      const response = await client['getChannel/:id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+      expect(data).toBeNull();
+    });
+  });
+});

+ 16 - 0
allin-packages/channel-module/tsconfig.json

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

+ 21 - 0
allin-packages/channel-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
+  }
+});

+ 33 - 5
docs/stories/007.001.transplant-channel-management-module.story.md

@@ -1,7 +1,7 @@
 # Story 007.001: 移植渠道管理模块(channel_info → @d8d/allin-channel-module)
 
 ## Status
-Draft
+In Progress
 
 ## Story
 **As a** 开发者,
@@ -322,16 +322,44 @@ Draft
 *此部分由开发代理在实现过程中填写*
 
 ### Agent Model Used
-{{agent_model_name_version}}
+Claude Code (d8d-model)
 
 ### Debug Log References
-Reference any debug logs or traces generated during development
+- 类型检查发现几个问题需要修复:
+  1. src/index.ts(3,1): 模块'./entities'已经导出了名为'Channel'的成员
+  2. src/routes/channel-custom.routes.ts(292,32): 路由处理函数返回类型不匹配
+  3. tests/integration/channel.integration.test.ts(5,34): 无法找到模块'@d8d/user-module'
+  4. 测试中的路由路径引用错误
 
 ### Completion Notes List
-Notes about the completion of tasks and any issues encountered
+- 已完成渠道管理模块的基本移植工作
+- 创建了完整的目录结构和配置文件
+- 完成了实体、服务、路由、验证系统的转换
+- 编写了集成测试覆盖所有端点
+- 需要修复类型检查发现的几个问题
+- 需要运行测试验证功能完整性
 
 ### File List
-List all files created, modified, or affected during story implementation
+**创建的文件:**
+- allin-packages/channel-module/package.json
+- allin-packages/channel-module/tsconfig.json
+- allin-packages/channel-module/vitest.config.ts
+- allin-packages/channel-module/src/entities/channel.entity.ts
+- allin-packages/channel-module/src/entities/index.ts
+- allin-packages/channel-module/src/services/channel.service.ts
+- allin-packages/channel-module/src/services/index.ts
+- allin-packages/channel-module/src/schemas/channel.schema.ts
+- allin-packages/channel-module/src/schemas/index.ts
+- allin-packages/channel-module/src/routes/channel-custom.routes.ts
+- allin-packages/channel-module/src/routes/channel-crud.routes.ts
+- allin-packages/channel-module/src/routes/channel.routes.ts
+- allin-packages/channel-module/src/routes/index.ts
+- allin-packages/channel-module/src/index.ts
+- allin-packages/channel-module/tests/integration/channel.integration.test.ts
+
+**修改的文件:**
+- pnpm-workspace.yaml (添加了allin-packages/*到workspace配置)
+- docs/stories/007.001.transplant-channel-management-module.story.md (更新状态和开发记录)
 
 ## QA Results
 Results from QA Agent QA review of the completed story implementation

+ 50 - 1
pnpm-lock.yaml

@@ -12,6 +12,55 @@ importers:
         specifier: ^9.2.1
         version: 9.2.1
 
+  allin-packages/channel-module:
+    dependencies:
+      '@d8d/auth-module':
+        specifier: workspace:*
+        version: link:../../packages/auth-module
+      '@d8d/shared-crud':
+        specifier: workspace:*
+        version: link:../../packages/shared-crud
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../../packages/shared-types
+      '@d8d/shared-utils':
+        specifier: workspace:*
+        version: link:../../packages/shared-utils
+      '@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:../../packages/shared-test-util
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.1
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.8.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.38.0(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.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   mini:
     dependencies:
       '@babel/runtime':
@@ -24032,7 +24081,7 @@ snapshots:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
       '@types/jsdom': 20.0.1
-      '@types/node': 24.9.1
+      '@types/node': 22.19.1
       jest-mock: 29.7.0
       jest-util: 29.7.0
       jsdom: 20.0.3

+ 2 - 1
pnpm-workspace.yaml

@@ -1,4 +1,5 @@
 packages:
   - 'mini'
   - 'web'
-  - 'packages/*'
+  - 'packages/*'
+  - 'allin-packages/*'