Преглед изворни кода

✨ feat(areas): add area disabled status filter

- add isDisabled filter to area queries to only return enabled areas
- ensure disabled areas are excluded from all area-related API responses

✅ test(areas): add area API integration tests

- create comprehensive test suite for area API endpoints
- test province, city and district retrieval functionality
- verify disabled areas are properly filtered out
- add pagination and parameter validation tests
yourname пре 3 месеци
родитељ
комит
740ad4d4c8

+ 8 - 5
packages/server/src/modules/areas/area.service.ts

@@ -29,7 +29,8 @@ export class AreaService {
     const areas = await this.areaRepository.find({
       where: {
         level,
-        isDeleted: 0
+        isDeleted: 0,
+        isDisabled: DisabledStatus.ENABLED
       },
       relations: ['children'],
       order: {
@@ -49,7 +50,8 @@ export class AreaService {
     const area = await this.areaRepository.findOne({
       where: {
         id: areaId,
-        isDeleted: 0
+        isDeleted: 0,
+        isDisabled: DisabledStatus.ENABLED
       },
       relations: ['children'],
     });
@@ -70,7 +72,8 @@ export class AreaService {
         const fullChild = await this.areaRepository.findOne({
           where: {
             id: child.id,
-            isDeleted: 0
+            isDeleted: 0,
+            isDisabled: DisabledStatus.ENABLED
           },
           relations: ['children'],
         });
@@ -119,7 +122,7 @@ export class AreaService {
   async getAreaPath(areaId: number): Promise<AreaEntity[]> {
     const path: AreaEntity[] = [];
     let currentArea = await this.areaRepository.findOne({
-      where: { id: areaId, isDeleted: 0 }
+      where: { id: areaId, isDeleted: 0, isDisabled: DisabledStatus.ENABLED }
     });
 
     while (currentArea) {
@@ -130,7 +133,7 @@ export class AreaService {
       }
 
       currentArea = await this.areaRepository.findOne({
-        where: { id: currentArea.parentId, isDeleted: 0 }
+        where: { id: currentArea.parentId, isDeleted: 0, isDisabled: DisabledStatus.ENABLED }
       });
     }
 

+ 348 - 0
web/tests/integration/server/api/areas/index.test.ts

@@ -0,0 +1,348 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooks,
+  TestDataFactory
+} from '~/utils/server/integration-test-db';
+import { areasUserRoutesExport as areaRoutes } from '@d8d/server/api';
+import { AreaLevel, AreaEntity } from '@d8d/server/modules/areas/area.entity';
+import { DisabledStatus } from '@d8d/server/share/types';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooks()
+
+describe('区域API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof areaRoutes>>['api']['v1']['areas'];
+  let testAreas: AreaEntity[];
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(areaRoutes).api.v1.areas;
+
+    // 创建测试数据
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建启用状态的省份
+    const province1 = await TestDataFactory.createTestArea(dataSource, {
+      name: '北京市',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const province2 = await TestDataFactory.createTestArea(dataSource, {
+      name: '上海市',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const province3 = await TestDataFactory.createTestArea(dataSource, {
+      name: '广东省',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.ENABLED
+    });
+
+    // 创建启用状态的城市
+    const city11 = await TestDataFactory.createTestArea(dataSource, {
+      name: '北京市',
+      level: AreaLevel.CITY,
+      parentId: province1.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city12 = await TestDataFactory.createTestArea(dataSource, {
+      name: '朝阳区',
+      level: AreaLevel.CITY,
+      parentId: province1.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city13 = await TestDataFactory.createTestArea(dataSource, {
+      name: '海淀区',
+      level: AreaLevel.CITY,
+      parentId: province1.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city21 = await TestDataFactory.createTestArea(dataSource, {
+      name: '上海市',
+      level: AreaLevel.CITY,
+      parentId: province2.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city22 = await TestDataFactory.createTestArea(dataSource, {
+      name: '浦东新区',
+      level: AreaLevel.CITY,
+      parentId: province2.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+
+    // 创建启用状态的区县
+    const district101 = await TestDataFactory.createTestArea(dataSource, {
+      name: '朝阳区',
+      level: AreaLevel.DISTRICT,
+      parentId: city12.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const district102 = await TestDataFactory.createTestArea(dataSource, {
+      name: '海淀区',
+      level: AreaLevel.DISTRICT,
+      parentId: city13.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const district103 = await TestDataFactory.createTestArea(dataSource, {
+      name: '西城区',
+      level: AreaLevel.DISTRICT,
+      parentId: city12.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const district201 = await TestDataFactory.createTestArea(dataSource, {
+      name: '浦东新区',
+      level: AreaLevel.DISTRICT,
+      parentId: city22.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+
+    // 创建禁用状态的区域用于测试过滤
+    const disabledProvince = await TestDataFactory.createTestArea(dataSource, {
+      name: '禁用省份',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.DISABLED
+    });
+    const disabledCity = await TestDataFactory.createTestArea(dataSource, {
+      name: '禁用城市',
+      level: AreaLevel.CITY,
+      parentId: province3.id,
+      isDisabled: DisabledStatus.DISABLED
+    });
+    const disabledDistrict = await TestDataFactory.createTestArea(dataSource, {
+      name: '禁用区县',
+      level: AreaLevel.DISTRICT,
+      parentId: city12.id,
+      isDisabled: DisabledStatus.DISABLED
+    });
+
+    testAreas = [
+      province1, province2, province3,
+      city11, city12, city13, city21, city22,
+      district101, district102, district103, district201,
+      disabledProvince, disabledCity, disabledDistrict
+    ];
+  });
+
+  describe('GET /areas/provinces', () => {
+    it('应该成功获取启用状态的省份列表', async () => {
+      const response = await client.provinces.$get({
+        query: { page: 1, pageSize: 50 }
+      });
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证响应数据格式
+        expect(data).toHaveProperty('success', true);
+        expect(data).toHaveProperty('data');
+        expect(data.data).toHaveProperty('provinces');
+        expect(data.data).toHaveProperty('pagination');
+
+        // 验证只返回启用状态的省份
+        const provinces = data.data.provinces;
+        expect(provinces).toHaveLength(3); // 只返回3个启用状态的省份
+
+        // 验证不包含禁用状态的省份
+        const disabledProvince = provinces.find((p: any) => p.id === 4);
+        expect(disabledProvince).toBeUndefined();
+
+        // 验证分页信息
+        expect(data.data.pagination).toEqual({
+          page: 1,
+          pageSize: 50,
+          total: 3,
+          totalPages: 1
+        });
+      }
+    });
+
+    it('应该正确处理分页参数', async () => {
+      const response = await client.provinces.$get({
+        query: { page: 1, pageSize: 2 }
+      });
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证分页结果
+        expect(data.data.provinces).toHaveLength(2);
+        expect(data.data.pagination).toEqual({
+          page: 1,
+          pageSize: 2,
+          total: 3,
+          totalPages: 2
+        });
+      }
+    });
+  });
+
+  describe('GET /areas/cities', () => {
+    it('应该成功获取指定省份下启用状态的城市列表', async () => {
+      const response = await client.cities.$get({
+        query: { provinceId: testAreas[0].id, page: 1, pageSize: 50 }
+      });
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证响应数据格式
+        expect(data).toHaveProperty('success', true);
+        expect(data).toHaveProperty('data');
+        expect(data.data).toHaveProperty('cities');
+
+        // 验证只返回启用状态的城市
+        const cities = data.data.cities;
+        expect(cities).toHaveLength(3); // 北京市下有3个启用状态的城市
+
+        // 验证城市数据正确
+        const cityNames = cities.map((c: any) => c.name);
+        expect(cityNames).toContain('北京市');
+        expect(cityNames).toContain('朝阳区');
+        expect(cityNames).toContain('海淀区');
+
+        // 验证不包含禁用状态的城市
+        const disabledCity = cities.find((c: any) => c.name === '禁用城市');
+        expect(disabledCity).toBeUndefined();
+      }
+    });
+
+    it('应该处理不存在的省份ID', async () => {
+      const response = await client.cities.$get({
+        query: { provinceId: 999, page: 1, pageSize: 50 }
+      });
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 不存在的省份应该返回空数组
+        expect(data.data.cities).toHaveLength(0);
+      }
+    });
+
+    it('应该验证省份ID参数', async () => {
+      const response = await client.cities.$get({
+        query: { provinceId: 0, page: 1, pageSize: 50 }
+      });
+
+      // 参数验证应该返回400错误
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /areas/districts', () => {
+    it('应该成功获取指定城市下启用状态的区县列表', async () => {
+      // 找到朝阳区城市对象
+      const chaoyangCity = testAreas.find(area => area.name === '朝阳区' && area.level === AreaLevel.CITY);
+      expect(chaoyangCity).toBeDefined();
+
+      const response = await client.districts.$get({
+        query: { cityId: chaoyangCity!.id, page: 1, pageSize: 50 }
+      });
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证响应数据格式
+        expect(data).toHaveProperty('success', true);
+        expect(data).toHaveProperty('data');
+        expect(data.data).toHaveProperty('districts');
+
+        // 验证只返回启用状态的区县
+        const districts = data.data.districts;
+        expect(districts).toHaveLength(2); // 朝阳区下有2个启用状态的区县
+
+        // 验证区县数据正确
+        const districtNames = districts.map((d: any) => d.name);
+        expect(districtNames).toContain('朝阳区');
+        expect(districtNames).toContain('西城区');
+
+        // 验证不包含禁用状态的区县
+        const disabledDistrict = districts.find((d: any) => d.name === '禁用区县');
+        expect(disabledDistrict).toBeUndefined();
+      }
+    });
+
+    it('应该处理不存在的城市ID', async () => {
+      const response = await client.districts.$get({
+        query: { cityId: 999, page: 1, pageSize: 50 }
+      });
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 不存在的城市应该返回空数组
+        expect(data.data.districts).toHaveLength(0);
+      }
+    });
+
+    it('应该验证城市ID参数', async () => {
+      const response = await client.districts.$get({
+        query: { cityId: 0, page: 1, pageSize: 50 }
+      });
+
+      // 参数验证应该返回400错误
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('过滤禁用状态验证', () => {
+    it('应该确保所有API只返回启用状态的区域', async () => {
+      // 测试省份API
+      const provincesResponse = await client.provinces.$get({
+        query: { page: 1, pageSize: 50 }
+      });
+      expect(provincesResponse.status).toBe(200);
+      const provincesData = await provincesResponse.json();
+
+      // 验证省份不包含禁用状态
+      if ('data' in provincesData) {
+        const provinces = provincesData.data.provinces;
+        const hasDisabledProvince = provinces.some((p: any) => p.isDisabled === DisabledStatus.DISABLED);
+        expect(hasDisabledProvince).toBe(false);
+      }
+
+      // 测试城市API
+      const citiesResponse = await client.cities.$get({
+        query: { provinceId: testAreas[0].id, page: 1, pageSize: 50 }
+      });
+      expect(citiesResponse.status).toBe(200);
+      const citiesData = await citiesResponse.json();
+
+      // 验证城市不包含禁用状态
+      if ('data' in citiesData) {
+        const cities = citiesData.data.cities;
+        const hasDisabledCity = cities.some((c: any) => c.isDisabled === DisabledStatus.DISABLED);
+        expect(hasDisabledCity).toBe(false);
+      }
+
+      // 测试区县API
+      const chaoyangCity = testAreas.find(area => area.name === '朝阳区' && area.level === AreaLevel.CITY);
+      const districtsResponse = await client.districts.$get({
+        query: { cityId: chaoyangCity!.id, page: 1, pageSize: 50 }
+      });
+      expect(districtsResponse.status).toBe(200);
+      const districtsData = await districtsResponse.json();
+
+      // 验证区县不包含禁用状态
+      if ('data' in districtsData) {
+        const districts = districtsData.data.districts;
+        const hasDisabledDistrict = districts.some((d: any) => d.isDisabled === DisabledStatus.DISABLED);
+        expect(hasDisabledDistrict).toBe(false);
+      }
+    });
+  });
+});