Procházet zdrojové kódy

✨ feat(delivery-address-module): 创建配送地址管理模块

- 初始化配送地址模块结构,包含实体、路由、服务和模式定义
- 实现配送地址CRUD功能,使用shared-crud基础设施
- 添加用户地址关联、省市区数据关联和默认地址管理功能
- 实现地址验证、用户追踪和权限控制

📝 docs(story): 更新配送地址模块任务清单

- 修改Task 4,明确使用shared-crud基础设施迁移CRUD路由
- 调整任务编号,将原Task 6重命名为Task 5
- 添加新的Task 6,包含系统集成相关子任务
yourname před 1 měsícem
rodič
revize
1121d3c967

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

@@ -48,19 +48,11 @@ Draft
 
 - [ ] Task 4: 创建配送地址路由 (AC: 3, 4)
   - [ ] 创建 packages/delivery-address-module/src/routes/index.ts
-  - [ ] 实现配送地址的完整CRUD路由
+  - [ ] 迁移配送地址的完整CRUD路由,使用 shared-crud 基础设施, 原文件 packages/server/src/api/delivery-address/index.ts 
   - [ ] 集成认证中间件
   - [ ] 配置用户追踪字段
 
-- [ ] Task 5: 集成到现有系统 (AC: 5, 6, 8)
-  - [ ] 更新 server package 依赖,添加 @d8d/delivery-address-module
-  - [ ] 在 server 中注册配送地址路由
-  - [ ] 验证与用户模块的集成
-  - [ ] 验证与 @d8d/geo-areas 包的集成
-  - [ ] 确保省市区数据关联正常工作
-  - [ ] 验证地址创建和更新时的地区验证
-
-- [ ] Task 6: 创建测试套件 (AC: 7, 8)
+- [ ] Task 5: 创建测试套件 (AC: 7, 8)
   - [ ] 创建集成测试 packages/delivery-address-module/tests/integration/,参考广告模块集成测试结构 [Source: packages/advertisements-module/tests/integration/advertisements.integration.test.ts#L1-L50]
   - [ ] 配置测试数据库连接,使用 shared-test-util [Source: packages/shared-test-util/src/integration-test-db.ts#L1-L30]
   - [ ] 添加省市区关联测试场景
@@ -68,6 +60,14 @@ Draft
   - [ ] 测试地址查询时的地区数据关联
   - [ ] 确保测试覆盖率满足要求
 
+- [ ] Task 6: 集成到现有系统 (AC: 5, 6, 8)
+  - [ ] 更新 server package 依赖,添加 @d8d/delivery-address-module
+  - [ ] 在 server 中注册配送地址路由
+  - [ ] 验证与用户模块的集成
+  - [ ] 验证与 @d8d/geo-areas 包的集成
+  - [ ] 确保省市区数据关联正常工作
+  - [ ] 验证地址创建和更新时的地区验证
+
 - [ ] Task 7: 验证和文档 (AC: 4, 6)
   - [ ] 运行所有测试验证功能
   - [ ] 更新 docs/architecture/source-tree.md 文档

+ 79 - 0
packages/delivery-address-module/package.json

@@ -0,0 +1,79 @@
+{
+  "name": "@d8d/delivery-address-module",
+  "version": "1.0.0",
+  "description": "配送地址管理模块 - 提供用户配送地址的完整CRUD功能,支持省市区关联",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./services": {
+      "types": "./src/services/index.ts",
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts"
+    },
+    "./schemas": {
+      "types": "./src/schemas/index.ts",
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts"
+    },
+    "./routes": {
+      "types": "./src/routes/index.ts",
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts"
+    },
+    "./entities": {
+      "types": "./src/entities/index.ts",
+      "import": "./src/entities/index.ts",
+      "require": "./src/entities/index.ts"
+    }
+  },
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/user-module": "workspace:*",
+    "@d8d/auth-module": "workspace:*",
+    "@d8d/geo-areas": "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": [
+    "delivery-address",
+    "address",
+    "shipping",
+    "crud",
+    "api"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 71 - 0
packages/delivery-address-module/src/entities/delivery-address.entity.ts

@@ -0,0 +1,71 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { UserEntity } from '@d8d/user-module';
+import { AreaEntity } from '@d8d/geo-areas';
+
+@Entity('delivery_address')
+export class DeliveryAddress {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户id' })
+  userId!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '姓名' })
+  name!: string;
+
+  @Column({ name: 'phone', type: 'varchar', length: 255, comment: '手机号' })
+  phone!: string;
+
+  @Column({ name: 'address', type: 'varchar', length: 255, comment: '详细地址' })
+  address!: string;
+
+  @Column({ name: 'receiver_province', type: 'bigint', unsigned: true, default: 0, comment: '省' })
+  receiverProvince!: number;
+
+  @Column({ name: 'receiver_city', type: 'bigint', unsigned: true, default: 0, comment: '市' })
+  receiverCity!: number;
+
+  @Column({ name: 'receiver_district', type: 'bigint', unsigned: true, default: 0, comment: '区' })
+  receiverDistrict!: number;
+
+  @Column({ name: 'receiver_town', type: 'bigint', unsigned: true, default: 0, comment: '街道' })
+  receiverTown!: number;
+
+  @Column({ name: 'state', type: 'int', unsigned: true, default: 1, comment: '是否可用1正常2禁用3删除(不显示)' })
+  state!: number;
+
+  @Column({ name: 'is_default', type: 'int', unsigned: true, default: 0, comment: '是否常用1是 2否' })
+  isDefault!: number;
+
+  @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;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => UserEntity)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: UserEntity;
+
+  @ManyToOne(() => AreaEntity)
+  @JoinColumn({ name: 'receiver_province', referencedColumnName: 'id' })
+  province!: AreaEntity;
+
+  @ManyToOne(() => AreaEntity)
+  @JoinColumn({ name: 'receiver_city', referencedColumnName: 'id' })
+  city!: AreaEntity;
+
+  @ManyToOne(() => AreaEntity)
+  @JoinColumn({ name: 'receiver_district', referencedColumnName: 'id' })
+  district!: AreaEntity;
+
+  @ManyToOne(() => AreaEntity)
+  @JoinColumn({ name: 'receiver_town', referencedColumnName: 'id' })
+  town!: AreaEntity;
+}

+ 1 - 0
packages/delivery-address-module/src/entities/index.ts

@@ -0,0 +1 @@
+export { DeliveryAddress } from './delivery-address.entity';

+ 4 - 0
packages/delivery-address-module/src/index.ts

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

+ 21 - 0
packages/delivery-address-module/src/routes/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { DeliveryAddress } from '../entities';
+import { DeliveryAddressSchema, CreateDeliveryAddressDto, UpdateDeliveryAddressDto } from '../schemas';
+import { authMiddleware } from '@d8d/auth-module';
+
+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 deliveryAddressRoutes;

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

@@ -0,0 +1,293 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@d8d/user-module';
+import { AreaSchema } 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
+export const DeliveryAddressSchema = 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: AreaSchema.optional().openapi({
+    description: '关联省份信息'
+  }),
+  city: AreaSchema.optional().openapi({
+    description: '关联城市信息'
+  }),
+  district: AreaSchema.optional().openapi({
+    description: '关联区县信息'
+  }),
+  town: AreaSchema.optional().openapi({
+    description: '关联街道信息'
+  })
+});
+
+// 创建DTO
+export const CreateDeliveryAddressDto = 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 UpdateDeliveryAddressDto = 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
+    })
+});

+ 7 - 0
packages/delivery-address-module/src/schemas/index.ts

@@ -0,0 +1,7 @@
+export {
+  DeliveryAddressSchema,
+  CreateDeliveryAddressDto,
+  UpdateDeliveryAddressDto,
+  DeliveryAddressStatusEnum,
+  IsDefaultEnum
+} from './delivery-address.schema';

+ 156 - 0
packages/delivery-address-module/src/services/delivery-address.service.ts

@@ -0,0 +1,156 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { DeliveryAddress } from '../entities';
+import { AreaService } from '@d8d/geo-areas';
+import { AreaLevel } from '@d8d/geo-areas';
+
+export class DeliveryAddressService extends GenericCrudService<DeliveryAddress> {
+  private areaService: AreaService;
+
+  constructor(dataSource: DataSource, areaService: AreaService) {
+    super(dataSource, DeliveryAddress);
+    this.areaService = areaService;
+  }
+
+  /**
+   * 获取用户的收货地址列表
+   * @param userId 用户ID
+   * @returns 收货地址列表
+   */
+  async findByUser(userId: number): Promise<DeliveryAddress[]> {
+    return this.repository.find({
+      where: { userId, state: 1 },
+      relations: ['user', 'province', 'city', 'district', 'town'],
+      order: { isDefault: 'DESC', createdAt: 'DESC' }
+    });
+  }
+
+  /**
+   * 设置默认地址
+   * @param id 地址ID
+   * @param userId 用户ID
+   * @returns 是否设置成功
+   */
+  async setDefault(id: number, userId: number): Promise<boolean> {
+    await this.repository.manager.transaction(async (transactionalEntityManager) => {
+      // 先将该用户的所有地址设为非默认
+      await transactionalEntityManager.update(DeliveryAddress,
+        { userId },
+        { isDefault: 0 }
+      );
+
+      // 将指定地址设为默认
+      await transactionalEntityManager.update(DeliveryAddress,
+        { id, userId },
+        { isDefault: 1 }
+      );
+    });
+
+    return true;
+  }
+
+  /**
+   * 获取用户的默认地址
+   * @param userId 用户ID
+   * @returns 默认地址或null
+   */
+  async findDefaultByUser(userId: number): Promise<DeliveryAddress | null> {
+    return this.repository.findOne({
+      where: { userId, isDefault: 1, state: 1 },
+      relations: ['user', 'province', 'city', 'district', 'town']
+    });
+  }
+
+  /**
+   * 验证地区数据
+   * @param provinceId 省份ID
+   * @param cityId 城市ID
+   * @param districtId 区县ID
+   * @param townId 街道ID
+   * @returns 验证结果
+   */
+  async validateAreaData(
+    provinceId: number,
+    cityId: number,
+    districtId: number,
+    townId: number
+  ): Promise<boolean> {
+    try {
+      // 验证省份
+      if (provinceId > 0) {
+        const province = await this.areaService.getAreaTreeByLevel(AreaLevel.PROVINCE);
+        const validProvince = province.some(area => area.id === provinceId);
+        if (!validProvince) return false;
+      }
+
+      // 验证城市
+      if (cityId > 0) {
+        const city = await this.areaService.getAreaTreeByLevel(AreaLevel.CITY);
+        const validCity = city.some(area => area.id === cityId);
+        if (!validCity) return false;
+      }
+
+      // 验证区县
+      if (districtId > 0) {
+        const district = await this.areaService.getAreaTreeByLevel(AreaLevel.DISTRICT);
+        const validDistrict = district.some(area => area.id === districtId);
+        if (!validDistrict) return false;
+      }
+
+      // 验证街道(如果支持)
+      if (townId > 0) {
+        // 街道级别的验证可以根据需要扩展
+        // 目前假设街道ID是有效的
+        return true;
+      }
+
+      return true;
+    } catch (error) {
+      console.error('地区数据验证失败:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 创建配送地址(包含地区验证)
+   * @param data 创建数据
+   * @returns 创建的地址
+   */
+  async createWithValidation(data: Partial<DeliveryAddress>): Promise<DeliveryAddress> {
+    // 验证地区数据
+    const isValid = await this.validateAreaData(
+      data.receiverProvince || 0,
+      data.receiverCity || 0,
+      data.receiverDistrict || 0,
+      data.receiverTown || 0
+    );
+
+    if (!isValid) {
+      throw new Error('地区数据验证失败,请检查省市区信息是否正确');
+    }
+
+    return this.create(data);
+  }
+
+  /**
+   * 更新配送地址(包含地区验证)
+   * @param id 地址ID
+   * @param data 更新数据
+   * @returns 更新的地址
+   */
+  async updateWithValidation(id: number, data: Partial<DeliveryAddress>): Promise<DeliveryAddress | null> {
+    // 验证地区数据
+    const isValid = await this.validateAreaData(
+      data.receiverProvince || 0,
+      data.receiverCity || 0,
+      data.receiverDistrict || 0,
+      data.receiverTown || 0
+    );
+
+    if (!isValid) {
+      throw new Error('地区数据验证失败,请检查省市区信息是否正确');
+    }
+
+    return this.update(id, data);
+  }
+}

+ 1 - 0
packages/delivery-address-module/src/services/index.ts

@@ -0,0 +1 @@
+export { DeliveryAddressService } from './delivery-address.service';

+ 31 - 0
packages/delivery-address-module/src/types/delivery-address.types.ts

@@ -0,0 +1,31 @@
+import { z } from '@hono/zod-openapi';
+import { DeliveryAddressSchema, CreateDeliveryAddressDto, UpdateDeliveryAddressDto } from '../schemas';
+
+export type DeliveryAddress = z.infer<typeof DeliveryAddressSchema>;
+export type CreateDeliveryAddressDto = z.infer<typeof CreateDeliveryAddressDto>;
+export type UpdateDeliveryAddressDto = z.infer<typeof UpdateDeliveryAddressDto>;
+
+// 状态枚举
+export enum DeliveryAddressStatus {
+  NORMAL = 1,
+  DISABLED = 2,
+  DELETED = 3
+}
+
+// 默认地址枚举
+export enum IsDefaultEnum {
+  NOT_DEFAULT = 0,
+  IS_DEFAULT = 1
+}
+
+// 地址查询参数
+export interface FindDeliveryAddressParams {
+  userId: number;
+  state?: DeliveryAddressStatus;
+}
+
+// 设置默认地址参数
+export interface SetDefaultAddressParams {
+  id: number;
+  userId: number;
+}

+ 16 - 0
packages/delivery-address-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
packages/delivery-address-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
+  }
+});

+ 55 - 0
pnpm-lock.yaml

@@ -339,6 +339,61 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/delivery-address-module:
+    dependencies:
+      '@d8d/auth-module':
+        specifier: workspace:*
+        version: link:../auth-module
+      '@d8d/geo-areas':
+        specifier: workspace:*
+        version: link:../geo-areas
+      '@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/user-module':
+        specifier: workspace:*
+        version: link:../user-module
+      '@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.0
+      '@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.0)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/file-module:
     dependencies:
       '@d8d/auth-module':