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

✨ feat(tenant): 集成租户模块并添加相关路由

- 添加租户模块依赖到server项目
- 注册租户实体到数据源
- 添加租户相关路由(/api/v1/tenants和/api/v1/tenant-auth)

♻️ refactor(schema): 重构用户响应模型

- 移除UserSchemaMt依赖,直接定义UserResponseSchema
- 在file-module-mt中添加UserSchemaMt定义
- 统一用户响应模型结构和字段描述

🐛 fix(schema): 修复租户配置类型定义

- 将tenant schema中的config字段类型从z.record(z.any())改为z.record(z.string(), z.any())
- 确保配置对象键为字符串类型
yourname 1 месяц назад
Родитель
Сommit
94470a8a8e

+ 81 - 2
packages/core-module-mt/auth-module-mt/src/schemas/auth.schema.mt.ts

@@ -1,5 +1,4 @@
 import { z } from '@hono/zod-openapi';
-import { UserSchemaMt } from '@d8d/core-module-mt/user-module-mt';
 
 export const LoginSchema = z.object({
   username: z.string().min(3).openapi({
@@ -38,7 +37,87 @@ export const MiniLoginSchema = z.object({
   }).optional()
 });
 
-export const UserResponseSchema = UserSchemaMt.omit({ password: true });
+export const UserResponseSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '用户ID' }),
+  tenantId: z.number().int().positive().openapi({ description: '租户ID' }),
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.string().email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  avatarFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'avatar.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/avatar.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '头像文件信息'
+  }),
+  openid: z.string().max(255).nullable().optional().openapi({
+    example: 'oABCDEFGH123456789',
+    description: '微信小程序openid'
+  }),
+  unionid: z.string().max(255).nullable().optional().openapi({
+    example: 'unionid123456789',
+    description: '微信unionid'
+  }),
+  registrationSource: z.string().max(20).default('web').openapi({
+    example: 'miniapp',
+    description: '注册来源: web, miniapp'
+  }),
+  isDisabled: z.number().int().min(0).max(1).default(0).openapi({
+    example: 0,
+    description: '是否禁用(0:启用,1:禁用)'
+  }),
+  isDeleted: z.number().int().min(0).max(1).default(0).openapi({
+    example: 0,
+    description: '是否删除(0:未删除,1:已删除)'
+  }),
+  roles: z.array(z.object({
+    id: z.number().int().positive().openapi({ description: '角色ID' }),
+    tenantId: z.number().int().positive().openapi({ description: '租户ID' }),
+    name: z.string().max(255).openapi({ description: '角色名称', example: 'admin' }),
+    description: z.string().max(255).nullable().openapi({ description: '角色描述', example: '管理员' }),
+    permissions: z.array(z.string()).openapi({ description: '权限列表', example: ['user:create'] }),
+    createdAt: z.coerce.date().openapi({ description: '创建时间' }),
+    updatedAt: z.coerce.date().openapi({ description: '更新时间' })
+  })).optional().openapi({
+    example: [
+      {
+        id: 1,
+        tenantId: 1,
+        name: 'admin',
+        description: '管理员',
+        permissions: ['user:create'],
+        createdAt: new Date(),
+        updatedAt: new Date()
+      }
+    ],
+    description: '用户角色列表'
+  }),
+  createdAt: z.coerce.date().openapi({ description: '创建时间' }),
+  updatedAt: z.coerce.date().openapi({ description: '更新时间' })
+});
 
 export const TokenResponseSchema = z.object({
   token: z.string().openapi({

+ 82 - 1
packages/core-module-mt/file-module-mt/src/schemas/file.schema.mt.ts

@@ -1,5 +1,86 @@
 import { z } from '@hono/zod-openapi';
-import { UserSchemaMt } from '@d8d/core-module-mt/user-module-mt/schemas';
+
+export const UserSchemaMt = z.object({
+  id: z.number().int().positive().openapi({ description: '用户ID' }),
+  tenantId: z.number().int().positive().openapi({ description: '租户ID' }),
+  username: z.string().min(3, '用户名至少3个字符').max(255, '用户名最多255个字符').openapi({
+    example: 'admin',
+    description: '用户名,3-255个字符'
+  }),
+  phone: z.string().max(255, '手机号最多255个字符').nullable().openapi({
+    example: '13800138000',
+    description: '手机号'
+  }),
+  email: z.string().email('请输入正确的邮箱格式').max(255, '邮箱最多255个字符').nullable().openapi({
+    example: 'user@example.com',
+    description: '邮箱'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().openapi({
+    example: '昵称',
+    description: '用户昵称'
+  }),
+  name: z.string().max(255, '姓名最多255个字符').nullable().openapi({
+    example: '张三',
+    description: '真实姓名'
+  }),
+  avatarFileId: z.number().int().positive().nullable().openapi({
+    example: 1,
+    description: '头像文件ID'
+  }),
+  avatarFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'avatar.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/avatar.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '头像文件信息'
+  }),
+  openid: z.string().max(255).nullable().optional().openapi({
+    example: 'oABCDEFGH123456789',
+    description: '微信小程序openid'
+  }),
+  unionid: z.string().max(255).nullable().optional().openapi({
+    example: 'unionid123456789',
+    description: '微信unionid'
+  }),
+  registrationSource: z.string().max(20).default('web').openapi({
+    example: 'miniapp',
+    description: '注册来源: web, miniapp'
+  }),
+  isDisabled: z.number().int().min(0).max(1).default(0).openapi({
+    example: 0,
+    description: '是否禁用(0:启用,1:禁用)'
+  }),
+  isDeleted: z.number().int().min(0).max(1).default(0).openapi({
+    example: 0,
+    description: '是否删除(0:未删除,1:已删除)'
+  }),
+  roles: z.array(z.object({
+    id: z.number().int().positive().openapi({ description: '角色ID' }),
+    tenantId: z.number().int().positive().openapi({ description: '租户ID' }),
+    name: z.string().max(255).openapi({ description: '角色名称', example: 'admin' }),
+    description: z.string().max(255).nullable().openapi({ description: '角色描述', example: '管理员' }),
+    permissions: z.array(z.string()).openapi({ description: '权限列表', example: ['user:create'] }),
+    createdAt: z.coerce.date().openapi({ description: '创建时间' }),
+    updatedAt: z.coerce.date().openapi({ description: '更新时间' })
+  })).optional().openapi({
+    example: [
+      {
+        id: 1,
+        tenantId: 1,
+        name: 'admin',
+        description: '管理员',
+        permissions: ['user:create'],
+        createdAt: new Date(),
+        updatedAt: new Date()
+      }
+    ],
+    description: '用户角色列表'
+  }),
+  createdAt: z.coerce.date().openapi({ description: '创建时间' }),
+  updatedAt: z.coerce.date().openapi({ description: '更新时间' })
+});
 
 export const FileSchema = z.object({
   id: z.number().int().positive().openapi({

+ 1 - 0
packages/server/package.json

@@ -37,6 +37,7 @@
     "@d8d/user-module-mt": "workspace:*",
     "@d8d/auth-module-mt": "workspace:*",
     "@d8d/file-module-mt": "workspace:*",
+    "@d8d/tenant-module-mt": "workspace:*",
     "@d8d/geo-areas-mt": "workspace:*",
     "@d8d/mini-payment-mt": "workspace:*",
     "@d8d/advertisements-module-mt": "workspace:*",

+ 5 - 0
packages/server/src/index.ts

@@ -4,11 +4,13 @@ import { errorHandler, initializeDataSource } from '@d8d/shared-utils'
 import { userRoutesMt as userModuleRoutes, roleRoutesMt as roleModuleRoutes } from '@d8d/user-module-mt'
 import { authRoutes as authModuleRoutes } from '@d8d/auth-module-mt'
 import { fileRoutesMt as fileModuleRoutes } from '@d8d/file-module-mt'
+import { tenantRoutes, authRoutes as tenantAuthRoutes } from '@d8d/tenant-module-mt'
 import { AuthContext } from '@d8d/shared-types'
 import { AppDataSource } from '@d8d/shared-utils'
 import { Hono } from 'hono'
 import { UserEntityMt, RoleMt } from '@d8d/user-module-mt'
 import { FileMt } from '@d8d/file-module-mt'
+import { TenantEntityMt } from '@d8d/tenant-module-mt'
 
 // 导入已实现的包实体
 import { AreaEntityMt } from '@d8d/geo-areas-mt'
@@ -24,6 +26,7 @@ if(!AppDataSource || !AppDataSource.isInitialized) {
   initializeDataSource([
     // 已实现的包实体
     UserEntityMt, RoleMt, FileMt,
+    TenantEntityMt,
     AreaEntityMt, PaymentMtEntity,
     Advertisement, AdvertisementType,
     DeliveryAddressMt,
@@ -126,6 +129,8 @@ export const userRoutes = api.route('/api/v1/users', userModuleRoutes)
 export const authRoutes = api.route('/api/v1/auth', authModuleRoutes)
 export const fileApiRoutes = api.route('/api/v1/files', fileModuleRoutes)
 export const roleRoutes = api.route('/api/v1/roles', roleModuleRoutes)
+export const tenantApiRoutes = api.route('/api/v1/tenants', tenantRoutes)
+export const tenantAuthApiRoutes = api.route('/api/v1/tenant-auth', tenantAuthRoutes)
 
 // 导入已实现的包路由
 import { areasRoutesMt, adminAreasRoutesMt } from '@d8d/geo-areas-mt'

+ 2 - 1
packages/tenant-module-mt/src/routes/index.ts

@@ -17,7 +17,8 @@ export const tenantRoutes = createCrudRoutes({
     updatedByField: 'updatedBy'
   },
   dataPermission: {
-    enabled: false // 租户管理不使用数据权限控制,由固定的超级管理员账号管理
+    enabled: false, // 租户管理不使用数据权限控制,由固定的超级管理员账号管理
+    userIdField: 'id'
   }
 });
 

+ 3 - 3
packages/tenant-module-mt/src/schemas/tenant.schema.ts

@@ -22,7 +22,7 @@ export const TenantSchema = z.object({
     description: '状态 1启用 2禁用',
     example: 1
   }),
-  config: z.record(z.any()).nullable().optional().openapi({
+  config: z.record(z.string(), z.any()).nullable().optional().openapi({
     description: '租户配置',
     example: { theme: 'dark', language: 'zh-CN' }
   } as const),
@@ -73,7 +73,7 @@ export const CreateTenantDto = z.object({
     description: '状态 1启用 2禁用',
     example: 1
   }),
-  config: z.record(z.any()).nullable().optional().openapi({
+  config: z.record(z.string(), z.any()).nullable().optional().openapi({
     description: '租户配置',
     example: { theme: 'dark', language: 'zh-CN' }
   } as const),
@@ -108,7 +108,7 @@ export const UpdateTenantDto = z.object({
     description: '状态 1启用 2禁用',
     example: 1
   }),
-  config: z.record(z.any()).nullable().optional().openapi({
+  config: z.record(z.string(), z.any()).nullable().optional().openapi({
     description: '租户配置',
     example: { theme: 'dark', language: 'zh-CN' }
   } as const),

+ 3 - 0
pnpm-lock.yaml

@@ -3297,6 +3297,9 @@ importers:
       '@d8d/supplier-module-mt':
         specifier: workspace:*
         version: link:../supplier-module-mt
+      '@d8d/tenant-module-mt':
+        specifier: workspace:*
+        version: link:../tenant-module-mt
       '@d8d/user-module-mt':
         specifier: workspace:*
         version: link:../user-module-mt