Bladeren bron

✨ feat(delivery-address): implement user and admin permission-based routes

- create user-delivery-address.schema.ts with userId field removed for automatic current user association
- create admin-delivery-address.schema.ts with userId field retained for admin user specification
- implement user-routes.ts with data permission control ensuring users only access their own data
- implement admin-routes.ts with full CRUD functionality without data permission restrictions
- update route exports to support both user and admin route collections
- add documentation for completed Task 5 in story file

📝 docs(delivery-address): update task status and implementation details

- mark Task 5 as completed in story documentation
- add details about user and admin route implementation
- update file list with newly created schema and route files
yourname 1 maand geleden
bovenliggende
commit
ece4eeaf95

+ 25 - 15
docs/stories/005.009.delivery-address-module.story.md

@@ -52,21 +52,21 @@ Draft
   - [x] 集成认证中间件
   - [x] 配置用户追踪字段
 
-- [ ] Task 5: 创建当前用户权限API路由文件 (AC: 3, 4)
-  - [ ] 创建 packages/delivery-address-module/src/schemas/user-delivery-address.schema.ts - 用户专用schema
-  - [ ] 移除userId字段,自动使用当前登录用户ID
-  - [ ] 创建 packages/delivery-address-module/src/schemas/admin-delivery-address.schema.ts - 管理员专用schema
-  - [ ] 保留userId字段,允许管理员指定用户
-  - [ ] 创建 packages/delivery-address-module/src/routes/user-routes.ts - 仅限当前用户使用的路由
-  - [ ] 配置数据权限控制,使用 shared-crud 的 dataPermission 配置
-  - [ ] 设置 userIdField: 'userId',确保用户只能操作自己的数据
-  - [ ] 使用用户专用schema
-  - [ ] 创建 packages/delivery-address-module/src/routes/admin-routes.ts - 管理员使用的完整权限路由
-  - [ ] 配置管理员路由不使用数据权限控制,保持完整CRUD功能
-  - [ ] 使用管理员专用schema
-  - [ ] 更新 packages/delivery-address-module/src/routes/index.ts 导出两个路由集合
-  - [ ] 验证用户路由只能访问和操作当前用户的数据
-  - [ ] 验证管理员路由可以访问所有用户的数据
+- [x] Task 5: 创建当前用户权限API路由文件 (AC: 3, 4)
+  - [x] 创建 packages/delivery-address-module/src/schemas/user-delivery-address.schema.ts - 用户专用schema
+  - [x] 移除userId字段,自动使用当前登录用户ID
+  - [x] 创建 packages/delivery-address-module/src/schemas/admin-delivery-address.schema.ts - 管理员专用schema
+  - [x] 保留userId字段,允许管理员指定用户
+  - [x] 创建 packages/delivery-address-module/src/routes/user-routes.ts - 仅限当前用户使用的路由
+  - [x] 配置数据权限控制,使用 shared-crud 的 dataPermission 配置
+  - [x] 设置 userIdField: 'userId',确保用户只能操作自己的数据
+  - [x] 使用用户专用schema
+  - [x] 创建 packages/delivery-address-module/src/routes/admin-routes.ts - 管理员使用的完整权限路由
+  - [x] 配置管理员路由不使用数据权限控制,保持完整CRUD功能
+  - [x] 使用管理员专用schema
+  - [x] 更新 packages/delivery-address-module/src/routes/index.ts 导出两个路由集合
+  - [x] 验证用户路由只能访问和操作当前用户的数据
+  - [x] 验证管理员路由可以访问所有用户的数据
 
 - [ ] Task 6: 创建测试套件 (AC: 7, 8)
   - [ ] 创建集成测试 packages/delivery-address-module/tests/integration/,参考广告模块集成测试结构 [Source: packages/advertisements-module/tests/integration/advertisements.integration.test.ts#L1-L50]
@@ -210,9 +210,19 @@ Draft
 - Task 4 已完成:成功创建配送地址路由,使用shared-crud基础设施
 - 路由文件已正确配置认证中间件和用户追踪字段
 - 类型检查通过,所有依赖导入正确
+- Task 5 已完成:成功创建用户权限API路由文件
+- 用户专用schema移除userId字段,自动使用当前登录用户ID
+- 管理员专用schema保留userId字段,允许管理员指定用户
+- 用户路由配置数据权限控制,确保用户只能操作自己的数据
+- 管理员路由不使用数据权限控制,保持完整CRUD功能
+- 路由导出文件已更新,支持两个路由集合导出
 
 ### File List
 - packages/delivery-address-module/src/routes/index.ts (新建)
+- packages/delivery-address-module/src/schemas/user-delivery-address.schema.ts (新建)
+- packages/delivery-address-module/src/schemas/admin-delivery-address.schema.ts (新建)
+- packages/delivery-address-module/src/routes/user-routes.ts (新建)
+- packages/delivery-address-module/src/routes/admin-routes.ts (新建)
 
 ## QA Results
 

+ 27 - 0
packages/delivery-address-module/src/routes/admin-routes.ts

@@ -0,0 +1,27 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { DeliveryAddress } from '../entities';
+import {
+  AdminDeliveryAddressSchema,
+  CreateAdminDeliveryAddressDto,
+  UpdateAdminDeliveryAddressDto
+} from '../schemas/admin-delivery-address.schema';
+import { authMiddleware } from '@d8d/auth-module';
+
+// 管理员专用路由 - 完整权限,不使用数据权限控制
+const adminDeliveryAddressRoutes = createCrudRoutes({
+  entity: DeliveryAddress,
+  createSchema: CreateAdminDeliveryAddressDto,
+  updateSchema: UpdateAdminDeliveryAddressDto,
+  getSchema: AdminDeliveryAddressSchema,
+  listSchema: AdminDeliveryAddressSchema,
+  searchFields: ['name', 'phone', 'address'],
+  relations: ['user', 'province', 'city', 'district', 'town'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+  // 注意:管理员路由不配置 dataPermission,保持完整CRUD功能
+});
+
+export default adminDeliveryAddressRoutes;

+ 9 - 19
packages/delivery-address-module/src/routes/index.ts

@@ -1,21 +1,11 @@
-import { createCrudRoutes } from '@d8d/shared-crud';
-import { DeliveryAddress } from '../entities';
-import { DeliveryAddressSchema, CreateDeliveryAddressDto, UpdateDeliveryAddressDto } from '../schemas';
-import { authMiddleware } from '@d8d/auth-module';
+import userDeliveryAddressRoutes from './user-routes';
+import adminDeliveryAddressRoutes from './admin-routes';
 
-const deliveryAddressRoutes = createCrudRoutes({
-  entity: DeliveryAddress,
-  createSchema: CreateDeliveryAddressDto,
-  updateSchema: UpdateDeliveryAddressDto,
-  getSchema: DeliveryAddressSchema,
-  listSchema: DeliveryAddressSchema,
-  searchFields: ['name', 'phone', 'address'],
-  relations: ['user', 'province', 'city', 'district', 'town'],
-  middleware: [authMiddleware],
-  userTracking: {
-    createdByField: 'createdBy',
-    updatedByField: 'updatedBy'
-  }
-});
+// 导出用户路由集合 - 仅限当前用户使用
+export { default as userDeliveryAddressRoutes } from './user-routes';
 
-export default deliveryAddressRoutes;
+// 导出管理员路由集合 - 完整权限
+export { default as adminDeliveryAddressRoutes } from './admin-routes';
+
+// 默认导出用户路由,保持向后兼容性
+export default userDeliveryAddressRoutes;

+ 33 - 0
packages/delivery-address-module/src/routes/user-routes.ts

@@ -0,0 +1,33 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { DeliveryAddress } from '../entities';
+import {
+  UserDeliveryAddressSchema,
+  CreateUserDeliveryAddressDto,
+  UpdateUserDeliveryAddressDto
+} from '../schemas/user-delivery-address.schema';
+import { authMiddleware } from '@d8d/auth-module';
+
+// 用户专用路由 - 仅限当前用户使用
+const userDeliveryAddressRoutes = createCrudRoutes({
+  entity: DeliveryAddress,
+  createSchema: CreateUserDeliveryAddressDto,
+  updateSchema: UpdateUserDeliveryAddressDto,
+  getSchema: UserDeliveryAddressSchema,
+  listSchema: UserDeliveryAddressSchema,
+  searchFields: ['name', 'phone', 'address'],
+  relations: ['user', 'province', 'city', 'district', 'town'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  // 配置数据权限控制,确保用户只能操作自己的数据
+  dataPermission: {
+    enabled: true,
+    userIdField: 'userId',
+    // 自动注入当前登录用户ID到查询条件
+    // 用户只能访问和操作自己创建的配送地址
+  }
+});
+
+export default userDeliveryAddressRoutes;

+ 301 - 0
packages/delivery-address-module/src/schemas/admin-delivery-address.schema.ts

@@ -0,0 +1,301 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@d8d/user-module';
+import { getAreaSchema } from '@d8d/geo-areas';
+
+// 状态枚举
+export const DeliveryAddressStatusEnum = {
+  NORMAL: 1,
+  DISABLED: 2,
+  DELETED: 3
+} as const;
+
+export const IsDefaultEnum = {
+  NOT_DEFAULT: 0,
+  IS_DEFAULT: 1
+} as const;
+
+// 管理员专用基础实体Schema - 保留userId字段
+export const AdminDeliveryAddressSchema = z.object({
+  id: z.number()
+    .int('ID必须是整数')
+    .positive('ID必须是正整数')
+    .openapi({
+      description: '收货地址ID',
+      example: 1
+    }),
+  userId: z.number()
+    .int('用户ID必须是整数')
+    .positive('用户ID必须是正整数')
+    .openapi({
+      description: '用户ID',
+      example: 1
+    }),
+  name: z.string()
+    .min(1, '姓名不能为空')
+    .max(255, '姓名最多255个字符')
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  state: z.coerce.number<number>()
+    .int('状态必须是整数')
+    .min(1, '状态最小值为1')
+    .max(3, '状态最大值为3')
+    .default(1)
+    .openapi({
+      description: '状态:1正常,2禁用,3删除',
+      example: 1
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .default(0)
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    }),
+  createdBy: z.number()
+    .int('创建人ID必须是整数')
+    .positive('创建人ID必须是正整数')
+    .nullable()
+    .openapi({
+      description: '创建用户ID',
+      example: 1
+    }),
+  updatedBy: z.number()
+    .int('更新人ID必须是整数')
+    .positive('更新人ID必须是正整数')
+    .nullable()
+    .openapi({
+      description: '更新用户ID',
+      example: 1
+    }),
+  createdAt: z.coerce.date('创建时间格式不正确')
+    .openapi({
+      description: '创建时间',
+      example: '2024-01-01T12:00:00Z'
+    }),
+  updatedAt: z.coerce.date('更新时间格式不正确')
+    .openapi({
+      description: '更新时间',
+      example: '2024-01-01T12:00:00Z'
+    }),
+  user: UserSchema.optional().openapi({
+    description: '关联用户信息'
+  }),
+  province: getAreaSchema.optional().openapi({
+    description: '关联省份信息'
+  }),
+  city: getAreaSchema.optional().openapi({
+    description: '关联城市信息'
+  }),
+  district: getAreaSchema.optional().openapi({
+    description: '关联区县信息'
+  }),
+  town: getAreaSchema.optional().openapi({
+    description: '关联街道信息'
+  })
+});
+
+// 管理员专用创建DTO - 保留userId字段
+export const CreateAdminDeliveryAddressDto = z.object({
+  userId: z.number()
+    .int('用户ID必须是整数')
+    .positive('用户ID必须是正整数')
+    .openapi({
+      description: '用户ID',
+      example: 1
+    }),
+  name: z.string()
+    .min(1, '收货人姓名不能为空')
+    .max(255, '收货人姓名最多255个字符')
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .default(0)
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    })
+});
+
+// 管理员专用更新DTO
+export const UpdateAdminDeliveryAddressDto = z.object({
+  userId: z.number()
+    .int('用户ID必须是整数')
+    .positive('用户ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '用户ID',
+      example: 1
+    }),
+  name: z.string()
+    .min(1, '收货人姓名不能为空')
+    .max(255, '收货人姓名最多255个字符')
+    .optional()
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .optional()
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .optional()
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  state: z.coerce.number<number>()
+    .int('状态必须是整数')
+    .min(1, '状态最小值为1')
+    .max(3, '状态最大值为3')
+    .optional()
+    .openapi({
+      description: '状态:1正常,2禁用,3删除',
+      example: 1
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .optional()
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    })
+});

+ 281 - 0
packages/delivery-address-module/src/schemas/user-delivery-address.schema.ts

@@ -0,0 +1,281 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@d8d/user-module';
+import { getAreaSchema } from '@d8d/geo-areas';
+
+// 状态枚举
+export const DeliveryAddressStatusEnum = {
+  NORMAL: 1,
+  DISABLED: 2,
+  DELETED: 3
+} as const;
+
+export const IsDefaultEnum = {
+  NOT_DEFAULT: 0,
+  IS_DEFAULT: 1
+} as const;
+
+// 用户专用基础实体Schema - 移除userId字段
+export const UserDeliveryAddressSchema = z.object({
+  id: z.number()
+    .int('ID必须是整数')
+    .positive('ID必须是正整数')
+    .openapi({
+      description: '收货地址ID',
+      example: 1
+    }),
+  // 注意:移除了userId字段,自动使用当前登录用户ID
+  name: z.string()
+    .min(1, '姓名不能为空')
+    .max(255, '姓名最多255个字符')
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  state: z.coerce.number<number>()
+    .int('状态必须是整数')
+    .min(1, '状态最小值为1')
+    .max(3, '状态最大值为3')
+    .default(1)
+    .openapi({
+      description: '状态:1正常,2禁用,3删除',
+      example: 1
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .default(0)
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    }),
+  createdBy: z.number()
+    .int('创建人ID必须是整数')
+    .positive('创建人ID必须是正整数')
+    .nullable()
+    .openapi({
+      description: '创建用户ID',
+      example: 1
+    }),
+  updatedBy: z.number()
+    .int('更新人ID必须是整数')
+    .positive('更新人ID必须是正整数')
+    .nullable()
+    .openapi({
+      description: '更新用户ID',
+      example: 1
+    }),
+  createdAt: z.coerce.date('创建时间格式不正确')
+    .openapi({
+      description: '创建时间',
+      example: '2024-01-01T12:00:00Z'
+    }),
+  updatedAt: z.coerce.date('更新时间格式不正确')
+    .openapi({
+      description: '更新时间',
+      example: '2024-01-01T12:00:00Z'
+    }),
+  user: UserSchema.optional().openapi({
+    description: '关联用户信息'
+  }),
+  province: getAreaSchema.optional().openapi({
+    description: '关联省份信息'
+  }),
+  city: getAreaSchema.optional().openapi({
+    description: '关联城市信息'
+  }),
+  district: getAreaSchema.optional().openapi({
+    description: '关联区县信息'
+  }),
+  town: getAreaSchema.optional().openapi({
+    description: '关联街道信息'
+  })
+});
+
+// 用户专用创建DTO - 移除userId字段
+export const CreateUserDeliveryAddressDto = z.object({
+  // 注意:移除了userId字段,自动使用当前登录用户ID
+  name: z.string()
+    .min(1, '收货人姓名不能为空')
+    .max(255, '收货人姓名最多255个字符')
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .default(0)
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    })
+});
+
+// 用户专用更新DTO
+export const UpdateUserDeliveryAddressDto = z.object({
+  name: z.string()
+    .min(1, '收货人姓名不能为空')
+    .max(255, '收货人姓名最多255个字符')
+    .optional()
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .optional()
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .optional()
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  state: z.coerce.number<number>()
+    .int('状态必须是整数')
+    .min(1, '状态最小值为1')
+    .max(3, '状态最大值为3')
+    .optional()
+    .openapi({
+      description: '状态:1正常,2禁用,3删除',
+      example: 1
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .optional()
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    })
+});